style(专题): 「题单」改名「专题」,顶栏、列表、详情、后台、成就说法统一
Deploy / deploy (push) Canceled after 0s

只改界面文字和注释:网址 /problemset、接口、表名、字段名都不动,老链接照常能打开。
设计稿页名「题单重设计」和已执行的迁移文件保留原样。
线上 3 个成就(题单收藏家 / 入门 / 通关)已同步改名。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-03 00:12:18 +08:00
1 parent 959c326bc9
commit 1b703d13ae
68 files changed
+306 -306

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ const editing = ref<AdminAchievement | null>(null)
const offline = computed(() => list.value.filter((a) => !a.visible).length)
/** 按指标分组,指标按后端给的顺序(做题 → 坚持 → 比赛题单 → 趣味),组内按门槛从低到高 */
/** 按指标分组,指标按后端给的顺序(做题 → 坚持 → 比赛和专题 → 趣味),组内按门槛从低到高 */
const groups = computed(() => {
const byMetric = new Map<string, AdminAchievement[]>()
for (const a of list.value) byMetric.set(a.metric, [...(byMetric.get(a.metric) ?? []), a])
+3 -3
View File
@@ -604,7 +604,7 @@ export function updateACMHelperChecked(contest_id: number, rank_id: number, prob
} satisfies UpdateAcmHelperRequest)
}
// 题单管理 API
// 专题管理 API
export function getProblemSetList(offset = 0, limit = 10, keyword = '') {
return api.get<AdminProblemSetList>('admin/problem-sets', {
params: { offset, limit, keyword },
@@ -639,7 +639,7 @@ export function toggleProblemSetVisible(id: number) {
return api.put<AdminProblemSet>(`admin/problem-sets/${id}/visibility`)
}
// 题单题目管理 API
// 专题题目管理 API
export function getProblemSetProblems(problemSetId: number) {
return api.get<AdminProblemSetProblem[]>(`admin/problem-sets/${problemSetId}/problems`)
}
@@ -667,7 +667,7 @@ export function removeProblemFromSet(problemSetId: number, itemId: number) {
return api.delete(`admin/problem-sets/${problemSetId}/problems/${itemId}`)
}
// 题单奖章管理 API
// 专题奖章管理 API
export function getProblemSetBadges(problemSetId: number) {
return api.get<AdminProblemSetBadge[]>(`admin/problem-sets/${problemSetId}/badges`)
}
+2 -2
View File
@@ -16,7 +16,7 @@ import { getBaseInfo, randomUser10 } from '../api'
/**
* 后台首页(设计稿「后台首页」AdminHome):一块看板。上面一排看「现在」—— 判题机在不在、
* 今天交了多少其中有没有系统错误、现在几个人在线、哪个班在上课;中间一排全站的数(用户、题目、
* 提交、比赛、题单、自学、这周做题的人,用户要的「类似一个看板」)和最近 30 天每天交了几次;
* 提交、比赛、专题、自学、这周做题的人,用户要的「类似一个看板」)和最近 30 天每天交了几次;
* 最下面是常用的四个入口和「抽一个人」。
*
* 原来是总用户数 / 今日提交 / 近期比赛 / 判题机四个数,加一张要手输班级前缀的班级排名表 ——
@@ -52,7 +52,7 @@ const siteTiles = computed(() => {
{ label: '提交', value: n(site.submissions), note: `这学期 ${n(site.termSubmissions)} 次` },
{ label: '这周做题的人', value: n(site.weekActive), note: '周一起交过题的' },
{ label: '比赛', value: n(site.contests), note: '', to: '/admin/contest/list' },
{ label: '题单', value: n(site.problemSets), note: '', to: '/admin/problemset/list' },
{ label: '专题', value: n(site.problemSets), note: '', to: '/admin/problemset/list' },
{ label: '自学', value: n(site.tutorials), note: '课', to: '/admin/tutorial/list' },
]
})
+12 -12
View File
@@ -24,11 +24,11 @@ import {
import BadgeModal from './components/BadgeModal.vue'
/**
* 新建 / 编辑题单,一屏做完(设计稿「题单重设计」后台那块):左边名字、说明、布置期、公开;
* 新建 / 编辑专题,一屏做完(设计稿「题单重设计」后台那块):左边名字、说明、布置期、公开;
* 右边题目(搜题号或一次粘贴好几个、拖动排序、点一下切选做)和奖章。
*
* 布置期:选「布置到哪天」,布置期内这几道题学生以前交的代码先藏起来(防抄),不拦做题。
* 新建时题目先攒在页面上,保存时建好题单再一次加进去;编辑时加、删、排、切选做当场生效。
* 新建时题目先攒在页面上,保存时建好专题再一次加进去;编辑时加、删、排、切选做当场生效。
*/
const props = defineProps<{ problemSetId?: string }>()
@@ -196,7 +196,7 @@ function remove(item: Item) {
return
}
dialog.warning({
title: '从题单里拿掉这道题?',
title: '从专题里拿掉这道题?',
content: `「${item.title}」。已经加入的 ${current.value?.participantCount ?? 0} 个人的进度会重算,题目本身不受影响。`,
positiveText: '拿掉',
negativeText: '不拿',
@@ -312,7 +312,7 @@ async function load() {
async function submit() {
if (!form.title.trim()) {
message.error('题单还没起名字')
message.error('专题还没起名字')
return
}
if (untilMs.value !== null && untilMs.value <= Date.now()) {
@@ -346,7 +346,7 @@ async function submit() {
}
}
}
message.success(items.value.length ? `题单建好了,${items.value.length} 道题都加进去了` : '题单建好了')
message.success(items.value.length ? `专题建好了,${items.value.length} 道题都加进去了` : '专题建好了')
router.replace({ name: 'admin problemset edit', params: { problemSetId: created.id } })
} catch (err) {
message.error(errorMessage(err))
@@ -358,7 +358,7 @@ async function submit() {
function removeSet() {
const set = current.value!
dialog.error({
title: `删除题单「${set.title}」?`,
title: `删除专题「${set.title}」?`,
content: set.participantCount
? `${set.participantCount} 个人的进度和他们拿到的奖章会一起删掉,找不回来。只是不想让学生看到的话,关掉「公开」就行。`
: '还没有人加入。删掉找不回来。',
@@ -380,9 +380,9 @@ onMounted(load)
</script>
<template>
<PageHeader :title="editing ? '编辑题单' : '新建题单'" :back="{ name: 'admin problemset list' }">
<PageHeader :title="editing ? '编辑专题' : '新建专题'" :back="{ name: 'admin problemset list' }">
<template v-if="editing" #actions>
<n-button tag="a" :href="`/problemset/${problemSetId}`" target="_blank"> 到前台看这个题单 </n-button>
<n-button tag="a" :href="`/problemset/${problemSetId}`" target="_blank"> 到前台看这个专题 </n-button>
</template>
</PageHeader>
@@ -410,7 +410,7 @@ onMounted(load)
</n-flex>
<span class="hint">{{ assignHint }}</span>
<span class="hint">
布置期内,题单里这几道题学生以前交的代码先藏起来(不管加没加入),在题单里做对或者到期后再放出来,防止照着旧答案抄。不拦做题,到期后照样能做、照样算进度。
布置期内,专题里这几道题学生以前交的代码先藏起来(不管加没加入),在专题里做对或者到期后再放出来,防止照着旧答案抄。不拦做题,到期后照样能做、照样算进度。
</span>
</div>
</div>
@@ -418,7 +418,7 @@ onMounted(load)
<div class="field">
<span class="label">公开</span>
<n-switch v-model:value="form.visible" />
<span class="hint">{{ form.visible ? '学生能在题单列表里看到' : '藏起来,学生看不到也进不去' }}</span>
<span class="hint">{{ form.visible ? '学生能在专题列表里看到' : '藏起来,学生看不到也进不去' }}</span>
</div>
<div v-if="current" class="stats">
@@ -434,7 +434,7 @@ onMounted(load)
><template v-if="editing"> · {{ badges.length }} 个奖章</template></span
>
<div class="spacer"></div>
<n-button v-if="editing" text type="error" @click="removeSet">删除题单</n-button>
<n-button v-if="editing" text type="error" @click="removeSet">删除专题</n-button>
</div>
</section>
@@ -503,7 +503,7 @@ onMounted(load)
<div class="spacer"></div>
<n-button v-if="editing" size="small" @click="openBadge(null)">+ 加奖章</n-button>
</div>
<div v-if="!editing" class="empty hint">先保存题单,再加奖章</div>
<div v-if="!editing" class="empty hint">先保存专题,再加奖章</div>
<div v-else-if="badges.length" class="list">
<div v-for="badge in badges" :key="badge.id" class="item badge">
<img :src="badge.icon" alt="" class="badge-icon" />
+7 -7
View File
@@ -10,7 +10,7 @@ import { useBreakpoints } from 'shared/composables/breakpoints'
import { useListLoader } from 'shared/composables/listLoader'
/**
* 后台题单列表。难度、状态两列和筛选拿掉了(线上全是简单 / 活跃,「状态 + 可见」合成了「公开」),
* 后台专题列表。难度、状态两列和筛选拿掉了(线上全是简单 / 活跃,「状态 + 可见」合成了「公开」),
* 换成老师真正关心的:几道题、多少人加入 / 做完、布置到哪天。点一行进一屏编辑
*/
const router = useRouter()
@@ -42,7 +42,7 @@ function renderVisible(row: AdminProblemSet) {
})
}
/** 手机上一个题单一张卡:名字一行(点了编辑),题数 · 加入 / 做完 · 奖章 · 创建者 · 日期一行小字 */
/** 手机上一个专题一张卡:名字一行(点了编辑),题数 · 加入 / 做完 · 奖章 · 创建者 · 日期一行小字 */
function renderMobileCard(row: AdminProblemSet) {
const items = [
`${row.problemsCount} 道题`,
@@ -62,14 +62,14 @@ function renderMobileCard(row: AdminProblemSet) {
}
const mobileColumns: DataTableColumn<AdminProblemSet>[] = [
{ title: '题单', key: 'title', render: renderMobileCard },
{ title: '专题', key: 'title', render: renderMobileCard },
{ title: '公开', key: 'visible', width: 72, align: 'center', render: renderVisible },
]
const desktopColumns: DataTableColumn<AdminProblemSet>[] = [
{ title: 'ID', key: 'id', width: 70 },
{
title: '题单',
title: '专题',
key: 'title',
minWidth: 200,
render: renderTitle,
@@ -131,12 +131,12 @@ watch(() => [query.page, query.limit], listProblemSets)
</script>
<template>
<PageHeader title="题单列表">
<PageHeader title="专题列表">
<template #actions>
<n-button type="primary" @click="router.push({ name: 'admin problemset create' })"> 新建题单 </n-button>
<n-button type="primary" @click="router.push({ name: 'admin problemset create' })"> 新建专题 </n-button>
</template>
<template #filters>
<n-input v-model:value="query.keyword" placeholder="搜题单名字" clearable @clear="clearQuery" :style="{ width: isMobile ? '100%' : '220px' }" />
<n-input v-model:value="query.keyword" placeholder="搜专题名字" clearable @clear="clearQuery" :style="{ width: isMobile ? '100%' : '220px' }" />
</template>
</PageHeader>
<n-data-table :loading="loading" striped class="set-table" :columns="columns" :data="problemSets" />