- 「画流程图 / 语法要求 / 有参考图」标签不再各自调 useDark():vueuse 每个实例都会重写 <html> 的 class、插一段禁用过渡的样式再强制整页重算,一页十几个标签就重算十几遍。 明暗两套色改走 CSS(html.dark)。每次切换 Vue 更新 46ms → 15ms、样式重算 24ms → 1.6ms - 新索引 submission_public_problem_user_idx(problem_id, user_id, result,只含公开提交): 每页「做对 / 做过人数」原来要把以 user_id 打头的索引 11 万行扫一遍。 列表接口 19ms → 9ms,「最多人做对」排序 55ms → 26ms - 第一次打开、没带筛选时,不再单独多数一次题目总数 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed7752afbb
commit
74af88046e
6 files changed
+4196
-21
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX "submission_public_problem_user_idx" ON "submission" USING btree ("problem_id","user_id","result") WHERE "submission"."contest_id" is null;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -176,6 +176,13 @@
|
|||||||
"when": 1790732755406,
|
"when": 1790732755406,
|
||||||
"tag": "0024_add_class_lesson_language",
|
"tag": "0024_add_class_lesson_language",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 25,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791521256834,
|
||||||
|
"tag": "0025_submission_problem_user_idx",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -906,6 +906,24 @@ export const submission = pgTable(
|
|||||||
table.createTime.asc().nullsLast(),
|
table.createTime.asc().nullsLast(),
|
||||||
)
|
)
|
||||||
.where(sql`${table.contestId} is null`),
|
.where(sql`${table.contestId} is null`),
|
||||||
|
/**
|
||||||
|
* 题目列表每一页的「做对 / 做过人数」(`services/problem-progress.ts` 的
|
||||||
|
* problemUserCounts):按 problem_id 取一页 30 道题、数 distinct user_id。
|
||||||
|
* 上面那条以 user_id 打头,按题号查只能把整棵索引 11 万行扫一遍再滤,12–18ms 而且
|
||||||
|
* 随提交数线性涨;这条以 problem_id 打头,快照实测 12ms → 3.6ms,「最多人做对」排序
|
||||||
|
* 的全表聚合 85ms → 16ms。
|
||||||
|
*
|
||||||
|
* problem_user_idx 也以 problem_id 打头,但它不是部分索引、也不带 result,规划器
|
||||||
|
* 不选它(还得回表看 contest_id 和 result)。
|
||||||
|
*/
|
||||||
|
index("submission_public_problem_user_idx")
|
||||||
|
.using(
|
||||||
|
"btree",
|
||||||
|
table.problemId.asc().nullsLast(),
|
||||||
|
table.userId.asc().nullsLast(),
|
||||||
|
table.result.asc().nullsLast(),
|
||||||
|
)
|
||||||
|
.where(sql`${table.contestId} is null`),
|
||||||
foreignKey({
|
foreignKey({
|
||||||
columns: [table.contestId],
|
columns: [table.contestId],
|
||||||
foreignColumns: [contest.id],
|
foreignColumns: [contest.id],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import type { ProblemTypeFilter } from "@oj2/contract"
|
import type { ProblemTypeFilter } from "@oj2/contract"
|
||||||
import { useDark } from "@vueuse/core"
|
|
||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { PROBLEM_TYPE_LABEL } from "../utils/problemType"
|
import { PROBLEM_TYPE_LABEL } from "../utils/problemType"
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ import { PROBLEM_TYPE_LABEL } from "../utils/problemType"
|
|||||||
const props = defineProps<{ kind: ProblemTypeFilter }>()
|
const props = defineProps<{ kind: ProblemTypeFilter }>()
|
||||||
|
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
const isDark = useDark()
|
|
||||||
|
|
||||||
const ICON: Record<ProblemTypeFilter, string> = {
|
const ICON: Record<ProblemTypeFilter, string> = {
|
||||||
flowchart: "ph:pencil-simple-line-bold",
|
flowchart: "ph:pencil-simple-line-bold",
|
||||||
@@ -23,27 +21,19 @@ const ICON: Record<ProblemTypeFilter, string> = {
|
|||||||
reference: "ph:flow-arrow-bold",
|
reference: "ph:flow-arrow-bold",
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = computed(() => {
|
// 明暗两套色都交给 CSS(html.dark),这里只递主题色。**别在这里调 useDark()**:
|
||||||
const t = theme.value
|
// 每个实例都会往 <html> 上重写一遍 class、插一段禁用过渡的样式再强制整页重算样式,
|
||||||
if (props.kind === "flowchart") {
|
// 一页题目十几个标签,切一次知识点就白白重算十几遍(实测占了切换耗时的一大半)
|
||||||
// 主题里没有紫色,两套手配的值,字都压到 4.5:1 以上
|
const vars = computed(() => ({
|
||||||
return isDark.value
|
"--tag-info": theme.value.infoColorPressed,
|
||||||
? { color: "#c4adf5", background: "rgba(160, 120, 240, 0.18)", borderColor: "transparent" }
|
"--tag-info-dark": theme.value.infoColor,
|
||||||
: { color: "#5b2fa6", background: "#efe8fb", borderColor: "transparent" }
|
"--tag-muted": theme.value.textColor3,
|
||||||
}
|
"--tag-border": theme.value.borderColor,
|
||||||
if (props.kind === "ast") {
|
}))
|
||||||
return {
|
|
||||||
color: isDark.value ? t.infoColor : t.infoColorPressed,
|
|
||||||
background: "transparent",
|
|
||||||
borderColor: isDark.value ? "rgba(112, 192, 232, 0.45)" : "#b9cdee",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { color: t.textColor3, background: "transparent", borderColor: t.borderColor }
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="type-tag" :class="kind" :style="style">
|
<span class="type-tag" :class="kind" :style="vars">
|
||||||
<Icon :icon="ICON[props.kind]" :width="12" />{{ PROBLEM_TYPE_LABEL[props.kind] }}
|
<Icon :icon="ICON[props.kind]" :width="12" />{{ PROBLEM_TYPE_LABEL[props.kind] }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -64,7 +54,31 @@ const style = computed(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主题里没有紫色,两套手配的值,字都压到 4.5:1 以上 */
|
||||||
.type-tag.flowchart {
|
.type-tag.flowchart {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #5b2fa6;
|
||||||
|
background: #efe8fb;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .type-tag.flowchart {
|
||||||
|
color: #c4adf5;
|
||||||
|
background: rgba(160, 120, 240, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tag.ast {
|
||||||
|
color: var(--tag-info);
|
||||||
|
border-color: #b9cdee;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .type-tag.ast {
|
||||||
|
color: var(--tag-info-dark);
|
||||||
|
border-color: rgba(112, 192, 232, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tag.reference {
|
||||||
|
color: var(--tag-muted);
|
||||||
|
border-color: var(--tag-border);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -110,7 +110,9 @@ async function listProblems() {
|
|||||||
total.value = res.total
|
total.value = res.total
|
||||||
problems.value = res.results
|
problems.value = res.results
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
|
// 左栏「全部题目」的题数:没筛的时候这一页的 total 就是;带着筛选进来的才单独数一次
|
||||||
if (!query.tag && !hasExtraFilter.value && !undoneOn.value) allCount.value = res.total
|
if (!query.tag && !hasExtraFilter.value && !undoneOn.value) allCount.value = res.total
|
||||||
|
else if (!allCount.value) allCount.value = (await getProblemList(0, 1, {})).total
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSide() {
|
async function loadSide() {
|
||||||
@@ -120,7 +122,6 @@ async function loadSide() {
|
|||||||
])
|
])
|
||||||
tags.value = tagRes
|
tags.value = tagRes
|
||||||
progress.value = progressRes
|
progress.value = progressRes
|
||||||
if (!allCount.value) allCount.value = (await getProblemList(0, 1, {})).total
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDebounced(() => query.keyword, listProblems, { debounce: 500, maxWait: 1000 })
|
watchDebounced(() => query.keyword, listProblems, { debounce: 500, maxWait: 1000 })
|
||||||
|
|||||||
Reference in new issue
Block a user