refactor(时段): 两份手抄的选项列表与五份 "weeks:1" 解析收成一处
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
时段选项的 value 是 `<date-fns 单位>:<数量>`,把它解成 Duration 的那段 `split(":")`
在**五个组件里各写了一遍**(提交统计、流程图统计、AI 分析页、榜单页、班级对比页),
每份的兜底还都不一样;选项列表也抄了好几份:
- 两个统计面板逐字相同地拼「10/20/30 分钟 + DURATION_OPTIONS + 全部时段」;
- rank/list.vue 和 class/pk.vue 各手写了同样的五条长时段,pk.vue 里还留着一句
「与 rank/list.vue 保持一致」的注释 —— 靠注释同步的东西迟早不同步。
现在:`PANEL_DURATION_OPTIONS`(面板用,含分钟级和 all)、`LONG_DURATION_OPTIONS`
(榜单/班级对比用,从 DURATION_OPTIONS 派生)、`durationFromValue()`(唯一解析)。
各站点自己的兜底保留在原处,那部分本来就该各不相同。
## 行为零变化,逐条比对过
把改动前各处手写的列表原样取出来和新的比:面板 12 条、榜单 5 条、班级对比 6 条,
标签与取值逐条一致;11 个时段值的解析结果与旧的内联写法逐个相同。
唯一的差异在 `all`:旧写法产出 `{all: NaN}`,新写法回 null。**两边都到不了** ——
三处用到 subOptions 的地方全在 `query.duration === "all" ? … : …` 的 else 分支里,
三元短路,all 时根本不求值。逐处确认过。
## 实跑
- 榜单页:下拉 5 条顺序正确;切一周内,请求从 start=2026-08-10(months:1)
变成 start=2026-09-03(weeks:1),正好差 7 天;
- 班级对比页:下拉 6 条含「全部时间」;两个班 PK,全部时间和一个月内都正常出结果;
- 提交统计面板:默认 minutes:10,统计请求的 start 正好比 end 早 10 分钟。
另:ChartJS.register 那 15 处**没有动**。逐个列出来看,它们注册的是各自需要的那一套,
不是同一份重复(真正逐字相同的只有 4 个 Bar 图和 2 个 Line 图),而且组件各自声明依赖
正是 chart.js 该用的方式 —— 没用到的控制器不会进包,忘了注册会当场抛
`"bar" is not a registered controller`,是响的不是静默的。集中注册只会把懒加载的图表
代码推进首屏,为省 6 处重复不值得。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
This commit is contained in:
@@ -60,6 +60,7 @@ import SolvedTable from "./components/SolvedTable.vue"
|
||||
import { useAIStore } from "../store/ai"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { DURATION_OPTIONS } from "utils/constants"
|
||||
import { durationFromValue } from "utils/functions"
|
||||
|
||||
const aiStore = useAIStore()
|
||||
const userStore = useUserStore()
|
||||
@@ -72,11 +73,9 @@ const urlDuration = useRouteQuery<string>("duration", "months:6")
|
||||
aiStore.targetUsername = urlUsername.value
|
||||
aiStore.duration = urlDuration.value
|
||||
|
||||
const subOptions = computed<Duration>(() => {
|
||||
let dur = options.find((it) => it.value === aiStore.duration) ?? options[0]
|
||||
const x = dur.value!.toString().split(":")
|
||||
return { [x[0]]: parseInt(x[1]) } as Duration
|
||||
})
|
||||
const subOptions = computed<Duration>(
|
||||
() => durationFromValue(aiStore.duration) ?? durationFromValue(DURATION_OPTIONS[0].value)!,
|
||||
)
|
||||
|
||||
const start = computed(() => formatISO(sub(new Date(), subOptions.value)))
|
||||
const end = computed(() => formatISO(new Date()))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { ClassComparison } from "utils/types"
|
||||
import { LONG_DURATION_OPTIONS } from "utils/constants"
|
||||
import { durationFromValue } from "utils/functions"
|
||||
import { h } from "vue"
|
||||
import { formatISO, sub, type Duration } from "date-fns"
|
||||
import { getClassPK } from "oj/api"
|
||||
@@ -57,30 +59,14 @@ const aiContent = ref("")
|
||||
const showAIModal = ref(false)
|
||||
let aiController: AbortController | null = null
|
||||
|
||||
// 时间段选项(与 rank/list.vue 保持一致)
|
||||
// 长时段和榜单页同一份(LONG_DURATION_OPTIONS),外加一个「全部时间」
|
||||
const timeRangeOptions: SelectOption[] = [
|
||||
{ label: "全部时间", value: "" },
|
||||
{ label: "一周内", value: "weeks:1" },
|
||||
{ label: "一个月内", value: "months:1" },
|
||||
{ label: "两个月内", value: "months:2" },
|
||||
{ label: "半年内", value: "months:6" },
|
||||
{ label: "一年内", value: "years:1" },
|
||||
...LONG_DURATION_OPTIONS,
|
||||
]
|
||||
|
||||
// 计算时间段(与 rank/list.vue 保持一致)
|
||||
const subOptions = computed<Duration | null>(() => {
|
||||
if (!duration.value || duration.value === "") {
|
||||
return null
|
||||
}
|
||||
const dur = timeRangeOptions.find((it) => it.value === duration.value)
|
||||
if (!dur || !dur.value || dur.value === "") {
|
||||
return null
|
||||
}
|
||||
const x = dur.value.toString().split(":")
|
||||
const unit = x[0]
|
||||
const n = x[1]
|
||||
return { [unit]: parseInt(n) } as Duration
|
||||
})
|
||||
// 「全部时间」的 value 是空串,解不出来就是 null —— 正是不带时间条件的意思
|
||||
const subOptions = computed<Duration | null>(() => durationFromValue(duration.value))
|
||||
|
||||
// 根据时间段选项计算开始和结束时间
|
||||
function getTimeRange(): {
|
||||
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
getClassPK,
|
||||
} from "oj/api"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { getACRate } from "utils/functions"
|
||||
import { durationFromValue, getACRate } from "utils/functions"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { ChartType } from "utils/constants"
|
||||
import { ChartType, LONG_DURATION_OPTIONS } from "utils/constants"
|
||||
import { renderTableTitle } from "utils/renders"
|
||||
import Chart from "./components/Chart.vue"
|
||||
import Index from "./components/Index.vue"
|
||||
@@ -282,21 +282,12 @@ async function listActivity() {
|
||||
}))
|
||||
}
|
||||
|
||||
const options: SelectOption[] = [
|
||||
{ label: "一周内", value: "weeks:1" },
|
||||
{ label: "一个月内", value: "months:1" },
|
||||
{ label: "两个月内", value: "months:2" },
|
||||
{ label: "半年内", value: "months:6" },
|
||||
{ label: "一年内", value: "years:1" },
|
||||
]
|
||||
const options: SelectOption[] = [...LONG_DURATION_OPTIONS]
|
||||
|
||||
const subOptions = computed<Duration>(() => {
|
||||
let dur = options.find((it) => it.value === duration.value) ?? options[1]
|
||||
const x = dur.value!.toString().split(":")
|
||||
const unit = x[0]
|
||||
const n = x[1]
|
||||
return { [unit]: parseInt(n) }
|
||||
})
|
||||
// 认不出来退回 options[1](一个月内),和 duration 的初值一致
|
||||
const subOptions = computed<Duration>(
|
||||
() => durationFromValue(duration.value) ?? durationFromValue(LONG_DURATION_OPTIONS[1]!.value)!,
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
// 「全服 Top10」就是榜单第一页的前 10 条:挂载时 init() 取的正是 offset=0&limit=10,
|
||||
|
||||
@@ -158,7 +158,8 @@
|
||||
import { formatISO, sub, type Duration } from "date-fns"
|
||||
import type { FlowchartStatistics } from "@oj2/contract"
|
||||
import { getFlowchartStatistics } from "oj/api"
|
||||
import { DURATION_OPTIONS, FLOWCHART_CRITERIA_ORDER } from "utils/constants"
|
||||
import { PANEL_DURATION_OPTIONS, FLOWCHART_CRITERIA_ORDER } from "utils/constants"
|
||||
import { durationFromValue } from "utils/functions"
|
||||
import { useHiddenStudents } from "../composables/hiddenStudents"
|
||||
import { Doughnut, Radar, Bar } from "vue-chartjs"
|
||||
import {
|
||||
@@ -202,13 +203,7 @@ const props = defineProps<Props>()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const durationOptions: SelectOption[] = [
|
||||
{ label: "10分钟内", value: "minutes:10" },
|
||||
{ label: "20分钟内", value: "minutes:20" },
|
||||
{ label: "30分钟内", value: "minutes:30" },
|
||||
...DURATION_OPTIONS,
|
||||
{ label: "全部时段", value: "all" },
|
||||
]
|
||||
const durationOptions: SelectOption[] = [...PANEL_DURATION_OPTIONS]
|
||||
|
||||
const query = reactive({
|
||||
username: props.username,
|
||||
@@ -474,13 +469,10 @@ function renderWordCloud() {
|
||||
})
|
||||
}
|
||||
|
||||
const subOptions = computed<Duration>(() => {
|
||||
const dur =
|
||||
durationOptions.find((it) => it.value === query.duration) ??
|
||||
durationOptions[0]
|
||||
const x = dur.value!.toString().split(":")
|
||||
return { [x[0]]: parseInt(x[1]) }
|
||||
})
|
||||
const subOptions = computed<Duration>(
|
||||
() =>
|
||||
durationFromValue(query.duration) ?? durationFromValue(PANEL_DURATION_OPTIONS[0].value)!,
|
||||
)
|
||||
|
||||
async function handleStatistics() {
|
||||
const current = Date.now()
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
import { h } from "vue"
|
||||
import { formatISO, sub, type Duration } from "date-fns"
|
||||
import { getSubmissionStatistics, getSubmissionStatisticsItems } from "oj/api"
|
||||
import { DURATION_OPTIONS, STORAGE_KEY } from "utils/constants"
|
||||
import { PANEL_DURATION_OPTIONS, STORAGE_KEY } from "utils/constants"
|
||||
import storage from "utils/storage"
|
||||
import { useConfigStore } from "../store/config"
|
||||
import { useHiddenStudents } from "../composables/hiddenStudents"
|
||||
@@ -206,7 +206,7 @@ import { Doughnut } from "vue-chartjs"
|
||||
import { Chart as ChartJS, ArcElement, Title, Tooltip, Legend } from "chart.js"
|
||||
import { NFlex, NTag, NText, NTooltip, type DataTableRowKey } from "naive-ui"
|
||||
import { JUDGE_STATUS, SubmissionStatus } from "utils/constants"
|
||||
import { parseTime } from "utils/functions"
|
||||
import { durationFromValue, parseTime } from "utils/functions"
|
||||
import type {
|
||||
AttemptedStudent,
|
||||
SubmissionStatisticsItems,
|
||||
@@ -224,13 +224,7 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const options: SelectOption[] = [
|
||||
{ label: "10分钟内", value: "minutes:10" },
|
||||
{ label: "20分钟内", value: "minutes:20" },
|
||||
{ label: "30分钟内", value: "minutes:30" },
|
||||
...DURATION_OPTIONS,
|
||||
{ label: "全部时段", value: "all" },
|
||||
]
|
||||
const options: SelectOption[] = [...PANEL_DURATION_OPTIONS]
|
||||
|
||||
/**
|
||||
* 轨迹方块的颜色。取值就是 JUDGE_STATUS 里那个 type,色号沿用 Naive UI 的语义色
|
||||
@@ -695,13 +689,11 @@ const completionChartOptions = {
|
||||
},
|
||||
}
|
||||
|
||||
const subOptions = computed<Duration>(() => {
|
||||
let dur = options.find((it) => it.value === query.duration) ?? options[0]
|
||||
const x = dur.value!.toString().split(":")
|
||||
const unit = x[0]
|
||||
const n = x[1]
|
||||
return { [unit]: parseInt(n) }
|
||||
})
|
||||
const subOptions = computed<Duration>(
|
||||
// 认不出来(含 all)就退回列表第一档,和原来 `?? options[0]` 一致;
|
||||
// all 实际不会走到这里,handleStatistics 先分支掉了
|
||||
() => durationFromValue(query.duration) ?? durationFromValue(PANEL_DURATION_OPTIONS[0].value)!,
|
||||
)
|
||||
|
||||
function goSubmissions() {
|
||||
router.push({
|
||||
|
||||
@@ -322,6 +322,10 @@ export function sortFlowchartCriteria<T>(
|
||||
return Object.entries(details).sort(([a], [b]) => rank(a) - rank(b))
|
||||
}
|
||||
|
||||
/**
|
||||
* 时段选项。`value` 是 `<date-fns 的单位>:<数量>`,由 `durationFromValue()` 解析 ——
|
||||
* 别在组件里再手写一遍 `split(":")`,那套解析原来散在五个文件里。
|
||||
*/
|
||||
export const DURATION_OPTIONS = [
|
||||
{ label: "本节课内", value: "hours:1" },
|
||||
{ label: "两节课内", value: "hours:2" },
|
||||
@@ -333,6 +337,34 @@ export const DURATION_OPTIONS = [
|
||||
{ label: "一年内", value: "years:1" },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* 两个统计面板(提交统计、流程图统计)的时段下拉。
|
||||
*
|
||||
* 比通用的那份多了头尾:前面三档分钟级是给**上课当场**用的 —— 老师布置完一道题,
|
||||
* 想看的就是「刚才这十分钟谁交了」;末尾的 `all` 不是一个时长,`query.duration === "all"`
|
||||
* 会走各自的分支不带时间条件,所以它永远不会进 durationFromValue()。
|
||||
*
|
||||
* 原来这份列表在两个面板里逐字抄了两遍。
|
||||
*/
|
||||
export const PANEL_DURATION_OPTIONS = [
|
||||
{ label: "10分钟内", value: "minutes:10" },
|
||||
{ label: "20分钟内", value: "minutes:20" },
|
||||
{ label: "30分钟内", value: "minutes:30" },
|
||||
...DURATION_OPTIONS,
|
||||
{ label: "全部时段", value: "all" },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* 榜单和班级对比用的长时段,一周起步。
|
||||
*
|
||||
* 这两个页面看的是长期趋势,「本节课内」这种窗口在那儿没有意义 —— 全班一小时内的
|
||||
* AC 数拉出来比不出什么。原来 rank/list.vue 和 class/pk.vue 各手抄了一份同样的五条,
|
||||
* pk.vue 里还留着「与 rank/list.vue 保持一致」的注释,现在从上面那份派生。
|
||||
*/
|
||||
export const LONG_DURATION_OPTIONS = DURATION_OPTIONS.filter(
|
||||
(option) => !["hours:1", "hours:2", "days:1"].includes(option.value),
|
||||
)
|
||||
|
||||
// 班级号的位数范围。学生用户名形如 ks<班级号><姓名>,班级号还要跟
|
||||
// 网站配置里的班级列表对得上。
|
||||
// 后端 OnlineJudge/utils/shortcuts.py 的 CLASS_NAME_MIN/MAX_DIGITS
|
||||
|
||||
@@ -70,6 +70,24 @@ export function getTagColor(
|
||||
}
|
||||
|
||||
// 2023-04-03T02:43:28.673156Z
|
||||
/**
|
||||
* 把时段选项的 `value`(`"weeks:1"`、`"minutes:10"`)解成 date-fns 的 Duration。
|
||||
*
|
||||
* 认不出来返回 null —— 「全部时段」那个 `all` 走的就是这条,调用方本来就该在
|
||||
* `duration === "all"` 时不带时间条件。原来这段 `split(":")` 在五个组件里各写了一遍
|
||||
* (两个统计面板、AI 分析页、榜单页、班级对比页),每份的兜底还都不一样。
|
||||
*/
|
||||
export function durationFromValue(
|
||||
// 放宽到 SelectOption["value"] 那个形状:这些值直接来自 n-select 的绑定,
|
||||
// Naive 那边的类型是 string | number | undefined。数字解不出来,照样回 null
|
||||
value: string | number | null | undefined,
|
||||
): Duration | null {
|
||||
const [unit, amount] = String(value ?? "").split(":")
|
||||
const count = Number(amount)
|
||||
if (!unit || !Number.isFinite(count)) return null
|
||||
return { [unit]: count } as Duration
|
||||
}
|
||||
|
||||
export function parseTime(utc: Date | string, format = "YYYY年M月D日") {
|
||||
const time = useDateFormat(utc, format, { locales: "zh-CN" })
|
||||
return time.value
|
||||
|
||||
Reference in New Issue
Block a user