feat(题单): 题单页重做——布置中的放最上面,详情顶上一条进度和奖章阶梯,没加入看得见题点不进;老师看全班、后台一屏编辑
Deploy / deploy (push) Canceled after 0s

- 列表:布置中的大卡片(做对几道、布置到哪天、再做对几道拿哪个奖章),其余小卡片,加入过的在前
- 详情:进度 + 奖章阶梯(下一个金圈「再做对 n 道」),题目两列写几点做对 / 交过几次;
  以前做对过的标「旧代码先藏着」;做完整条变绿,烟花只在做完那一刻放
- 老师多一个「全班情况」页签;题目页从链接直接进来没加入时,条上给「加入题单」
- 后台一屏编辑:布置一周 / 两周 / 四周 / 选日期,公开开关,粘贴多个题号一次加,拖动排序,奖章只剩两种条件
- 旧代码藏着的地方都说清楚是哪个题单、哪天之后能看
- 修掉列表卡片把选做题数说成「新加了 N 题」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-09 22:01:39 +08:00
1 parent 8c53e28dcb
commit c17f88cdbb
32 files changed
+2630 -2181

No files matched your search

+53 -115
View File
@@ -5,87 +5,68 @@ import { usePagination } from "shared/composables/pagination"
import { parseTime } from "utils/functions"
import type { AdminProblemSet } from "utils/types"
import { getProblemSetList, toggleProblemSetVisible } from "../api"
import Actions from "./components/Actions.vue"
import { NTag, NSwitch } from "naive-ui"
import { NButton, NSwitch, NTag } from "naive-ui"
/**
* 后台题单列表。难度、状态两列和筛选拿掉了(线上全是简单 / 活跃,「状态 + 可见」合成了「公开」),
* 换成老师真正关心的:几道题、多少人加入 / 做完、布置到哪天。点一行进一屏编辑
*/
const router = useRouter()
const total = ref(0)
const problemSets = ref<AdminProblemSet[]>([])
interface ProblemSetQuery {
keyword: string
difficulty: string
status: string
const { query, clearQuery } = usePagination<{ keyword: string }>({ keyword: "" })
function edit(row: AdminProblemSet) {
router.push({ name: "admin problemset edit", params: { problemSetId: row.id } })
}
// 使用分页 composable
const { query, clearQuery } = usePagination<ProblemSetQuery>({
keyword: "",
difficulty: "",
status: "",
})
const difficultyOptions = [
{ label: "全部", value: "" },
{ label: "简单", value: "Easy" },
{ label: "中等", value: "Medium" },
{ label: "困难", value: "Hard" },
]
const statusOptions = [
{ label: "全部", value: "" },
{ label: "活跃", value: "active" },
{ label: "已归档", value: "archived" },
{ label: "草稿", value: "draft" },
]
const columns: DataTableColumn<AdminProblemSet>[] = [
{ title: "ID", key: "id", width: 80 },
{ title: "标题", key: "title", minWidth: 200 },
{ title: "描述", key: "description", minWidth: 300, ellipsis: true },
{ title: "ID", key: "id", width: 70 },
{
title: "题单",
key: "title",
minWidth: 200,
render: (row) =>
h(NButton, { text: true, onClick: () => edit(row) }, { default: () => row.title }),
},
{ title: "题数", key: "problemsCount", width: 70 },
{
title: "加入 / 做完",
key: "participantCount",
width: 110,
render: (row) => `${row.participantCount} / ${row.completedCount}`,
},
{ title: "奖章", key: "badgeCount", width: 70 },
{
title: "布置",
key: "assignedUntil",
width: 130,
render: (row) =>
row.assigning
? h(
NTag,
{ type: "info", size: "small", bordered: false },
{ default: () => `到 ${parseTime(row.assignedUntil!, "M月D日")}` },
)
: "—",
},
{
title: "创建者",
key: "created_by",
width: 120,
key: "createdBy",
width: 110,
render: (row) => row.createdBy.username,
},
{
title: "难度",
key: "difficulty",
width: 100,
render: (row) => {
const difficultyMap = {
Easy: { type: "success" as const, text: "简单" },
Medium: { type: "warning" as const, text: "中等" },
Hard: { type: "error" as const, text: "困难" },
}
const config = difficultyMap[row.difficulty]
return h(NTag, { type: config.type, size: "small" }, { default: () => config.text })
},
},
{
title: "状态",
key: "status",
width: 100,
render: (row) => {
const statusMap = {
active: { type: "success" as const, text: "活跃" },
archived: { type: "default" as const, text: "已归档" },
draft: { type: "info" as const, text: "草稿" },
}
const config = statusMap[row.status]
return h(NTag, { type: config.type, size: "small" }, { default: () => config.text })
},
},
{
title: "创建时间",
key: "create_time",
width: 180,
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
key: "createTime",
width: 120,
render: (row) => parseTime(row.createTime, "YYYY-MM-DD"),
},
{
title: "可见",
title: "公开",
key: "visible",
width: 100,
width: 80,
render: (row) =>
h(NSwitch, {
value: row.visible,
@@ -94,84 +75,41 @@ const columns: DataTableColumn<AdminProblemSet>[] = [
onUpdateValue: () => toggleVisible(row.id),
}),
},
{
title: "选项",
key: "actions",
fixed: "right",
width: 300,
render: (row) =>
h(Actions, {
problemSetId: row.id,
onUpdated: listProblemSets,
}),
},
]
async function listProblemSets() {
if (query.page < 1) query.page = 1
const offset = (query.page - 1) * query.limit
const res = await getProblemSetList(
offset,
query.limit,
query.keyword,
query.difficulty,
query.status,
)
const res = await getProblemSetList(offset, query.limit, query.keyword)
total.value = res.total
problemSets.value = res.results
}
async function toggleVisible(problemSetId: number) {
await toggleProblemSetVisible(problemSetId)
problemSets.value = problemSets.value.map((it) => {
if (it.id === problemSetId) {
it.visible = !it.visible
}
return it
})
const updated = await toggleProblemSetVisible(problemSetId)
problemSets.value = problemSets.value.map((it) => (it.id === problemSetId ? updated : it))
}
onMounted(listProblemSets)
// 监听搜索关键词变化(防抖)
watchDebounced(() => query.keyword, listProblemSets, {
debounce: 500,
maxWait: 1000,
})
// 监听其他查询条件变化
watch(() => [query.page, query.limit, query.difficulty, query.status], listProblemSets)
watchDebounced(() => query.keyword, listProblemSets, { debounce: 500, maxWait: 1000 })
watch(() => [query.page, query.limit], listProblemSets)
</script>
<template>
<PageHeader title="题单列表">
<template #actions>
<n-button type="primary" @click="$router.push({ name: 'admin problemset create' })">
<n-button type="primary" @click="router.push({ name: 'admin problemset create' })">
新建题单
</n-button>
</template>
<template #filters>
<n-input
v-model:value="query.keyword"
placeholder="输入标题关键字"
placeholder="搜题单名字"
clearable
@clear="clearQuery"
style="width: 220px"
/>
<n-select
v-model:value="query.difficulty"
:options="difficultyOptions"
placeholder="全部难度"
style="width: 120px"
clearable
/>
<n-select
v-model:value="query.status"
:options="statusOptions"
placeholder="全部状态"
style="width: 120px"
clearable
/>
</template>
</PageHeader>
<n-data-table striped :columns="columns" :data="problemSets" />