- 随便来一道:按学生在编辑器里上次用的语言挑(没用过按 Python),C 学生不再抽到 Python 专用题和 SQL 题,Python 学生不再抽到 SQL 题;选了 SQL 知识点或 SQL 类型才从 SQL 题里抽。也跟着「全部类型」的筛选走 - 老师那一版去掉「只看没做完」:没有状态列,开了只是题少了 - 题目页「相似题目」:流程图评到 A / S 的题也算做对,不再推荐,和列表打勾一个口径 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
84bfb44c20
commit
98dfcc1e7e
3 files changed
+58
-23
No files matched your search
@@ -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>>
|
||||||
|
|
||||||
@@ -155,10 +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`)
|
|
||||||
if (type === "sql") filters.push(sql`${schema.problem.languages} @> '["SQL"]'::jsonb`)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -254,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
|
||||||
@@ -282,11 +301,13 @@ 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 里的整数常量是
|
// 没做对过题(没登录、新生)就不排这一项。别拿 sql`1` 占位:ORDER BY 里的整数常量是
|
||||||
// 「按第 1 列排」,会先按题号排、random() 形同虚设,每次都是同一道
|
// 「按第 1 列排」,会先按题号排、random() 形同虚设,每次都是同一道
|
||||||
.orderBy(
|
.orderBy(
|
||||||
|
desc(sql`${schema.problem.languages} @> ${JSON.stringify([language])}::jsonb`),
|
||||||
...(solved.length ? [asc(inArray(schema.problem.id, solved))] : []),
|
...(solved.length ? [asc(inArray(schema.problem.id, solved))] : []),
|
||||||
asc(sql`${schema.problem.difficulty} = 'High'`),
|
asc(sql`${schema.problem.difficulty} = 'High'`),
|
||||||
sql`random()`,
|
sql`random()`,
|
||||||
@@ -337,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`
|
||||||
@@ -388,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)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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)
|
||||||
@@ -279,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
|
||||||
@@ -511,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 ? '做对的题藏起来' : '登录后才能用'"
|
||||||
@@ -665,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>
|
||||||
|
|||||||
Reference in new issue
Block a user