只改界面文字和注释:网址 /problemset、接口、表名、字段名都不动,老链接照常能打开。 设计稿页名「题单重设计」和已执行的迁移文件保留原样。 线上 3 个成就(题单收藏家 / 入门 / 通关)已同步改名。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
959c326bc9
commit
1b703d13ae
68 files changed
+306
-306
No files matched your search
@@ -6,11 +6,11 @@ import type { Group } from '../series'
|
||||
import Medal from './Medal.vue'
|
||||
import SeriesRow from './SeriesRow.vue'
|
||||
|
||||
/** 一组成就线(做题 / 坚持 / …),「彩蛋」组末尾是还没找到的问号,「题单和比赛」组末尾是拿到的题单奖章 */
|
||||
/** 一组成就线(做题 / 坚持 / …),「彩蛋」组末尾是还没找到的问号,「专题和比赛」组末尾是拿到的专题奖章 */
|
||||
const props = defineProps<{ group: Group; badges?: UserBadge[]; compact?: boolean }>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
const showBadges = computed(() => props.group.name === '题单和比赛' && !!props.badges?.length)
|
||||
const showBadges = computed(() => props.group.name === '专题和比赛' && !!props.badges?.length)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -27,7 +27,7 @@ const showBadges = computed(() => props.group.name === '题单和比赛' && !!pr
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showBadges" class="badges">
|
||||
<span class="masked-title">拿到的题单奖章</span>
|
||||
<span class="masked-title">拿到的专题奖章</span>
|
||||
<div class="badge-list">
|
||||
<n-tooltip v-for="b in badges" :key="b.id" :delay="150">
|
||||
<template #trigger>
|
||||
@@ -36,7 +36,7 @@ const showBadges = computed(() => props.group.name === '题单和比赛' && !!pr
|
||||
<span>{{ b.badge.name }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
{{ b.badge.description }} · 来自题单「{{ b.problemSet.title }}」
|
||||
{{ b.badge.description }} · 来自专题「{{ b.problemSet.title }}」
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,9 +37,9 @@ const got = computed(() => data.value?.achievements.filter((item) => item.unlock
|
||||
const rarities: AchievementRarity[] = ['platinum', 'gold', 'silver', 'bronze']
|
||||
const countOf = (rarity: AchievementRarity) => got.value.filter((item) => item.rarity === rarity).length
|
||||
|
||||
// 桌面两栏:左边做题、其他、彩蛋,右边坚持、题单和比赛 —— 两边差不多高
|
||||
// 桌面两栏:左边做题、其他、彩蛋,右边坚持、专题和比赛 —— 两边差不多高
|
||||
const LEFT: GroupName[] = ['做题', '其他', '彩蛋']
|
||||
const RIGHT: GroupName[] = ['坚持', '题单和比赛']
|
||||
const RIGHT: GroupName[] = ['坚持', '专题和比赛']
|
||||
const pick = (names: GroupName[]) => groups.value.filter((group) => names.includes(group.name))
|
||||
|
||||
async function load() {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Achievement } from 'utils/types'
|
||||
* 指标是后台可配的,这里没登记的指标归到「其他」、拿第一档的名字当线名 —— 不会丢,只是不好看,
|
||||
* 在后台加了新指标记得回来补一行。
|
||||
*/
|
||||
export const GROUPS = ['做题', '坚持', '题单和比赛', '其他', '彩蛋'] as const
|
||||
export const GROUPS = ['做题', '坚持', '专题和比赛', '其他', '彩蛋'] as const
|
||||
export type GroupName = (typeof GROUPS)[number]
|
||||
|
||||
interface MetricInfo {
|
||||
@@ -55,19 +55,19 @@ const METRICS: Record<string, MetricInfo> = {
|
||||
},
|
||||
submission_count: { group: '坚持', label: '交了几次', unit: '次', remain: (r) => `再交 ${r} 次` },
|
||||
problemset_completed: {
|
||||
group: '题单和比赛',
|
||||
label: '做完的题单',
|
||||
group: '专题和比赛',
|
||||
label: '做完的专题',
|
||||
unit: '个',
|
||||
remain: (r) => `再做完 ${r} 个题单`,
|
||||
remain: (r) => `再做完 ${r} 个专题`,
|
||||
},
|
||||
badge_count: {
|
||||
group: '题单和比赛',
|
||||
label: '题单奖章',
|
||||
group: '专题和比赛',
|
||||
label: '专题奖章',
|
||||
unit: '枚',
|
||||
remain: (r) => `再拿 ${r} 枚题单奖章`,
|
||||
remain: (r) => `再拿 ${r} 枚专题奖章`,
|
||||
},
|
||||
contest_joined: {
|
||||
group: '题单和比赛',
|
||||
group: '专题和比赛',
|
||||
label: '参加的比赛',
|
||||
unit: '场',
|
||||
remain: (r) => `再参加 ${r} 场比赛`,
|
||||
|
||||
@@ -463,12 +463,12 @@ export function retryFlowchartSubmission(submissionId: string) {
|
||||
return api.post<{ status: string }>(`flowcharts/${encodeURIComponent(submissionId)}/retry`)
|
||||
}
|
||||
|
||||
/** 自己在这道题上评完的每一次(早的在前),题单布置期内藏着的已经滤掉 */
|
||||
/** 自己在这道题上评完的每一次(早的在前),专题布置期内藏着的已经滤掉 */
|
||||
export function getFlowchartScores(problemId: number) {
|
||||
return api.get<FlowchartScores>(`problems/${problemId}/flowchart/scores`)
|
||||
}
|
||||
|
||||
// ==================== 题单相关API ====================
|
||||
// ==================== 专题相关API ====================
|
||||
|
||||
export function getProblemSetList(offset = 0, limit = 10, keyword = '') {
|
||||
return api.get<ProblemSetList>('problem-sets', { params: { offset, limit, keyword } })
|
||||
@@ -490,14 +490,14 @@ export function getUserBadges(username?: string) {
|
||||
return api.get<UserBadge[]>(`users/${encodeURIComponent(username ?? 'me')}/badges`)
|
||||
}
|
||||
|
||||
/** 老师看某个班在题单里的情况;不传班级就看服务端推断的那个班 */
|
||||
/** 老师看某个班在专题里的情况;不传班级就看服务端推断的那个班 */
|
||||
export function getProblemSetClassView(problemSetId: number, className = '') {
|
||||
return api.get<ProblemSetClassView>(`problem-sets/${problemSetId}/class-view`, {
|
||||
params: className ? { className } : {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 这道题以前的代码为什么藏着:落在哪些正在布置的题单里。传主键或题号都行 */
|
||||
/** 这道题以前的代码为什么藏着:落在哪些正在布置的专题里。传主键或题号都行 */
|
||||
export function getProblemSetLocks(problem: { id: number } | { displayId: string }) {
|
||||
return api.get<ProblemSetLock[]>('problem-sets/locks', {
|
||||
params: 'id' in problem ? { problemId: problem.id } : { problemDisplayId: problem.displayId },
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Exercise } from 'utils/types'
|
||||
/**
|
||||
* 把一课的 Markdown 正文拆成几段,各段交给不同的组件渲染:
|
||||
*
|
||||
* - `heading`:二级标题单独拿出来,自己出 id —— 右边「这一课」的大纲靠它跳转、高亮;
|
||||
* - `heading`:二级标专题独拿出来,自己出 id —— 右边「这一课」的大纲靠它跳转、高亮;
|
||||
* - `goals`:「## 学习目标」那一节画成「学完这课你能」的方框;
|
||||
* - `code`:带语言的代码块(```python / ```c)换成能改、能运行的代码块;
|
||||
* 不带语言的(「输出结果」那种)留在 Markdown 里照常显示;
|
||||
|
||||
@@ -18,11 +18,11 @@ import ProblemChips, { type ChipItem } from './ProblemChips.vue'
|
||||
* 左栏顶上的一条:「这道题是从哪来的」。按入口路由决定是哪一种,最多一条:
|
||||
*
|
||||
* - 课堂:这道题在「班里在做」的列表里 —— 做完几道 + 每道的题号 + 上一题 / 下一题
|
||||
* - 题单:题单名 + 进度 + 第几题 + 上一题 / 下一题
|
||||
* - 专题:专题名 + 进度 + 第几题 + 上一题 / 下一题
|
||||
* - 比赛:比赛名 + 每道的题号 + 还剩多久 + 排名
|
||||
* - 都不是:不显示
|
||||
*
|
||||
* 一道题既在这节课里、又是从题单进来的,按入口显示题单条。
|
||||
* 一道题既在这节课里、又是从专题进来的,按入口显示专题条。
|
||||
* 换题用的都是普通链接,通过之后欠着的那次强制点评由 SubmissionEffects 的路由守卫拦下。
|
||||
*/
|
||||
|
||||
@@ -35,7 +35,7 @@ const theme = useThemeVars()
|
||||
const isDark = useDark()
|
||||
|
||||
/**
|
||||
* 三种入口各一种底色(设计稿「上下文条与协作状态」):课堂浅绿、题单浅蓝、比赛浅黄。
|
||||
* 三种入口各一种底色(设计稿「上下文条与协作状态」):课堂浅绿、专题浅蓝、比赛浅黄。
|
||||
* 字色在暗色下换成浅一档的,深绿、深蓝、深黄放在暗底上看不清
|
||||
*/
|
||||
const tone = computed(() => ({
|
||||
@@ -119,7 +119,7 @@ onSolvedHere((step) => {
|
||||
lessonStore.load(true)
|
||||
})
|
||||
|
||||
// ==================== 题单 ====================
|
||||
// ==================== 专题 ====================
|
||||
const problemSet = ref<ProblemSet | null>(null)
|
||||
const setProblems = ref<ProblemSetProblem[]>([])
|
||||
|
||||
@@ -131,12 +131,12 @@ watch(
|
||||
if (!id) return
|
||||
try {
|
||||
const [detail, problems] = await Promise.all([getProblemSetDetail(Number(id)), getProblemSetProblems(Number(id))])
|
||||
// 拉的这一会儿已经离开了这个题单
|
||||
// 拉的这一会儿已经离开了这个专题
|
||||
if (ctx.value.problemSetId !== id) return
|
||||
problemSet.value = detail
|
||||
setProblems.value = problems
|
||||
} catch {
|
||||
// 拉不到就没有题单条,不影响做题
|
||||
// 拉不到就没有专题条,不影响做题
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -153,8 +153,8 @@ const setBar = computed(() => {
|
||||
params: { problemSetId: ctx.value.problemSetId, problemID: item.problem._id },
|
||||
}
|
||||
: null
|
||||
// 进度只数必做题,和题单页的「完成进度 2 / 8(另有 3 道选做)」同一个口径;
|
||||
// 一道必做都没有的题单才数全部
|
||||
// 进度只数必做题,和专题页的「完成进度 2 / 8(另有 3 道选做)」同一个口径;
|
||||
// 一道必做都没有的专题才数全部
|
||||
const required = list.some((item) => item.isRequired) ? list.filter((item) => item.isRequired) : list
|
||||
const done = required.filter((item) => item.isCompleted).length
|
||||
return {
|
||||
@@ -180,7 +180,7 @@ async function joinHere() {
|
||||
await joinProblemSet(Number(id))
|
||||
problemSet.value = await getProblemSetDetail(Number(id))
|
||||
} catch {
|
||||
// 加入失败就还是那条提示,回题单页再点一次
|
||||
// 加入失败就还是那条提示,回专题页再点一次
|
||||
} finally {
|
||||
joiningSet.value = false
|
||||
}
|
||||
@@ -296,10 +296,10 @@ onSolvedHere(() => {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 题单 -->
|
||||
<!-- 专题 -->
|
||||
<div v-else-if="setBar" class="context-bar problemset">
|
||||
<router-link :to="setBar.home" class="back set-back" :title="`回到题单:${setBar.title}`"> ‹ {{ setBar.title }} </router-link>
|
||||
<!-- 手机上一排放不下题单名 + 进度条 + 做完几道 + 两个大箭头,进度条让掉,「做完 x/y」还在 -->
|
||||
<router-link :to="setBar.home" class="back set-back" :title="`回到专题:${setBar.title}`"> ‹ {{ setBar.title }} </router-link>
|
||||
<!-- 手机上一排放不下专题名 + 进度条 + 做完几道 + 两个大箭头,进度条让掉,「做完 x/y」还在 -->
|
||||
<div v-if="setBar.joined && !isMobile" class="set-progress" aria-hidden="true">
|
||||
<div class="set-progress-fill" :style="{ width: `${setBar.percentage}%` }" />
|
||||
</div>
|
||||
@@ -309,7 +309,7 @@ onSolvedHere(() => {
|
||||
</span>
|
||||
<template v-else>
|
||||
<span class="meta">{{ isMobile ? '没加入,不算进度' : '还没加入,在这里做对了不算进度' }}</span>
|
||||
<n-button size="tiny" type="primary" :loading="joiningSet" @click="joinHere">加入题单</n-button>
|
||||
<n-button size="tiny" type="primary" :loading="joiningSet" @click="joinHere">加入专题</n-button>
|
||||
</template>
|
||||
<div class="spacer" />
|
||||
<component
|
||||
|
||||
@@ -226,7 +226,7 @@ function goCode() {
|
||||
<span class="attempt pending">第 {{ scores.length + 1 }} 次</span>
|
||||
</template>
|
||||
</div>
|
||||
<p v-if="hiddenCount" class="note">另外 {{ hiddenCount }} 次是题单布置之前画的,先藏起来了:在题单里画到 A 或 S,或者布置期结束就解锁</p>
|
||||
<p v-if="hiddenCount" class="note">另外 {{ hiddenCount }} 次是专题布置之前画的,先藏起来了:在专题里画到 A 或 S,或者布置期结束就解锁</p>
|
||||
|
||||
<!-- 正在评:评的是最新这一次,所以只在看最新时显示 -->
|
||||
<template v-if="phase === 'evaluating' && viewingLatest">
|
||||
|
||||
@@ -74,17 +74,17 @@ const lessonProblemIds = computed(() => lessonStore.activity?.problems.map((item
|
||||
<template v-else>
|
||||
<div class="text">
|
||||
<span class="headline">{{ label }}都做完了,一共 {{ lesson.total }} 道</span>
|
||||
<span v-if="!challenges" class="detail">想接着练,可以去题单里挑一个</span>
|
||||
<span v-if="!challenges" class="detail">想接着练,可以去专题里挑一个</span>
|
||||
</div>
|
||||
<router-link v-if="!challenges" to="/problemset" class="next secondary">
|
||||
<span class="next-label">去题单继续练</span>
|
||||
<span class="next-label">去专题继续练</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 做完了:先给这节课里还能挑战的,再给题单(设计稿 StkDone)。挑战一条都没有时只剩题单那句 -->
|
||||
<!-- 做完了:先给这节课里还能挑战的,再给专题(设计稿 StkDone)。挑战一条都没有时只剩专题那句 -->
|
||||
<template v-if="lesson && !lesson.next && challenges">
|
||||
<ChallengeList :problem-ids="lessonProblemIds" :language="language" :refresh-key="refreshKey" />
|
||||
<router-link to="/problemset" class="more">去题单继续练</router-link>
|
||||
<router-link to="/problemset" class="more">去专题继续练</router-link>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ const ctx = useProblemPageContext()
|
||||
// SQL 题:隐藏输入/输出/例子,改为渲染数据表与期望结果
|
||||
const isSQL = computed(() => !!problem.value && isSqlProblem(problem.value))
|
||||
|
||||
// 相似题推荐:做对了、或者错了 3 次以上才给(比赛、题单里不给,见 problemPageContext)
|
||||
// 相似题推荐:做对了、或者错了 3 次以上才给(比赛、专题里不给,见 problemPageContext)
|
||||
const showSimilar = computed(() => ctx.value.similar && (problem.value?.myStatus === 0 || problemStore.failCount >= 3))
|
||||
|
||||
/**
|
||||
* 标题下的状态标签。原来是题面最上面两条整宽的提示(「本题已经被你解决啦」「尝试过但还没有
|
||||
* 通过」),占掉一屏里最值钱的那几行。题单里不给(入口差异表)。
|
||||
* 通过」),占掉一屏里最值钱的那几行。专题里不给(入口差异表)。
|
||||
*/
|
||||
const myStatus = computed(() => {
|
||||
if (!ctx.value.statusTag || !userStore.isAuthed) return null
|
||||
|
||||
@@ -94,7 +94,7 @@ const mobileExtensions = [...desktopExtensions, ...hangWrapExtensions]
|
||||
const editorExtensions = computed(() => (isDesktop.value ? desktopExtensions : mobileExtensions))
|
||||
|
||||
const contestID = route.params.contestID || null
|
||||
// 草稿存在哪个键下(题单入口单独一份),和「重置代码」共用一份算法
|
||||
// 草稿存在哪个键下(专题入口单独一份),和「重置代码」共用一份算法
|
||||
const storageKey = useDraftKey()
|
||||
|
||||
// 桌面:顶栏 56 + 工具栏 48,协作条再占 40(两栏顶满内容区,见 detail.vue 的 problem-split)。
|
||||
@@ -120,8 +120,8 @@ onMounted(loadCode)
|
||||
watch(() => problem.value?._id, loadCode)
|
||||
|
||||
/**
|
||||
* 流程图草稿:每道题一份;题单入口单独一份 —— 加入题单之前在题库里画到 A 的那张,不能一进
|
||||
* 题单就摆在画布上(设计文档第 11 节 3,和代码草稿同理)。
|
||||
* 流程图草稿:每道题一份;专题入口单独一份 —— 加入专题之前在题库里画到 A 的那张,不能一进
|
||||
* 专题就摆在画布上(设计文档第 11 节 3,和代码草稿同理)。
|
||||
* 键名带 v2:2026-10 之前的草稿是 vue-flow 的格式,读不了,也不去覆盖它
|
||||
*/
|
||||
const flowchartDraftKey = computed(() => {
|
||||
|
||||
@@ -44,7 +44,7 @@ const teacherCollab = useTeacherCollab()
|
||||
const peer = computed(() => (teacherCollab.value ? (collabStore.room?.peerName ?? null) : null))
|
||||
|
||||
// ==================== 班上第几个做对的 ====================
|
||||
// 只在题库入口给:比赛有自己的排名,题单里整个抽屉都不出现
|
||||
// 只在题库入口给:比赛有自己的排名,专题里整个抽屉都不出现
|
||||
const rank = ref<ProblemRank | null>(null)
|
||||
|
||||
const rankLine = computed(() => {
|
||||
@@ -109,7 +109,7 @@ async function listSubmissions() {
|
||||
|
||||
/**
|
||||
* 「我的提交统计」:这一页里各种结果各几次(「答案错误 × 3」「答案正确 × 1」),和原来一样
|
||||
* 只数当前这一页。被题单闸门锁住的那几次不数 —— 它们在列表里只露一行说明,这里也不该露出结果
|
||||
* 只数当前这一页。被专题闸门锁住的那几次不数 —— 它们在列表里只露一行说明,这里也不该露出结果
|
||||
*/
|
||||
const statusDistribution = computed(() => {
|
||||
const counts = new Map<number, number>()
|
||||
@@ -137,7 +137,7 @@ type Loaded = { code: string; language: LANGUAGE }
|
||||
const details = reactive<Record<string, Loaded | 'loading' | 'failed'>>({})
|
||||
|
||||
async function toggle(row: SubmissionListItem) {
|
||||
// 题单布置期内藏着的旧提交,点不开(后端也不会给代码)
|
||||
// 专题布置期内藏着的旧提交,点不开(后端也不会给代码)
|
||||
if (!row.showLink) return
|
||||
if (expanded.value === row.id) {
|
||||
expanded.value = null
|
||||
@@ -154,7 +154,7 @@ async function toggle(row: SubmissionListItem) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 这一页里被题单闸门藏起来的次数 */
|
||||
/** 这一页里被专题闸门藏起来的次数 */
|
||||
const lockedCount = computed(() => submissions.value.filter((row) => !row.showLink).length)
|
||||
|
||||
/** 展开的代码先只露 5 行,「一共 11 行」点一下再全摆出来(设计稿) */
|
||||
@@ -304,7 +304,7 @@ watch(query, () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 题单布置期内藏着的那几次:合成一行,点不开(后端也不给代码) -->
|
||||
<!-- 专题布置期内藏着的那几次:合成一行,点不开(后端也不给代码) -->
|
||||
<div v-if="lockedCount" class="locked">
|
||||
<svg
|
||||
width="13"
|
||||
|
||||
@@ -12,11 +12,11 @@ import { useProblemPageContext } from '../composables/problemPageContext'
|
||||
import { caseRevealDuration } from '../utils/caseReveal'
|
||||
|
||||
/**
|
||||
* 判完之后「该发生什么」:失败计数、标成已解决、烟花、点评、回题单。
|
||||
* 判完之后「该发生什么」:失败计数、标成已解决、烟花、点评、回专题。
|
||||
*
|
||||
* 挂在题目页这一层(detail.vue),和编辑器、提交按钮都无关 —— 原来这些写在提交按钮
|
||||
* 那个组件(SubmitCode)里,手机上切一下页签、桌面切一下分栏模式,编辑器一卸载它们
|
||||
* 就跟着没了:判完不放烟花、不计失败次数、不弹点评、题单里不回题单。
|
||||
* 就跟着没了:判完不放烟花、不计失败次数、不弹点评、专题里不回专题。
|
||||
* 状态本身在 submission store,这里只是看着它做事。
|
||||
*/
|
||||
|
||||
@@ -167,7 +167,7 @@ const { start: goToProblemSetDelayed, stop: cancelGoToProblemSet } = useTimeoutF
|
||||
)
|
||||
|
||||
/**
|
||||
* 换题时收掉通过之后等着的点评和回题单 —— 评价弹窗是按**当前**题目去查、去弹的,
|
||||
* 换题时收掉通过之后等着的点评和回专题 —— 评价弹窗是按**当前**题目去查、去弹的,
|
||||
* 不取消的话就会给一道还没做的题弹评价。欠着的点评走不到这里:离开这道题的导航
|
||||
* 已经被 holdForReview 拦下、评完才放行;还能走到这里的只有弹窗开着时按了浏览器后退。
|
||||
* 结果面板、还在跟的那条提交由 submission store 自己收。
|
||||
@@ -221,14 +221,14 @@ watch(
|
||||
// 1. 刷新题目状态
|
||||
problem.value!.myStatus = 0
|
||||
|
||||
// 题单进度不在这里更新了。以前是 AC 之后回调 PUT /problem-set-progress,只认路由
|
||||
// 参数里那一个题单:从普通题库入口做出同一道题不计进度,网络一抖、页面提前关掉进度
|
||||
// 专题进度不在这里更新了。以前是 AC 之后回调 PUT /problem-set-progress,只认路由
|
||||
// 参数里那一个专题:从普通题库入口做出同一道题不计进度,网络一抖、页面提前关掉进度
|
||||
// 就静默丢失。现在判题那一路直接记账(judge/run.ts),而且是记进所有已加入且包含
|
||||
// 这道题的题单;收到「判完了」的时候进度已经落库,跳回题单页看到的就是新数据。
|
||||
// 这道题的专题;收到「判完了」的时候进度已经落库,跳回专题页看到的就是新数据。
|
||||
|
||||
if (result !== SubmissionStatus.accepted) return
|
||||
|
||||
// 3. 烟花放一会儿弹出评价框。比赛里不打扰;题单里 1.5 秒后要跳回题单页,弹了也会被冲掉
|
||||
// 3. 烟花放一会儿弹出评价框。比赛里不打扰;专题里 1.5 秒后要跳回专题页,弹了也会被冲掉
|
||||
// (评价是欠着的这件事马上记下:方块还在亮的时候离开这道题,也要先评)
|
||||
if (ctx.value.reviewAfterAccepted) reviewOwed.value = true
|
||||
|
||||
@@ -241,15 +241,15 @@ watch(
|
||||
})
|
||||
|
||||
if (ctx.value.backToProblemSet) {
|
||||
// 延迟回到题单页面
|
||||
// 延迟回到专题页面
|
||||
goToProblemSetDelayed()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// ==================== 流程图画到 A / S ====================
|
||||
// 题单里流程图作业拿到 A 或 S 就算这道题做完(后端评分那一路已经记了账),
|
||||
// 和代码通过一样 1.5 秒后回题单(设计文档第 3 节决定 4、5)。只认这一页上刚评完的那一次
|
||||
// 专题里流程图作业拿到 A 或 S 就算这道题做完(后端评分那一路已经记了账),
|
||||
// 和代码通过一样 1.5 秒后回专题(设计文档第 3 节决定 4、5)。只认这一页上刚评完的那一次
|
||||
const flowchartStore = useFlowchartStore()
|
||||
watch(
|
||||
() => flowchartStore.evaluatedSeq,
|
||||
|
||||
@@ -80,8 +80,8 @@ const sampleCheck = computed(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 通过之后给「这节课的下一题」。比赛有自己的题目列表;从题单入口进来的,通过后
|
||||
* 1.5 秒会自动跳回题单(SubmissionEffects),这两种都不给
|
||||
* 通过之后给「这节课的下一题」。比赛有自己的题目列表;从专题入口进来的,通过后
|
||||
* 1.5 秒会自动跳回专题(SubmissionEffects),这两种都不给
|
||||
*/
|
||||
const ctx = useProblemPageContext()
|
||||
const showLessonNext = computed(() => !props.peer && props.submission?.result === SubmissionStatus.accepted && ctx.value.lessonNext)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 挑战的颜色:紫。课堂条是绿、题单条是蓝、比赛条是黄,挑战不能和它们撞(设计稿「做对后再挑战」)。
|
||||
* 挑战的颜色:紫。课堂条是绿、专题条是蓝、比赛条是黄,挑战不能和它们撞(设计稿「做对后再挑战」)。
|
||||
* Naive 的主题里没有紫,暗色下换浅一档,深紫放在暗底上看不清
|
||||
*/
|
||||
export function useChallengeTone() {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useProblemStore } from 'oj/store/problem'
|
||||
* (删草稿)都用这一份 —— 原来键在编辑器里拼、再作为 prop 传给工具栏,手机上「重置」
|
||||
* 挪进页签行的「⋯」之后就够不着了,再拼一份迟早和编辑器那份对不上。
|
||||
*
|
||||
* 题单入口单独一份:原来题库和题单共用 `problem_题号_contest_null_语言`,加入题单之前
|
||||
* 在题库里写对的代码,一进题单就摆在编辑器里(设计文档第 11 节 4)。
|
||||
* 专题入口单独一份:原来题库和专题共用 `problem_题号_contest_null_语言`,加入专题之前
|
||||
* 在题库里写对的代码,一进专题就摆在编辑器里(设计文档第 11 节 4)。
|
||||
* **键的最后一段必须是语言**,编辑器换语言时靠它判断草稿是不是这门语言的。
|
||||
*/
|
||||
export function useDraftKey() {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
|
||||
const options = computed<DropdownOption[]>(() => {
|
||||
const items: DropdownOption[] = []
|
||||
// 「本题提交」挪到了「我的提交」抽屉的底部;题单入口没有抽屉,给管理员角色留在这里
|
||||
// 「本题提交」挪到了「我的提交」抽屉的底部;专题入口没有抽屉,给管理员角色留在这里
|
||||
if (ctx.value.allSubmissionsInMenu && userStore.isAdminRole) {
|
||||
items.push({ label: '本题提交', key: 'allSubmissions' })
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
/**
|
||||
* 题目页的三种入口:题库(含课堂)、题单、比赛。同一个 detail.vue 挂在三条路由上。
|
||||
* 题目页的三种入口:题库(含课堂)、专题、比赛。同一个 detail.vue 挂在三条路由上。
|
||||
*
|
||||
* 入口之间的差异**只在这张表里判断**。原来散在各个组件里各查各的路由:同一件事
|
||||
* 在题单里是「禁用」、在比赛里是「隐藏」,加一处功能就得把每个组件翻一遍
|
||||
* 在专题里是「禁用」、在比赛里是「隐藏」,加一处功能就得把每个组件翻一遍
|
||||
* (见 docs/specs/2026-09-28-problem-page-redesign-design.md 第 8 节)。
|
||||
*/
|
||||
export type ProblemEntry = 'problem' | 'problemset' | 'contest'
|
||||
@@ -27,13 +27,13 @@ interface EntryRules {
|
||||
/** 通过之后给「这节课的下一题」 */
|
||||
lessonNext: boolean
|
||||
/**
|
||||
* 做对之后的挑战(编辑器顶上的挑战条、结果里的「还能挑战」)。题单里通过后 1.5 秒就跳回去,
|
||||
* 做对之后的挑战(编辑器顶上的挑战条、结果里的「还能挑战」)。专题里通过后 1.5 秒就跳回去,
|
||||
* 比赛提交不判挑战
|
||||
*/
|
||||
challenges: boolean
|
||||
/** 通过之后 1.5 秒弹强制点评 */
|
||||
reviewAfterAccepted: boolean
|
||||
/** 通过之后 1.5 秒回题单 */
|
||||
/** 通过之后 1.5 秒回专题 */
|
||||
backToProblemSet: boolean
|
||||
/** 举手求助 */
|
||||
help: boolean
|
||||
@@ -45,13 +45,13 @@ interface EntryRules {
|
||||
collab: boolean
|
||||
/**
|
||||
* 工具栏上教师的「课堂统计」(新标签开数据统计页)。题库入口已经并进「统计」页签的老师
|
||||
* 视图(按班看谁没做对,带「在数据统计里看」),只剩没有抽屉的题单入口还要这个按钮。
|
||||
* 视图(按班看谁没做对,带「在数据统计里看」),只剩没有抽屉的专题入口还要这个按钮。
|
||||
* 比赛不给:统计接口按 displayId 在公开题库里找题,比赛题号查到的是另一道公开题,或者 404
|
||||
*/
|
||||
classStats: boolean
|
||||
/**
|
||||
* 「看这道题所有人的提交」放进「⋯」(只给管理员角色)。它平时在「我的提交」抽屉底部,
|
||||
* 题单入口没有抽屉,老师就没地方点了
|
||||
* 专题入口没有抽屉,老师就没地方点了
|
||||
*/
|
||||
allSubmissionsInMenu: boolean
|
||||
}
|
||||
@@ -70,15 +70,15 @@ const RULES: Record<ProblemEntry, EntryRules> = {
|
||||
classStats: false,
|
||||
allSubmissionsInMenu: false,
|
||||
},
|
||||
// 题单里什么「以前的」都不给看:加入题单之前的提交、统计、点评都能拿来抄答案
|
||||
// 专题里什么「以前的」都不给看:加入专题之前的提交、统计、点评都能拿来抄答案
|
||||
problemset: {
|
||||
drawers: [],
|
||||
statusTag: false,
|
||||
// 点进相似题就离开了题单
|
||||
// 点进相似题就离开了专题
|
||||
similar: false,
|
||||
lessonNext: false,
|
||||
challenges: false,
|
||||
// 1.5 秒后要跳回题单页,弹了也会被冲掉
|
||||
// 1.5 秒后要跳回专题页,弹了也会被冲掉
|
||||
reviewAfterAccepted: false,
|
||||
backToProblemSet: true,
|
||||
help: true,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useFlowchartStore } from 'oj/store/flowchart'
|
||||
import { useCollabStore } from 'shared/store/collab'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import NotFound from 'shared/components/NotFound.vue'
|
||||
// 判完之后该发生的事(烟花、点评、回题单)。静态引入:它得在第一次判完之前就挂好
|
||||
// 判完之后该发生的事(烟花、点评、回专题)。静态引入:它得在第一次判完之前就挂好
|
||||
import SubmissionEffects from './components/SubmissionEffects.vue'
|
||||
import ResultTabLabel from './components/ResultTabLabel.vue'
|
||||
import ContextBar from './components/ContextBar.vue'
|
||||
@@ -21,8 +21,8 @@ import { DRAWER_TITLE, useProblemPageContext, type ProblemDrawer as DrawerKey }
|
||||
import { useCollabHere, useTeacherCollab } from './composables/teacherCollab'
|
||||
|
||||
// 抽成具名 loader,便于进页面时与接口并行预取编辑器 chunk。
|
||||
// 题库、比赛、题单三种入口用的是同一个编辑器:原来比赛和题单用的是另一个精简版,
|
||||
// 没有协作、也没有错误标记,于是题单里「求助」老师接不上、「把中文标点都换成英文」
|
||||
// 题库、比赛、专题三种入口用的是同一个编辑器:原来比赛和专题用的是另一个精简版,
|
||||
// 没有协作、也没有错误标记,于是专题里「求助」老师接不上、「把中文标点都换成英文」
|
||||
// 点了没反应(见 docs/specs/2026-09-28-problem-page-redesign-design.md 第 10 节)
|
||||
const loadProblemEditor = () => import('./components/ProblemEditor.vue')
|
||||
const ProblemEditor = defineAsyncComponent(loadProblemEditor)
|
||||
@@ -95,7 +95,7 @@ const leftPane = useTemplateRef<HTMLElement>('leftPane')
|
||||
const theme = useThemeVars()
|
||||
|
||||
/**
|
||||
* 分隔条两头按像素卡,不按比例:左栏窄于 360 时题单条的「下一题」箭头被裁掉、三个抽屉按钮
|
||||
* 分隔条两头按像素卡,不按比例:左栏窄于 360 时专题条的「下一题」箭头被裁掉、三个抽屉按钮
|
||||
* 折成两行;右栏窄于 460 时工具栏收进「⋯」之后也放不下、语言下拉被挤成两行(原来 0.8 在 1280 屏上只给右栏 256)
|
||||
*/
|
||||
const { width: windowWidth } = useWindowSize()
|
||||
@@ -234,12 +234,12 @@ async function init() {
|
||||
}
|
||||
onMounted(init)
|
||||
|
||||
/** 题不存在时回哪:比赛里回比赛,题单里回题单,否则回题目列表 */
|
||||
/** 题不存在时回哪:比赛里回比赛,专题里回专题,否则回题目列表 */
|
||||
const backTo = computed(() =>
|
||||
contestID
|
||||
? { to: `/contest/${contestID}`, toLabel: '回到比赛' }
|
||||
: problemSetId
|
||||
? { to: `/problemset/${problemSetId}`, toLabel: '回到题单' }
|
||||
? { to: `/problemset/${problemSetId}`, toLabel: '回到专题' }
|
||||
: { to: '/problem', toLabel: '去题目列表' },
|
||||
)
|
||||
watch(() => problemID, init)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { parseTime } from 'utils/functions'
|
||||
import type { ProblemSetClassView } from 'utils/types'
|
||||
|
||||
/**
|
||||
* 老师看一个班在题单里的情况(设计稿「题单重设计」老师 · 全班情况):真名 × 每道题,
|
||||
* 老师看一个班在专题里的情况(设计稿「题单重设计」老师 · 全班情况):真名 × 每道题,
|
||||
* 格子里写几点做对 / 错了几次;紫色角 = 加入之前就做对过(可能是凭记忆默写的)。
|
||||
* 没加入的人在上面点名。班级默认是服务端推断的(最近在做的那个班),可以换。
|
||||
* 点格子到提交列表看这个学生这道题的代码。
|
||||
@@ -70,7 +70,7 @@ const error = computed(() => tone('error'))
|
||||
|
||||
<template>
|
||||
<section class="card">
|
||||
<div v-if="data && !data.className" class="empty muted">还没有学生加入这个题单</div>
|
||||
<div v-if="data && !data.className" class="empty muted">还没有学生加入这个专题</div>
|
||||
<template v-else-if="data">
|
||||
<div class="summary">
|
||||
<n-select v-model:value="className" :options="classOptions" size="small" class="pick" :consistent-menu-width="false" />
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { ProblemSetLock } from 'utils/types'
|
||||
|
||||
/**
|
||||
* 「旧代码为什么藏着」那一句(设计稿「题单重设计」旧代码藏着时学生看到的):
|
||||
* 题单《X》布置中,这道题以前的代码先藏着 · 在题单里做对,或 M月D日 之后就能看。
|
||||
* 规则在后端 problemSetLockCutoffs;查不到是哪个题单(比如刚过期)就说通用的一句
|
||||
* 专题《X》布置中,这道题以前的代码先藏着 · 在专题里做对,或 M月D日 之后就能看。
|
||||
* 规则在后端 problemSetLockCutoffs;查不到是哪个专题(比如刚过期)就说通用的一句
|
||||
*/
|
||||
const props = defineProps<{ problem: { id: number } | { displayId: string } }>()
|
||||
const theme = useThemeVars()
|
||||
@@ -31,10 +31,10 @@ const first = computed(() => locks.value[0])
|
||||
|
||||
<template>
|
||||
<span v-if="first">
|
||||
题单<router-link :to="{ name: 'problemset', params: { problemSetId: first.problemSetId } }">《{{ first.title }}》</router-link
|
||||
>布置中,这道题以前的代码先藏着 · 在题单里做对,或 {{ parseTime(first.assignedUntil, 'M月D日') }} 之后就能看
|
||||
专题<router-link :to="{ name: 'problemset', params: { problemSetId: first.problemSetId } }">《{{ first.title }}》</router-link
|
||||
>布置中,这道题以前的代码先藏着 · 在专题里做对,或 {{ parseTime(first.assignedUntil, 'M月D日') }} 之后就能看
|
||||
</span>
|
||||
<span v-else>这道题正在题单里布置,以前的代码先藏着 · 在题单里做对,或者布置期结束就能看</span>
|
||||
<span v-else>这道题正在专题里布置,以前的代码先藏着 · 在专题里做对,或者布置期结束就能看</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -14,9 +14,9 @@ import BadgeLadder from './components/BadgeLadder.vue'
|
||||
import ClassView from './components/ClassView.vue'
|
||||
|
||||
/**
|
||||
* 题单详情(设计稿「题单重设计」详情 B):顶上一条是进度和奖章阶梯,下面题目分两列。
|
||||
* 专题详情(设计稿「题单重设计」详情 B):顶上一条是进度和奖章阶梯,下面题目分两列。
|
||||
*
|
||||
* - 还没加入:题目看得见、点不进去,右边一个大按钮「加入题单,开始做」。加入是用户要留的
|
||||
* - 还没加入:题目看得见、点不进去,右边一个大按钮「加入专题,开始做」。加入是用户要留的
|
||||
* 一步(「只是打开就算加入的话,会有疑惑的」),所以这里要让人一眼看懂为什么要点。
|
||||
* - 布置期内以前做对过的题标「以前做对过 · 旧代码先藏着」,规则见后端 problemSetLockCutoffs。
|
||||
* - 做完了:整条变绿,写几点做完、用了多久。烟花只在做完的那一刻放(做对最后一道、
|
||||
@@ -79,7 +79,7 @@ const spent = computed(() => {
|
||||
})
|
||||
|
||||
// 两栏放得下(每栏 420 起)才拆;拆完在窄屏上叠起来,就成了两张没有标题的卡片,
|
||||
// 看着像题单分了两组
|
||||
// 看着像专题分了两组
|
||||
const twoColumns = useMediaQuery('(min-width: 960px)')
|
||||
|
||||
const columns = computed(() => {
|
||||
@@ -132,14 +132,14 @@ const purple = { color: '#5b3fa8', background: 'rgba(122, 95, 208, 0.12)' }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NotFound v-if="missing" title="这个题单不存在" hint="可能还没公开,或者已经删掉了" to="/problemset" to-label="回到题单列表" />
|
||||
<NotFound v-if="missing" title="这个专题不存在" hint="可能还没公开,或者已经删掉了" to="/problemset" to-label="回到专题列表" />
|
||||
|
||||
<div v-else-if="set" class="page oj-page">
|
||||
<section class="band" :class="{ done: progress?.isCompleted }">
|
||||
<span v-if="progress?.isCompleted" class="done-mark" aria-hidden="true">✓</span>
|
||||
<div class="head">
|
||||
<div class="row">
|
||||
<router-link :to="{ name: 'problemsets' }" class="back">‹ 题单</router-link>
|
||||
<router-link :to="{ name: 'problemsets' }" class="back">‹ 专题</router-link>
|
||||
<h2 class="ell">{{ set.title }}</h2>
|
||||
<span v-if="set.assigning" class="pill assign">布置到 {{ parseTime(set.assignedUntil!, 'M月D日') }}</span>
|
||||
</div>
|
||||
@@ -164,7 +164,7 @@ const purple = { color: '#5b3fa8', background: 'rgba(122, 95, 208, 0.12)' }
|
||||
<div v-else class="muted small">
|
||||
{{ set.createdBy.username }} 出的 · {{ set.problemsCount }} 道<template v-if="optional > 0">,{{ optional }} 道选做</template
|
||||
><template v-else>,全部必做</template>
|
||||
<template v-if="!set.badges.length"> · 这个题单没有奖章</template>
|
||||
<template v-if="!set.badges.length"> · 这个专题没有奖章</template>
|
||||
</div>
|
||||
<div v-if="set.description && set.description !== set.title" class="sec small desc">
|
||||
{{ set.description }}
|
||||
@@ -176,7 +176,7 @@ const purple = { color: '#5b3fa8', background: 'rgba(122, 95, 208, 0.12)' }
|
||||
</template>
|
||||
<div class="spacer"></div>
|
||||
<div v-if="!joined && !userStore.isTeacherOrAbove" class="join">
|
||||
<n-button type="primary" size="large" :loading="joining" @click="join"> 加入题单,开始做 </n-button>
|
||||
<n-button type="primary" size="large" :loading="joining" @click="join"> 加入专题,开始做 </n-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -196,14 +196,14 @@ const purple = { color: '#5b3fa8', background: 'rgba(122, 95, 208, 0.12)' }
|
||||
<rect x="5" y="11" width="14" height="9" rx="2" />
|
||||
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
|
||||
</svg>
|
||||
其中 <b>{{ hiddenCount }}</b> 道你以前做对过。布置期内(到 {{ parseTime(set.assignedUntil!, 'M月D日') }})这几道的旧代码先藏起来,在题单里重新做对就能看。
|
||||
其中 <b>{{ hiddenCount }}</b> 道你以前做对过。布置期内(到 {{ parseTime(set.assignedUntil!, 'M月D日') }})这几道的旧代码先藏起来,在专题里重新做对就能看。
|
||||
</div>
|
||||
|
||||
<div v-if="userStore.isTeacherOrAbove" class="tabs">
|
||||
<button class="seg" :class="{ on: tab !== 'class' }" @click="tab = 'problems'">题目</button>
|
||||
<button class="seg" :class="{ on: tab === 'class' }" @click="tab = 'class'">全班情况</button>
|
||||
<div class="spacer"></div>
|
||||
<n-button v-if="canEdit" size="small" @click="router.push({ name: 'admin problemset edit', params: { problemSetId: set.id } })"> 编辑题单 </n-button>
|
||||
<n-button v-if="canEdit" size="small" @click="router.push({ name: 'admin problemset edit', params: { problemSetId: set.id } })"> 编辑专题 </n-button>
|
||||
</div>
|
||||
|
||||
<ClassView v-if="userStore.isTeacherOrAbove && tab === 'class'" :problem-set-id="set.id" />
|
||||
@@ -249,7 +249,7 @@ const purple = { color: '#5b3fa8', background: 'rgba(122, 95, 208, 0.12)' }
|
||||
</component>
|
||||
</section>
|
||||
</div>
|
||||
<n-empty v-else description="这个题单还没有题目" />
|
||||
<n-empty v-else description="这个专题还没有题目" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,10 +10,10 @@ import type { ProblemSet } from 'utils/types'
|
||||
import { badgeCondition, ladder, nextBadge } from './badges'
|
||||
|
||||
/**
|
||||
* 题单列表(设计稿「题单重设计」列表 A):老师正在布置的题单单独一排大卡片 —— 做对几道、
|
||||
* 布置到哪天、再做对几道拿哪个奖章;其余题单一排排小卡片,加入过的在前。
|
||||
* 专题列表(设计稿「题单重设计」列表 A):老师正在布置的专题单独一排大卡片 —— 做对几道、
|
||||
* 布置到哪天、再做对几道拿哪个奖章;其余专题一排排小卡片,加入过的在前。
|
||||
*
|
||||
* 题单一共十几个,一次全取回来在前端分组,不分页
|
||||
* 专题一共十几个,一次全取回来在前端分组,不分页
|
||||
*/
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
@@ -82,11 +82,11 @@ const doneBackground = computed(() => rgba(theme.value.successColor, 0.05))
|
||||
<template>
|
||||
<div class="page oj-page">
|
||||
<div class="top">
|
||||
<h2>题单</h2>
|
||||
<h2>专题</h2>
|
||||
<div class="spacer"></div>
|
||||
<n-input v-model:value="keyword" placeholder="搜题单" clearable class="search" />
|
||||
<n-input v-model:value="keyword" placeholder="搜专题" clearable class="search" />
|
||||
<!-- 手机上一行放不下,也没人在手机上出题,只给电脑 -->
|
||||
<AdminLink v-if="isDesktop && userStore.isTeacherOrAbove" to="/admin/problemset/create">新建题单</AdminLink>
|
||||
<AdminLink v-if="isDesktop && userStore.isTeacherOrAbove" to="/admin/problemset/create">新建专题</AdminLink>
|
||||
</div>
|
||||
|
||||
<template v-if="assigning.length">
|
||||
@@ -128,13 +128,13 @@ const doneBackground = computed(() => rgba(theme.value.successColor, 0.05))
|
||||
<img v-for="badge in ladder(set)" :key="badge.id" :src="badge.icon" :alt="badge.name" :title="badge.name" class="icon" />
|
||||
<span>奖章全拿到了</span>
|
||||
</div>
|
||||
<div v-else class="muted hint">这个题单没有奖章</div>
|
||||
<div v-else class="muted hint">这个专题没有奖章</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="others.length" class="sect">
|
||||
{{ assigning.length ? '其他题单' : '题单' }}
|
||||
{{ assigning.length ? '其他专题' : '专题' }}
|
||||
<span class="muted">{{ others.length }} 个</span>
|
||||
</div>
|
||||
<div class="small-grid">
|
||||
@@ -169,7 +169,7 @@ const doneBackground = computed(() => rgba(theme.value.successColor, 0.05))
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<n-empty v-if="loaded && !shown.length" :description="keyword ? '没有找到这样的题单' : '还没有题单'" />
|
||||
<n-empty v-if="loaded && !shown.length" :description="keyword ? '没有找到这样的专题' : '还没有专题'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -254,7 +254,7 @@ const doneBackground = computed(() => rgba(theme.value.successColor, 0.05))
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
/* 1200 限宽下布置中是一行三张、其他题单一行四张;原来一行四张的大卡只有 280 宽,
|
||||
/* 1200 限宽下布置中是一行三张、其他专题一行四张;原来一行四张的大卡只有 280 宽,
|
||||
「还没加入 · 9 道」和「另有 1 道选做」都被挤成两行 */
|
||||
.big-grid {
|
||||
display: grid;
|
||||
|
||||
@@ -56,7 +56,7 @@ const gradeColor = computed(() => ({
|
||||
C: tone('error').solid,
|
||||
}))
|
||||
|
||||
/** 拿到 A / S 算完成(和题单、课堂条的口径一致) */
|
||||
/** 拿到 A / S 算完成(和专题、课堂条的口径一致) */
|
||||
const goodCount = computed(() => {
|
||||
// 查几道题就要每道都拿到 A / S;没填题号时拿到一道就算
|
||||
const need = Math.max(1, props.data.problemCount)
|
||||
|
||||
@@ -92,7 +92,7 @@ function attendance(lesson: Lesson) {
|
||||
/** 要画流程图的课写出来:这一行的次数和正确率连流程图一起数(画到 A / S 算对) */
|
||||
function problemText(lesson: Lesson) {
|
||||
if (lesson.assigned) return `布置 ${lesson.problems.length} 道${lesson.mode && lesson.mode !== 'code' ? ` · ${LESSON_MODE_LABEL[lesson.mode]}` : ''}`
|
||||
return `${lesson.fromProblemSet ? '题单 ' : ''}${lesson.problems.length} 道`
|
||||
return `${lesson.fromProblemSet ? '专题 ' : ''}${lesson.problems.length} 道`
|
||||
}
|
||||
|
||||
// ---------- 时间轴(只在今天以内) ----------
|
||||
|
||||
@@ -65,11 +65,11 @@ export const useFlowchartStore = defineStore('flowchart', () => {
|
||||
const latestRating = ref<FlowchartRating>({ score: 0, grade: '' })
|
||||
const phase = ref<FlowchartPhase>('idle')
|
||||
/**
|
||||
* 这一页上**刚刚**评完一次就加一(不含进页面时读到的历史)。课堂条标做完、题单里
|
||||
* 1.5 秒后跳回题单,都只认这一下 —— 进页面读到以前的 A 不该再跳一次
|
||||
* 这一页上**刚刚**评完一次就加一(不含进页面时读到的历史)。课堂条标做完、专题里
|
||||
* 1.5 秒后跳回专题,都只认这一下 —— 进页面读到以前的 A 不该再跳一次
|
||||
*/
|
||||
const evaluatedSeq = ref(0)
|
||||
/** 这道题评完的每一次,早的在前;hidden = 题单布置期内被藏起来的次数 */
|
||||
/** 这道题评完的每一次,早的在前;hidden = 专题布置期内被藏起来的次数 */
|
||||
const scores = ref<FlowchartScores['scores']>([])
|
||||
const hiddenCount = ref(0)
|
||||
const submissionCount = computed(() => scores.value.length)
|
||||
@@ -201,7 +201,7 @@ export const useFlowchartStore = defineStore('flowchart', () => {
|
||||
}
|
||||
|
||||
const handleWebSocketMessage = (data: FlowchartEvaluationUpdate) => {
|
||||
// 流程图画到 A/S 算完成之后,后端会在同一条用户通道上推题单徽章、成就(flowchart/run.ts 的
|
||||
// 流程图画到 A/S 算完成之后,后端会在同一条用户通道上推专题徽章、成就(flowchart/run.ts 的
|
||||
// recordSolvedAndNotify)。原来这里只认评分那两种,交代码的监视器又不一定开着,要等下次
|
||||
// 换路由拉 pending 才弹。队列按 id 去重,两条连接各收一遍也只弹一次
|
||||
const frame = data as unknown as { type: string; achievements?: QueuedAchievement[] }
|
||||
|
||||
@@ -11,7 +11,7 @@ export const useProblemStore = defineStore('problem', () => {
|
||||
* 本次会话里新增的失败提交数。**只是增量**,历史失败数看 `problem.myFailedCount`。
|
||||
*
|
||||
* 原来 failCount 就是这一个从 0 起数的 ref,于是「失败 3 次解锁 AI 提示」实际变成了
|
||||
* 「在当前这次页面会话里再失败 3 次」:刷新一下、从题单跳进跳出一次就清零,
|
||||
* 「在当前这次页面会话里再失败 3 次」:刷新一下、从专题跳进跳出一次就清零,
|
||||
* 昨天在这题上撞了十次墙的学生今天进来照样看不到按钮。而后端的闸门数的是数据库里
|
||||
* 的历史失败数,两边根本不是一回事。
|
||||
*/
|
||||
@@ -28,7 +28,7 @@ export const useProblemStore = defineStore('problem', () => {
|
||||
const failCount = computed(() => (problem.value?.myFailedCount ?? 0) + sessionFailCount.value)
|
||||
|
||||
/**
|
||||
* 这道题能不能画流程图。比赛里不给(入口差异表);题单里原来也不给,现在给了,画到 A/S
|
||||
* 这道题能不能画流程图。比赛里不给(入口差异表);专题里原来也不给,现在给了,画到 A/S
|
||||
* 算完成(设计文档第 3 节决定 4)。手机上原来不给(节点库是 HTML5 拖放,拖不动),
|
||||
* 2026-10 换成插入式编辑器之后只用点,手机上也能画
|
||||
*/
|
||||
|
||||
@@ -17,10 +17,10 @@ import type { SubmitCodePayload } from 'utils/types'
|
||||
*
|
||||
* 原来它们都长在提交按钮那个组件(SubmitCode)里,而那个组件在编辑器的工具栏里 ——
|
||||
* 手机上交完切到「描述」页签(Naive 的页签默认切走即卸载)、桌面切到「题目 / 自测」
|
||||
* 模式,编辑器一卸载,判题结果、AI 提示、失败计数、通过后的点评和回题单全都丢了。
|
||||
* 模式,编辑器一卸载,判题结果、AI 提示、失败计数、通过后的点评和回专题全都丢了。
|
||||
* 挪到 store 之后组件卸了再挂回来,状态还在。
|
||||
*
|
||||
* 判完之后「该发生什么」(失败计数、标成已解决、烟花、点评、回题单)不在这里,
|
||||
* 判完之后「该发生什么」(失败计数、标成已解决、烟花、点评、回专题)不在这里,
|
||||
* 在页面这一层的 SubmissionEffects —— 它们要路由守卫和弹窗,得挂在一直在的组件上。
|
||||
*/
|
||||
export type ResultSegment = 'submit' | 'samples' | 'custom'
|
||||
@@ -155,8 +155,8 @@ export const useSubmissionStore = defineStore('submission', () => {
|
||||
if (entry.contestId) {
|
||||
data.contestId = parseInt(entry.contestId)
|
||||
}
|
||||
// 从题单入口进来的,把来源题单一起报上去:提交列表要据此标出「来自题单」。
|
||||
// 只是来源标记,题单进度仍由后端判完之后自己记账(judge/run.ts)
|
||||
// 从专题入口进来的,把来源专题一起报上去:提交列表要据此标出「来自专题」。
|
||||
// 只是来源标记,专题进度仍由后端判完之后自己记账(judge/run.ts)
|
||||
if (entry.problemSetId) {
|
||||
data.problemSetId = parseInt(entry.problemSetId)
|
||||
}
|
||||
|
||||
@@ -193,9 +193,9 @@ const canRetry = computed(
|
||||
|
||||
<div v-if="!row.showLink" class="locked">
|
||||
<Icon icon="ph:lock-simple" :width="30" class="lock-icon" />
|
||||
<!-- 列表项里没有 userId,分不出是别人的还是自己被题单挡住的,文案两种都说得通 -->
|
||||
<!-- 列表项里没有 userId,分不出是别人的还是自己被专题挡住的,文案两种都说得通 -->
|
||||
<span class="locked-title">这张流程图看不到</span>
|
||||
<span class="muted">别人画的看不到;自己以前画的在题单布置期内先藏着,在题单里做对或者布置期结束就能看到。</span>
|
||||
<span class="muted">别人画的看不到;自己以前画的在专题布置期内先藏着,在专题里做对或者布置期结束就能看到。</span>
|
||||
</div>
|
||||
<div v-else class="body">
|
||||
<div class="graph-box">
|
||||
|
||||
@@ -172,9 +172,9 @@ function openStandalone() {
|
||||
class="problemset"
|
||||
:href="'/problemset/' + row.problemSet.id"
|
||||
target="_blank"
|
||||
:title="'从题单「' + row.problemSet.title + '」交的'"
|
||||
:title="'从专题「' + row.problemSet.title + '」交的'"
|
||||
>
|
||||
题单 {{ row.problemSet.title }}
|
||||
专题 {{ row.problemSet.title }}
|
||||
</a>
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
@@ -243,7 +243,7 @@ function openStandalone() {
|
||||
<n-button type="primary" @click="emit('openProblem', row)"> 去做 {{ row.problemDisplayId }} {{ row.problemTitle }} </n-button>
|
||||
</template>
|
||||
<template v-else-if="locked === 'problemset'">
|
||||
<span class="locked-title">这份代码在题单布置期内先藏着</span>
|
||||
<span class="locked-title">这份代码在专题布置期内先藏着</span>
|
||||
<span class="muted"><LockNote :problem="{ displayId: row.problemDisplayId }" /></span>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -132,7 +132,7 @@ function attemptTime(time: string) {
|
||||
/** 除了正在看的这次,还有别的能点开的才提 ← → */
|
||||
const canStep = computed(() => attempts.value.some((item) => item.showLink && item.id !== shown.value?.id))
|
||||
|
||||
/** 「← →」一次次往前往后翻,跳过题单藏着的那几次 */
|
||||
/** 「← →」一次次往前往后翻,跳过专题藏着的那几次 */
|
||||
function step(delta: -1 | 1) {
|
||||
const list = attempts.value
|
||||
for (let i = attemptIndex.value + delta; i >= 0 && i < list.length; i += delta) {
|
||||
@@ -233,8 +233,8 @@ function handleMore(key: string) {
|
||||
<span class="problem-id">{{ shown.problemDisplayId }}</span>
|
||||
{{ shown.problemTitle }}
|
||||
</a>
|
||||
<a v-if="shown.problemSet" class="problemset" :href="'/problemset/' + shown.problemSet.id" :title="'从题单「' + shown.problemSet.title + '」交的'">
|
||||
题单 {{ shown.problemSet.title }}
|
||||
<a v-if="shown.problemSet" class="problemset" :href="'/problemset/' + shown.problemSet.id" :title="'从专题「' + shown.problemSet.title + '」交的'">
|
||||
专题 {{ shown.problemSet.title }}
|
||||
</a>
|
||||
<div class="spacer"></div>
|
||||
<div class="actions">
|
||||
@@ -297,7 +297,7 @@ function handleMore(key: string) {
|
||||
|
||||
<div v-if="!shown.showLink" class="locked">
|
||||
<Icon icon="ph:lock-simple" :width="30" class="lock-icon" />
|
||||
<span class="locked-title">这份代码在题单布置期内先藏着</span>
|
||||
<span class="locked-title">这份代码在专题布置期内先藏着</span>
|
||||
<span class="muted"><LockNote :problem="{ displayId: shown.problemDisplayId }" /></span>
|
||||
</div>
|
||||
<CodeView
|
||||
|
||||
@@ -531,7 +531,7 @@ function languageText(row: SubmissionListItem) {
|
||||
return LANGUAGE_SHOW_VALUE[row.language] ?? row.language
|
||||
}
|
||||
|
||||
/** 学生看别人的那几行:名字淡一档、带一把锁(自己的看不了是题单那道闸,另说) */
|
||||
/** 学生看别人的那几行:名字淡一档、带一把锁(自己的看不了是专题那道闸,另说) */
|
||||
/**
|
||||
* 演示模式(超管把界面装成学生投屏):后端认的还是超管,每一行都给了 showLink。
|
||||
* 学生是看不到别人代码的,投屏时也不该露出来 —— 别人的行一律当成看不到
|
||||
@@ -795,7 +795,7 @@ function dayBreak(index: number) {
|
||||
<span class="title">
|
||||
<span class="pid">{{ row.problemDisplayId }}</span> {{ row.problemTitle }}
|
||||
</span>
|
||||
<span v-if="row.problemSet" class="ps" :title="'从题单「' + row.problemSet.title + '」交的'"> 题单 </span>
|
||||
<span v-if="row.problemSet" class="ps" :title="'从专题「' + row.problemSet.title + '」交的'"> 专题 </span>
|
||||
</span>
|
||||
<span class="c-lang">{{ languageText(row) }}</span>
|
||||
<span class="c-state"><StatusPill :result="row.result" /></span>
|
||||
@@ -804,7 +804,7 @@ function dayBreak(index: number) {
|
||||
<span
|
||||
v-if="!row.showLink"
|
||||
class="lock-hint"
|
||||
:title="othersRow(row) ? '别人的代码看不到' : '题单布置期内先藏着:在题单里做对,或者布置期结束就能看'"
|
||||
:title="othersRow(row) ? '别人的代码看不到' : '专题布置期内先藏着:在专题里做对,或者布置期结束就能看'"
|
||||
>
|
||||
<Icon icon="ph:lock-simple" :width="13" />
|
||||
</span>
|
||||
@@ -819,7 +819,7 @@ function dayBreak(index: number) {
|
||||
<div class="m-line sub">
|
||||
<span class="m-time">{{ submissionClockText(row.createTime) }}</span>
|
||||
<span class="title" :title="`${row.problemDisplayId} ${row.problemTitle}`"> {{ row.problemDisplayId }} {{ row.problemTitle }} </span>
|
||||
<span v-if="row.problemSet" class="ps">题单</span>
|
||||
<span v-if="row.problemSet" class="ps">专题</span>
|
||||
<div class="spacer"></div>
|
||||
<span class="m-lang">{{ languageText(row) }}</span>
|
||||
<Icon v-if="!teacher && !row.showLink" icon="ph:lock-simple" :width="13" class="m-lock" />
|
||||
|
||||
Reference in new issue
Block a user