Compare commits

...
5 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5.5 98dfcc1e7e fix(题目列表): 「随便来一道」常抽到用自己的语言做不了的题,老师那边「只看没做完」开了看不出藏了什么
Deploy / deploy (push) Canceled after 0s
- 随便来一道:按学生在编辑器里上次用的语言挑(没用过按 Python),C 学生不再抽到 Python 专用题和 SQL 题,Python 学生不再抽到 SQL 题;选了 SQL 知识点或 SQL 类型才从 SQL 题里抽。也跟着「全部类型」的筛选走
- 老师那一版去掉「只看没做完」:没有状态列,开了只是题少了
- 题目页「相似题目」:流程图评到 A / S 的题也算做对,不再推荐,和列表打勾一个口径

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:56:34 +08:00
xuyueandClaude Opus 5.5 84bfb44c20 feat(题目列表): SQL 题混在一般题里看不出来,点进去才发现不是写 C——和画流程图一样挂「SQL」标签,「全部类型」里也能单独筛
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:51:42 +08:00
xuyueandClaude Opus 5.5 e958d69258 fix(题目列表): 连着快点几个知识点,列表偶尔停在前一个知识点的题上
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:49:09 +08:00
xuyueandClaude Opus 5.5 50660abf11 fix(题目列表): 切了几个知识点再按后退,地址回到了全部题目、页面还停在上一个知识点
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:49:09 +08:00
xuyueandClaude Opus 5.5 c3927d522c fix(题目列表): 新生和没登录的人点「随便来一道」,每次都是同一道题
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:49:09 +08:00
10 changed files with 132 additions and 31 deletions

No files matched your search

