fix(提交列表): 手机上好用多了:筛选里补上「这节课」、班级、只看某个学生,顶上加了统计入口;点开详情名字、题目、按钮分三行排,代码字号改小;流程图的评分不再被压成一条缝;翻页不再顶出屏幕、翻完回到顶上;电脑上「复制回到题目」「复制到自测猫」挪到右栏底栏,老师不用再进「⋯」找,题目名也不再被按钮挤没
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc89184488
commit
c018c147c1
4 files changed
+449
-144
No files matched your search
@@ -175,6 +175,8 @@ async function listSubmissions() {
|
||||
// 翻了页、换了条件就回到列表顶上(往回翻到上一页末尾的,settleSelection 会滚到那一条)
|
||||
const scroller = document.querySelector(".rows")
|
||||
if (scroller) scroller.scrollTop = 0
|
||||
// 手机上整页滚,滚的是外层 n-layout 的容器:翻到下一页原来停在页尾
|
||||
if (isMobile.value) pageScroller()?.scrollTo({ top: 0 })
|
||||
settleSelection()
|
||||
} catch {
|
||||
// 失败了别把「落地后停在哪一头」留给下一次不相干的加载
|
||||
@@ -184,6 +186,19 @@ async function listSubmissions() {
|
||||
}
|
||||
}
|
||||
|
||||
const pageRoot = useTemplateRef<HTMLElement>("pageRoot")
|
||||
|
||||
/** 往上找真正在滚的那层(同自学页:n-layout-content 也带 scroll-container 类却不滚) */
|
||||
function pageScroller(): HTMLElement | null {
|
||||
for (let el = pageRoot.value?.parentElement; el; el = el.parentElement) {
|
||||
const overflow = getComputedStyle(el).overflowY
|
||||
if ((overflow === "auto" || overflow === "scroll") && el.scrollHeight > el.clientHeight) {
|
||||
return el
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function getTodayCount() {
|
||||
todayCount.value = await getTodaySubmissionCount(flowMode.value ? "Flowchart" : undefined)
|
||||
}
|
||||
@@ -589,6 +604,20 @@ function openStatistics() {
|
||||
statsPanel.value = true
|
||||
}
|
||||
|
||||
/** 手机上的「统计」:同样的条件,带去统计页 */
|
||||
function openStatisticsPage() {
|
||||
router.push({
|
||||
name: "statistics",
|
||||
query: {
|
||||
tab: flowMode.value ? "flow" : "code",
|
||||
...(query.className ? { className: query.className } : {}),
|
||||
...(query.username ? { username: query.username } : {}),
|
||||
...(query.problem ? { problem: query.problem } : {}),
|
||||
...(query.today === "1" ? { period: "today" } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 统计里的「在提交列表里看」:直接改背后这张列表的筛选,关掉弹框 */
|
||||
function listFromStatistics(next: Record<string, string>) {
|
||||
statsPanel.value = false
|
||||
@@ -702,7 +731,7 @@ function dayBreak(index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page" :class="{ contest: inContest, mobile: isMobile }">
|
||||
<div ref="pageRoot" class="page" :class="{ contest: inContest, mobile: isMobile }">
|
||||
<!-- 筛选栏 -->
|
||||
<div v-if="isDesktop" class="filters">
|
||||
<!-- 流程图列表要登录才能看(GET /flowcharts 是 requireAuth),没登录就不给这个切换 -->
|
||||
@@ -840,48 +869,117 @@ function dayBreak(index: number) {
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 手机:只留全部/我的,其余收进「筛选」 -->
|
||||
<!-- 手机:只留全部/我的、统计,其余收进「筛选」 -->
|
||||
<div v-else class="filters mobile-filters">
|
||||
<n-radio-group v-if="userStore.isAuthed" v-model:value="scope" size="small">
|
||||
<n-radio-group v-if="userStore.isAuthed" v-model:value="scope">
|
||||
<n-radio-button value="all">全部</n-radio-button>
|
||||
<n-radio-button value="mine">我的</n-radio-button>
|
||||
</n-radio-group>
|
||||
<div class="spacer"></div>
|
||||
<n-button size="small" @click="mobileFilters = !mobileFilters">
|
||||
<!-- 统计那张大表在弹框里排不开,手机上老师直接去统计页(页面形态有手机排版) -->
|
||||
<n-button
|
||||
v-if="teacher && route.name === 'submissions'"
|
||||
quaternary
|
||||
@click="openStatisticsPage"
|
||||
>
|
||||
<template #icon><Icon icon="ph:chart-bar" /></template>
|
||||
统计
|
||||
</n-button>
|
||||
<n-button
|
||||
v-else-if="route.name === 'submissions' && !flowMode"
|
||||
quaternary
|
||||
@click="toggleTodayPanel(true)"
|
||||
>
|
||||
<template #icon><Icon icon="ph:chart-bar" /></template>
|
||||
今日统计
|
||||
</n-button>
|
||||
<n-button
|
||||
:type="mobileFilters || activeFilters ? 'primary' : 'default'"
|
||||
:secondary="!mobileFilters && !!activeFilters"
|
||||
@click="mobileFilters = !mobileFilters"
|
||||
>
|
||||
<template #icon><Icon icon="ph:funnel-simple" /></template>
|
||||
筛选<template v-if="activeFilters"> · {{ activeFilters }}</template>
|
||||
</n-button>
|
||||
</div>
|
||||
<div v-if="isMobile && mobileFilters" class="mobile-panel">
|
||||
<n-radio-group v-if="!inContest && userStore.isAuthed" v-model:value="mode" size="small">
|
||||
<n-radio-group v-if="!inContest && userStore.isAuthed" v-model:value="mode">
|
||||
<n-radio-button value="code">代码</n-radio-button>
|
||||
<n-radio-button value="flow">流程图</n-radio-button>
|
||||
</n-radio-group>
|
||||
<button v-if="!teacher && query.className" class="chip on" @click="query.className = ''">
|
||||
{{ classLabel(query.className) }}<Icon icon="ph:x-bold" :width="12" />
|
||||
</button>
|
||||
<template v-if="teacher && !inContest && !mine">
|
||||
<button
|
||||
class="lesson"
|
||||
:class="{ on: lessonActive }"
|
||||
:disabled="lessonLoading"
|
||||
@click="toggleLesson"
|
||||
>
|
||||
<Icon v-if="lessonActive" icon="ph:check-bold" :width="12" />
|
||||
<b>这节课</b>
|
||||
<template v-if="lessonActive && lesson?.className">
|
||||
<span class="lesson-text">
|
||||
{{ classLabel(lesson.className) }} · {{ lesson.problems.length }} 道题 · 今天
|
||||
</span>
|
||||
<Icon icon="ph:x-bold" :width="12" class="lesson-x" />
|
||||
</template>
|
||||
</button>
|
||||
<n-select
|
||||
v-if="!lessonActive"
|
||||
:value="query.className || null"
|
||||
:options="classOptions"
|
||||
placeholder="全部班级"
|
||||
clearable
|
||||
filterable
|
||||
@update:value="(v: string | null) => (query.className = v ?? '')"
|
||||
/>
|
||||
</template>
|
||||
<button v-if="!mine && exactPerson" class="chip on person" @click="clearPerson">
|
||||
<Icon icon="ph:user-bold" :width="12" />
|
||||
<UserName :username="query.username" class="person-name" />
|
||||
<Icon icon="ph:x-bold" :width="12" />
|
||||
</button>
|
||||
<n-input
|
||||
v-else-if="!mine"
|
||||
v-model:value="query.username"
|
||||
size="small"
|
||||
clearable
|
||||
:disabled="query.myself === '1'"
|
||||
placeholder="用户"
|
||||
/>
|
||||
<n-input v-model:value="query.problem" size="small" clearable placeholder="题号" />
|
||||
<n-select
|
||||
v-model:value="query.result"
|
||||
size="small"
|
||||
:options="flowMode ? gradeOptions : resultOptions"
|
||||
/>
|
||||
<n-select
|
||||
v-if="!inContest && !flowMode"
|
||||
v-model:value="query.language"
|
||||
size="small"
|
||||
:options="languageOptions"
|
||||
/>
|
||||
<n-flex align="center">
|
||||
<button v-if="!inContest" class="chip" :class="{ on: today }" @click="today = !today">
|
||||
:placeholder="teacher ? '学生' : '用户'"
|
||||
>
|
||||
<template #prefix><Icon icon="ph:magnifying-glass" /></template>
|
||||
</n-input>
|
||||
<n-select v-if="inContest" v-model:value="query.problem" :options="contestProblemOptions" />
|
||||
<n-input
|
||||
v-else-if="!lessonActive"
|
||||
v-model:value="query.problem"
|
||||
clearable
|
||||
placeholder="题号"
|
||||
:status="unknownProblems.length ? 'warning' : undefined"
|
||||
>
|
||||
<template #prefix><Icon icon="ph:hash" /></template>
|
||||
</n-input>
|
||||
<div class="mobile-pair">
|
||||
<n-select v-model:value="query.result" :options="flowMode ? gradeOptions : resultOptions" />
|
||||
<n-select
|
||||
v-if="!inContest && !flowMode"
|
||||
v-model:value="query.language"
|
||||
:options="languageOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="mobile-panel-foot">
|
||||
<button
|
||||
v-if="!inContest && !lessonActive"
|
||||
class="chip"
|
||||
:class="{ on: today }"
|
||||
@click="today = !today"
|
||||
>
|
||||
<Icon v-if="today" icon="ph:check-bold" :width="12" />今天
|
||||
</button>
|
||||
<n-button size="small" quaternary @click="clear">清空</n-button>
|
||||
</n-flex>
|
||||
<n-button quaternary @click="clear">清空</n-button>
|
||||
<div class="spacer"></div>
|
||||
<n-button @click="mobileFilters = false">收起</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="split">
|
||||
@@ -922,8 +1020,10 @@ function dayBreak(index: number) {
|
||||
<FlowchartState :status="row.status" :grade="row.aiGrade" />
|
||||
</div>
|
||||
<div class="m-line sub">
|
||||
<span>{{ submissionClockText(row.createTime) }}</span>
|
||||
<span class="m-time">{{ submissionClockText(row.createTime) }}</span>
|
||||
<span class="title">{{ row.problemDisplayId }} {{ row.problemTitle }}</span>
|
||||
<div class="spacer"></div>
|
||||
<Icon v-if="!row.showLink" icon="ph:lock-simple" :width="13" class="m-lock" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -979,10 +1079,17 @@ function dayBreak(index: number) {
|
||||
<StatusPill :result="row.result" />
|
||||
</div>
|
||||
<div class="m-line sub">
|
||||
<span>{{ submissionClockText(row.createTime) }}</span>
|
||||
<span class="m-time">{{ submissionClockText(row.createTime) }}</span>
|
||||
<span class="title">{{ row.problemDisplayId }} {{ row.problemTitle }}</span>
|
||||
<span v-if="row.problemSet" class="ps">题单</span>
|
||||
<div class="spacer"></div>
|
||||
<span>{{ languageText(row) }}</span>
|
||||
<span class="m-lang">{{ languageText(row) }}</span>
|
||||
<Icon
|
||||
v-if="!teacher && !row.showLink"
|
||||
icon="ph:lock-simple"
|
||||
:width="13"
|
||||
class="m-lock"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1180,12 +1287,59 @@ function dayBreak(index: number) {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.mobile-filters {
|
||||
height: 56px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mobile-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
padding: 12px 14px 14px;
|
||||
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||
background: v-bind("theme.actionColor");
|
||||
}
|
||||
|
||||
.mobile-panel > .n-radio-group,
|
||||
.mobile-panel > .chip {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.mobile-panel .lesson {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.lesson-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-panel .chip.person {
|
||||
max-width: 100%;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* 结果、语言两个下拉并排:各占一半 */
|
||||
.mobile-pair {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-pair > * {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-panel-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vsep {
|
||||
@@ -1416,11 +1570,41 @@ function dayBreak(index: number) {
|
||||
}
|
||||
|
||||
.m-line .title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m-time,
|
||||
.m-lang {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.m-lock {
|
||||
flex: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 手机上的 day 行和列表行一样贴两边 */
|
||||
.mobile .day {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
/* 手机:「共 N 条」和翻页上下排,翻页按钮居中,不再顶出屏幕右边 */
|
||||
.mobile .list-foot {
|
||||
height: auto;
|
||||
padding: 12px 14px 16px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile .list-foot .spacer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 「23:59:59」8 个等宽数字 */
|
||||
.c-time {
|
||||
width: 64px;
|
||||
|
||||
Reference in new issue
Block a user