+1
View File
@@ -244,6 +244,7 @@ contestRoutes.get(
allowFlowchart: problem.allowFlowchart, allowFlowchart: problem.allowFlowchart,
showFlowchart: problem.showFlowchart, showFlowchart: problem.showFlowchart,
hasAstRules: problem.astRules !== null, hasAstRules: problem.astRules !== null,
isSql: problem.languages.includes("SQL"),
myStatus: myStatusOf(statuses, problem.id), myStatus: myStatusOf(statuses, problem.id),
// 按人数是公开题库列表的列;比赛题表看的是榜单,不下发 // 按人数是公开题库列表的列;比赛题表看的是榜单,不下发
solvedUsers: 0, solvedUsers: 0,
+39 -12
View File
@@ -1,4 +1,5 @@
import { import {
normalizeLanguage,
problemListSortSchema, problemListSortSchema,
problemTypeFilterSchema, problemTypeFilterSchema,
type ProblemAuthor, type ProblemAuthor,
@@ -6,6 +7,7 @@ import {
type ProblemList, type ProblemList,
type ProblemListItem, type ProblemListItem,
type ProblemProgress, type ProblemProgress,
type ProblemTypeFilter,
type Tag, type Tag,
} from "@oj2/contract" } from "@oj2/contract"
import { import {
@@ -81,6 +83,15 @@ async function getProblemTags(problemIds: number[]) {
return result return result
} }
/** 「全部类型」下拉的筛选条件,列表和「随便来一道」共用 */
function typeFilter(type: ProblemTypeFilter | undefined) {
if (type === "reference") return eq(schema.problem.showFlowchart, true)
if (type === "flowchart") return eq(schema.problem.allowFlowchart, true)
if (type === "ast") return sql`${schema.problem.astRules} is not null`
if (type === "sql") return sql`${schema.problem.languages} @> '["SQL"]'::jsonb`
return undefined
}
type ProblemStates = Awaited<ReturnType<typeof problemStates>> | null type ProblemStates = Awaited<ReturnType<typeof problemStates>> | null
type UserCounts = Awaited<ReturnType<typeof problemUserCounts>> type UserCounts = Awaited<ReturnType<typeof problemUserCounts>>
@@ -110,6 +121,7 @@ function listItem(
allowFlowchart: row.problem.allowFlowchart, allowFlowchart: row.problem.allowFlowchart,
showFlowchart: row.problem.showFlowchart, showFlowchart: row.problem.showFlowchart,
hasAstRules: row.problem.astRules !== null, hasAstRules: row.problem.astRules !== null,
isSql: row.problem.languages.includes("SQL"),
// 流程图评到 A / S 也打勾:problemStates 已经把它算进 solved // 流程图评到 A / S 也打勾:problemStates 已经把它算进 solved
myStatus: states?.solved.has(id) myStatus: states?.solved.has(id)
? JudgeStatus.ACCEPTED ? JudgeStatus.ACCEPTED
@@ -154,9 +166,8 @@ problemRoutes.get("/problems", optionalAuth, async (c) => {
), ),
) )
} }
if (type === "reference") filters.push(eq(schema.problem.showFlowchart, true)) const byType = typeFilter(type)
if (type === "flowchart") filters.push(eq(schema.problem.allowFlowchart, true)) if (byType) filters.push(byType)
if (type === "ast") filters.push(sql`${schema.problem.astRules} is not null`)
const states = user ? await problemStates(user.id) : null const states = user ? await problemStates(user.id) : null
if (undone && states && states.solved.size > 0) if (undone && states && states.solved.size > 0)
@@ -252,12 +263,22 @@ problemRoutes.get("/problem-tags", async (c) => {
}) })
/** /**
* 「随便来一道」:范围是当前知识点(不传就是全部),先挑自己还没做对的简单 / 中等题, * 「随便来一道」:范围是当前知识点和类型(不传就是全部),先挑能用自己的语言做的、
* 没有就放宽到没做对的困难题,再没有就随便一道(全做对了也给一道,不让按钮落空)。 * 还没做对的简单 / 中等题,没有就一档档放宽:没做对的困难题 → 做对过的
* (全做对了也给一道,不让按钮落空)→ 别的语言的题。
*
* 语言是前端记着的「上次用的语言」,没有就是 Python。不按语言挑的话,C 学生大约每
* 9 次就抽到 1 道没有 C 的题(Python 专用题和 SQL 题)。语言排在最前、而不是筛掉:
* 选了「SQL」知识点时没有一道能用 Python 做,照样得抽一道 SQL 题出来。
*/ */
problemRoutes.get("/problems/random", optionalAuth, async (c) => { problemRoutes.get("/problems/random", optionalAuth, async (c) => {
const user = c.get("user") const user = c.get("user")
const tag = c.req.query("tag")?.trim() const tag = c.req.query("tag")?.trim()
const type = problemTypeFilterSchema.safeParse(c.req.query("type")).data
// 上次用的是 SQL(刚做完一道 SQL 题)不算偏好:SQL 题一共 9 道,照它挑就成了「随便来一道
// SQL」。要 SQL 题的人会选「SQL」知识点或类型,那时按上面说的照样抽得到
const saved = normalizeLanguage(c.req.query("language"))
const language = saved && saved !== "SQL" && saved !== "Flowchart" ? saved : "Python"
const states = user ? await problemStates(user.id) : null const states = user ? await problemStates(user.id) : null
const solved = states ? [...states.solved] : [] const solved = states ? [...states.solved] : []
const [row] = await db const [row] = await db
@@ -280,10 +301,14 @@ problemRoutes.get("/problems/random", optionalAuth, async (c) => {
.where(eq(schema.problemTag.name, tag)), .where(eq(schema.problemTag.name, tag)),
) )
: undefined, : undefined,
typeFilter(type),
), ),
) )
// 没做对过题(没登录、新生)就不排这一项。别拿 sql`1` 占位:ORDER BY 里的整数常量是
// 「按第 1 列排」,会先按题号排、random() 形同虚设,每次都是同一道
.orderBy( .orderBy(
solved.length ? asc(inArray(schema.problem.id, solved)) : sql`1`, desc(sql`${schema.problem.languages} @> ${JSON.stringify([language])}::jsonb`),
...(solved.length ? [asc(inArray(schema.problem.id, solved))] : []),
asc(sql`${schema.problem.difficulty} = 'High'`), asc(sql`${schema.problem.difficulty} = 'High'`),
sql`random()`, sql`random()`,
) )
@@ -333,11 +358,13 @@ problemRoutes.get("/problems/:displayId/similar", optionalAuth, async (c) => {
// 「已 AC 的不再推荐」必须下推到 SQL。早先是先 limit(5) 再在内存里筛,刷题多的 // 「已 AC 的不再推荐」必须下推到 SQL。早先是先 limit(5) 再在内存里筛,刷题多的
// 学生 5 条候选能被筛到只剩一两条、甚至清零(前端 v-if 一空整块就不渲染)—— // 学生 5 条候选能被筛到只剩一两条、甚至清零(前端 v-if 一空整块就不渲染)——
// 而这个接口恰好只在**刚 AC** 或**连挂三次**时才被调用,正是候选最容易全中的时候。 // 而这个接口恰好只在**刚 AC** 或**连挂三次**时才被调用,正是候选最容易全中的时候。
const statuses = await getProblemStatuses(c.get("user")?.id) // 做对了的和题目列表同一个口径:画流程图的题,流程图评到 A / S 也算(problemStates)
const solvedIds = Object.entries(statuses) const user = c.get("user")
.filter(([, value]) => asRecord(value).status === JudgeStatus.ACCEPTED) const [statuses, states] = await Promise.all([
.map(([key]) => Number(key)) getProblemStatuses(user?.id),
.filter((id) => Number.isInteger(id)) user ? problemStates(user.id) : null,
])
const solvedIds = states ? [...states.solved] : []
// difficulty 是 text(Low / Mid / High),直接 order by 走的是字典序 —— // difficulty 是 text(Low / Mid / High),直接 order by 走的是字典序 ——
// High 排在 Low 前面,「由易到难」会变成「最难的先上」。按语义显式排。 // High 排在 Low 前面,「由易到难」会变成「最难的先上」。按语义显式排。
const difficultyRank = sql`case ${schema.problem.difficulty} when 'Low' then 0 when 'Mid' then 1 else 2 end` const difficultyRank = sql`case ${schema.problem.difficulty} when 'Low' then 0 when 'Mid' then 1 else 2 end`
@@ -384,7 +411,7 @@ problemRoutes.get("/problems/:displayId/similar", optionalAuth, async (c) => {
const [tags, counts] = await Promise.all([getProblemTags(ids), problemUserCounts(ids)]) const [tags, counts] = await Promise.all([getProblemTags(ids), problemUserCounts(ids)])
return success( return success(
c, c,
rows.map((row) => listItem(row, tags, statuses, null, counts)), rows.map((row) => listItem(row, tags, statuses, states, counts)),
) )
}) })
+6 -3
View File
@@ -100,9 +100,12 @@ export function getProblemProgress() {
return api.get<ProblemProgress | null>("problems/progress") return api.get<ProblemProgress | null>("problems/progress")
} }
/** 「随便来一道」:当前知识点里先挑自己没做对的简单 / 中等题,返回题号 */ /**
export function getRandomProblem(tag: string) { * 「随便来一道」:当前知识点和类型里,先挑能用 language 做的、自己没做对的简单 / 中等题,
return api.get<string>("problems/random", { params: tag ? { tag } : {} }) * 返回题号。空串的参数拦截器会去掉
*/
export function getRandomProblem(params: { tag: string; type: string; language: string }) {
return api.get<string>("problems/random", { params })
} }
export function getAuthors(all = false) { export function getAuthors(all = false) {
@@ -5,7 +5,8 @@ import { useThemeVars } from "naive-ui"
import { PROBLEM_TYPE_LABEL } from "../utils/problemType" import { PROBLEM_TYPE_LABEL } from "../utils/problemType"
/** /**
* 三类特殊题的小标签(设计稿「题目列表重设计 · 改版」)。轻重按对学生意味着什么分: * 几类特殊题的小标签(设计稿「题目列表重设计 · 改版」)。轻重按对学生意味着什么分:
* - SQL:和画流程图一样重(实心底)—— 写的根本不是 C / Python,点进去之前就得知道
* - 画流程图:最醒目(浅紫底)—— 只有这类题能交流程图,要做的事和普通题不一样 * - 画流程图:最醒目(浅紫底)—— 只有这类题能交流程图,要做的事和普通题不一样
* - 语法要求:蓝描边 —— 判题时会检查写法,要看得见 * - 语法要求:蓝描边 —— 判题时会检查写法,要看得见
* - 有参考图:灰描边,最轻 —— 是帮助,不用提醒 * - 有参考图:灰描边,最轻 —— 是帮助,不用提醒
@@ -16,6 +17,7 @@ const props = defineProps<{ kind: ProblemTypeFilter }>()
const theme = useThemeVars() const theme = useThemeVars()
const ICON: Record<ProblemTypeFilter, string> = { const ICON: Record<ProblemTypeFilter, string> = {
sql: "ph:database-bold",
flowchart: "ph:pencil-simple-line-bold", flowchart: "ph:pencil-simple-line-bold",
ast: "ph:code-bold", ast: "ph:code-bold",
reference: "ph:flow-arrow-bold", reference: "ph:flow-arrow-bold",
@@ -67,12 +69,38 @@ html.dark .type-tag.flowchart {
background: rgba(160, 120, 240, 0.18); background: rgba(160, 120, 240, 0.18);
} }
/* 青色:绿 / 橙 / 红是难度、紫是画流程图、蓝是语法要求,剩下它不和谁撞 */
.type-tag.sql {
font-weight: 600;
color: #0b6470;
background: #dcf1f3;
border-color: transparent;
}
html.dark .type-tag.sql {
color: #8fd8e0;
background: rgba(60, 180, 195, 0.18);
}
.type-tag.ast { .type-tag.ast {
color: var(--tag-info); color: var(--tag-info);
border-color: #b9cdee; border-color: #b9cdee;
} }
html.dark .type-tag.ast { html.dark /* 青色:绿 / 橙 / 红是难度、紫是画流程图、蓝是语法要求,剩下它不和谁撞 */
.type-tag.sql {
font-weight: 600;
color: #0b6470;
background: #dcf1f3;
border-color: transparent;
}
html.dark .type-tag.sql {
color: #8fd8e0;
background: rgba(60, 180, 195, 0.18);
}
.type-tag.ast {
color: var(--tag-info-dark); color: var(--tag-info-dark);
border-color: rgba(112, 192, 232, 0.45); border-color: rgba(112, 192, 232, 0.45);
} }
+22 -9
View File
@@ -4,6 +4,7 @@ import type { ProblemProgress, ProblemTypeFilter, Tag } from "@oj2/contract"
import { useRouteQuery } from "@vueuse/router" import { useRouteQuery } from "@vueuse/router"
import { useThemeVars } from "naive-ui" import { useThemeVars } from "naive-ui"
import { getAuthors, getProblemList, getProblemProgress, getRandomProblem } from "oj/api" import { getAuthors, getProblemList, getProblemProgress, getRandomProblem } from "oj/api"
import { useCodeStore } from "oj/store/code"
import { useTone } from "oj/submission/composables/tone" import { useTone } from "oj/submission/composables/tone"
import { STORAGE_KEY } from "utils/constants" import { STORAGE_KEY } from "utils/constants"
import storage from "utils/storage" import storage from "utils/storage"
@@ -44,7 +45,7 @@ const DIFFICULTIES = [
{ label: "困难", value: "High" }, { label: "困难", value: "High" },
] ]
const TYPES: ProblemTypeFilter[] = ["reference", "flowchart", "ast"] const TYPES: ProblemTypeFilter[] = ["sql", "reference", "flowchart", "ast"]
/** 新生一道还没做对时,顶行引去的知识点 */ /** 新生一道还没做对时,顶行引去的知识点 */
const START_TAG = "输出入门" const START_TAG = "输出入门"
@@ -54,6 +55,7 @@ const userStore = useUserStore()
const authStore = useAuthModalStore() const authStore = useAuthModalStore()
const theme = useThemeVars() const theme = useThemeVars()
const tone = useTone() const tone = useTone()
const codeStore = useCodeStore()
const { isDesktop, isMobile } = useBreakpoints() const { isDesktop, isMobile } = useBreakpoints()
const { query } = usePagination<ProblemQuery>( const { query } = usePagination<ProblemQuery>(
@@ -69,11 +71,6 @@ const { query } = usePagination<ProblemQuery>(
{ defaultLimit: 30 }, { defaultLimit: 30 },
) )
const undoneOn = computed({
get: () => query.undone === "1",
set: (on: boolean) => (query.undone = on ? "1" : ""),
})
const problems = ref<ProblemRow[]>([]) const problems = ref<ProblemRow[]>([])
const total = ref(0) const total = ref(0)
const loaded = ref(false) const loaded = ref(false)
@@ -86,6 +83,12 @@ const teacher = computed(() => userStore.isTeacherOrAbove)
const showStatus = computed(() => userStore.isAuthed && !teacher.value) const showStatus = computed(() => userStore.isAuthed && !teacher.value)
const mine = computed(() => (showStatus.value ? progress.value : null)) const mine = computed(() => (showStatus.value ? progress.value : null))
// 老师那一版没有这个开关:没有状态列,开了也看不出藏了哪些题。带着 ?undone=1 进来也当没开
const undoneOn = computed({
get: () => query.undone === "1" && !teacher.value,
set: (on: boolean) => (query.undone = on ? "1" : ""),
})
const tagByName = computed(() => new Map(tags.value.map((t) => [t.name, t]))) const tagByName = computed(() => new Map(tags.value.map((t) => [t.name, t])))
const currentTag = computed(() => tagByName.value.get(query.tag) ?? null) const currentTag = computed(() => tagByName.value.get(query.tag) ?? null)
const startTag = computed(() => tagByName.value.get(START_TAG) ?? null) const startTag = computed(() => tagByName.value.get(START_TAG) ?? null)
@@ -94,8 +97,11 @@ const hasExtraFilter = computed(
() => !!(query.keyword || query.difficulty || query.author || query.type), () => !!(query.keyword || query.difficulty || query.author || query.type),
) )
// 连着点几个知识点时,先发的请求可能后回来(按人数排序那条要慢一截),只认最后一次
let listSeq = 0
async function listProblems() { async function listProblems() {
if (query.page < 1) query.page = 1 if (query.page < 1) query.page = 1
const seq = ++listSeq
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getProblemList(offset, query.limit, { const res = await getProblemList(offset, query.limit, {
keyword: query.keyword, keyword: query.keyword,
@@ -107,6 +113,7 @@ async function listProblems() {
// 不看 isAuthed:首屏请求发出时登录信息往往还没回来,后端对没登录的人自己会忽略它 // 不看 isAuthed:首屏请求发出时登录信息往往还没回来,后端对没登录的人自己会忽略它
undone: undoneOn.value ? "1" : "", undone: undoneOn.value ? "1" : "",
}) })
if (seq !== listSeq) return
total.value = res.total total.value = res.total
problems.value = res.results problems.value = res.results
loaded.value = true loaded.value = true
@@ -275,7 +282,12 @@ const picking = ref(false)
async function pickRandom() { async function pickRandom() {
picking.value = true picking.value = true
try { try {
const id = await getRandomProblem(query.tag) const id = await getRandomProblem({
tag: query.tag,
type: query.type,
// 只挑能用自己的语言做的题:学生在编辑器里上次用的语言,没用过就是 Python
language: codeStore.preferredLanguage(),
})
router.push("/problem/" + id) router.push("/problem/" + id)
} finally { } finally {
picking.value = false picking.value = false
@@ -507,8 +519,9 @@ const gridColumns = computed(() =>
:consistent-menu-width="false" :consistent-menu-width="false"
:options="sortOptions" :options="sortOptions"
/> />
<span class="vsep"></span> <span v-if="!teacher" class="vsep"></span>
<label <label
v-if="!teacher"
class="undone" class="undone"
:class="{ disabled: !userStore.isAuthed }" :class="{ disabled: !userStore.isAuthed }"
:title="userStore.isAuthed ? '做对的题藏起来' : '登录后才能用'" :title="userStore.isAuthed ? '做对的题藏起来' : '登录后才能用'"
@@ -661,7 +674,7 @@ const gridColumns = computed(() =>
@update:value="(v: string | null) => (query.author = v ?? '')" @update:value="(v: string | null) => (query.author = v ?? '')"
/> />
<n-select v-model:value="query.sort" size="small" :options="sortOptions" /> <n-select v-model:value="query.sort" size="small" :options="sortOptions" />
<label class="undone" :class="{ disabled: !userStore.isAuthed }"> <label v-if="!teacher" class="undone" :class="{ disabled: !userStore.isAuthed }">
<n-switch v-model:value="undoneOn" size="small" :disabled="!userStore.isAuthed" /> <n-switch v-model:value="undoneOn" size="small" :disabled="!userStore.isAuthed" />
只看没做完 只看没做完
</label> </label>
+5 -1
View File
@@ -2,6 +2,7 @@ import type { ProblemTypeFilter } from "@oj2/contract"
/** 名字和题目页一致:「语法要求」(题目页原来叫「代码要求」,一起改了) */ /** 名字和题目页一致:「语法要求」(题目页原来叫「代码要求」,一起改了) */
export const PROBLEM_TYPE_LABEL: Record<ProblemTypeFilter, string> = { export const PROBLEM_TYPE_LABEL: Record<ProblemTypeFilter, string> = {
sql: "SQL",
flowchart: "画流程图", flowchart: "画流程图",
ast: "语法要求", ast: "语法要求",
reference: "有参考图", reference: "有参考图",
@@ -9,18 +10,21 @@ export const PROBLEM_TYPE_LABEL: Record<ProblemTypeFilter, string> = {
/** 「全部类型」下拉里每项下面那句话 */ /** 「全部类型」下拉里每项下面那句话 */
export const PROBLEM_TYPE_HINT: Record<ProblemTypeFilter, string> = { export const PROBLEM_TYPE_HINT: Record<ProblemTypeFilter, string> = {
sql: "写 SQL 语句查数据库,不是写 C / Python 程序",
reference: "题目里附了老师画好的流程图,照着写代码", reference: "题目里附了老师画好的流程图,照着写代码",
flowchart: "除了写代码,也可以交流程图;评到 A 或 S 级算做对", flowchart: "除了写代码,也可以交流程图;评到 A 或 S 级算做对",
ast: "要求用(或者不能用)某种写法,交了会检查", ast: "要求用(或者不能用)某种写法,交了会检查",
} }
/** 一道题挂哪几个标签,从醒目到轻:画流程图 → 语法要求 → 有参考图 */ /** 一道题挂哪几个标签,从醒目到轻:SQL → 画流程图 → 语法要求 → 有参考图 */
export function problemTypes(problem: { export function problemTypes(problem: {
isSql: boolean
allowFlowchart: boolean allowFlowchart: boolean
hasAstRules: boolean hasAstRules: boolean
showFlowchart: boolean showFlowchart: boolean
}): ProblemTypeFilter[] { }): ProblemTypeFilter[] {
const kinds: ProblemTypeFilter[] = [] const kinds: ProblemTypeFilter[] = []
if (problem.isSql) kinds.push("sql")
if (problem.allowFlowchart) kinds.push("flowchart") if (problem.allowFlowchart) kinds.push("flowchart")
if (problem.hasAstRules) kinds.push("ast") if (problem.hasAstRules) kinds.push("ast")
if (problem.showFlowchart) kinds.push("reference") if (problem.showFlowchart) kinds.push("reference")
+1
View File
@@ -23,6 +23,7 @@ export function toProblemRow(result: ProblemListItem): ProblemRow {
allowFlowchart: result.allowFlowchart, allowFlowchart: result.allowFlowchart,
showFlowchart: result.showFlowchart, showFlowchart: result.showFlowchart,
hasAstRules: result.hasAstRules, hasAstRules: result.hasAstRules,
isSql: result.isSql,
solvedUsers: result.solvedUsers, solvedUsers: result.solvedUsers,
triedUsers: result.triedUsers, triedUsers: result.triedUsers,
} }
+23 -2
View File
@@ -49,7 +49,14 @@ export function usePagination<T extends Record<string, any>>(
// 同步其他查询参数 // 同步其他查询参数
Object.keys(initialQuery).forEach((key) => { Object.keys(initialQuery).forEach((key) => {
const value = route.query[key] const value = route.query[key]
if (value !== undefined) { if (value === undefined) {
// 地址里没有这一项(后退到不带筛选的地址、点顶栏回到列表)就是没筛,
// 不能留着上一个地址的值:后退回 /problem 页面还停在上一个知识点。
// 和 clearQuery 一样清成空值 —— initialQuery 是进页面时从地址读的,不一定是空的
const initialValue = initialQuery[key]
writable[key] =
typeof initialValue === "boolean" ? false : typeof initialValue === "number" ? 0 : ""
} else {
// 处理不同类型的参数 // 处理不同类型的参数
if (typeof initialQuery[key] === "boolean") { if (typeof initialQuery[key] === "boolean") {
writable[key] = value === "1" || value === "true" writable[key] = value === "1" || value === "true"
@@ -65,7 +72,21 @@ export function usePagination<T extends Record<string, any>>(
// 更新 URL // 更新 URL
function updateRoute() { function updateRoute() {
const newQuery = filterEmptyValue(query) const newQuery = filterEmptyValue(query)
router.push({ // 和当前地址只差默认的 page / limit(后退到 /problem,同步完又补上 ?page=1&limit=30):
// 是同一个页面,用 replace,别多压一条历史、让人得多按一次后退
const normalized = (q: Record<string, unknown>) =>
JSON.stringify(
Object.entries(q)
.filter(([key, value]) => {
if (key === "page") return String(value) !== String(defaultPage)
if (key === "limit") return String(value) !== String(defaultLimit)
return value != null && value !== ""
})
.map(([key, value]) => [key, String(value)])
.sort(),
)
const navigate = normalized(route.query) === normalized(newQuery) ? router.replace : router.push
navigate({
path: route.path, path: route.path,
query: newQuery, query: newQuery,
}) })
+1
View File
@@ -167,6 +167,7 @@ export interface ProblemRow {
allowFlowchart: boolean allowFlowchart: boolean
showFlowchart: boolean showFlowchart: boolean
hasAstRules: boolean hasAstRules: boolean
isSql: boolean
solvedUsers: number solvedUsers: number
triedUsers: number triedUsers: number
} }
+4 -2
View File
@@ -424,6 +424,8 @@ export const problemListItemSchema = z.object({
allowFlowchart: z.boolean(), allowFlowchart: z.boolean(),
showFlowchart: z.boolean(), showFlowchart: z.boolean(),
hasAstRules: z.boolean(), hasAstRules: z.boolean(),
/** SQL 题(可选语言里有 SQL):写 SQL 语句查数据库,和写 C / Python 是两回事,列表里单独标出来 */
isSql: z.boolean(),
/** 0 = 做对了。画流程图的题,流程图评到 A / S 也给 0(和题单、课堂条一个口径) */ /** 0 = 做对了。画流程图的题,流程图评到 A / S 也给 0(和题单、课堂条一个口径) */
myStatus: z.number().int().nullable(), myStatus: z.number().int().nullable(),
/** /**
@@ -436,8 +438,8 @@ export const problemListItemSchema = z.object({
export const problemListSchema = paginatedSchema(problemListItemSchema) export const problemListSchema = paginatedSchema(problemListItemSchema)
/** 题目列表「全部类型」下拉:有参考图 / 画流程图 / 语法要求 */ /** 题目列表「全部类型」下拉:SQL / 有参考图 / 画流程图 / 语法要求 */
export const problemTypeFilterSchema = z.enum(["reference", "flowchart", "ast"]) export const problemTypeFilterSchema = z.enum(["sql", "reference", "flowchart", "ast"])
/** /**
* 题目列表的排序。值沿用原来的(旧链接照样能用):空串 = 最新创建、create_time = 最早创建, * 题目列表的排序。值沿用原来的(旧链接照样能用):空串 = 最新创建、create_time = 最早创建,