refactor(前端): 去掉 nanoid 和 date-fns;「12月」这种时长改说「1年」,年月按东八区日历数
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
时长拆分(比赛倒计时、比赛时长、主页「最近一次提交」)改用自己写的东八区日历差, 和 zonedParts 同一个口径,不再跟浏览器时区走。顺手删掉几次重设计后已经没人用的 getRandomId、durationFromValue 和 DURATION_OPTIONS / LONG_DURATION_OPTIONS。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d6c3171d13
commit
a2dcafae14
5 files changed
+54
-80
No files matched your search
@@ -31,12 +31,10 @@
|
||||
"chartjs-chart-wordcloud": "^4.4.5",
|
||||
"codemirror": "^6.0.2",
|
||||
"copy-text-to-clipboard": "^3.2.2",
|
||||
"date-fns": "^4.4.0",
|
||||
"fflate": "^0.8.3",
|
||||
"lib0": "0.2.119",
|
||||
"markdown-it": "^14.3.0",
|
||||
"naive-ui": "^2.45.3",
|
||||
"nanoid": "^6.0.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pele": "1.6.0",
|
||||
"pinia": "^4.0.3",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { errorCode } from "utils/api"
|
||||
import { formatISO, getTime, parseISO } from "date-fns"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { ContestStatus, ContestType } from "utils/constants"
|
||||
import { duration, secondsToDuration } from "utils/functions"
|
||||
@@ -28,8 +27,8 @@ export const useContestStore = defineStore("contest", () => {
|
||||
|
||||
const contestStatus = computed<ContestStatus>(() => {
|
||||
if (!contest.value) return ContestStatus.initial
|
||||
const start = getTime(parseISO(contest.value.startTime.toString()))
|
||||
const end = getTime(parseISO(contest.value.endTime.toString()))
|
||||
const start = new Date(contest.value.startTime).getTime()
|
||||
const end = new Date(contest.value.endTime).getTime()
|
||||
if (start > now.value) {
|
||||
return ContestStatus.not_started
|
||||
} else if (end < now.value) {
|
||||
@@ -43,10 +42,10 @@ export const useContestStore = defineStore("contest", () => {
|
||||
if (contestStatus.value === ContestStatus.finished) {
|
||||
return "已结束"
|
||||
} else if (contestStatus.value === ContestStatus.not_started) {
|
||||
const d = duration(formatISO(now.value), contest.value!.startTime, true)
|
||||
const d = duration(new Date(now.value), contest.value!.startTime, true)
|
||||
return "距离比赛开始 " + d
|
||||
} else {
|
||||
const d = duration(formatISO(now.value), contest.value!.endTime, true)
|
||||
const d = duration(new Date(now.value), contest.value!.endTime, true)
|
||||
return "距离比赛结束 " + d
|
||||
}
|
||||
})
|
||||
@@ -54,7 +53,7 @@ export const useContestStore = defineStore("contest", () => {
|
||||
/** 进行中还剩多久,「38:12」「1:05:00」这种;不在进行中为空串。题目页上下文条用 */
|
||||
const remaining = computed(() => {
|
||||
if (contestStatus.value !== ContestStatus.underway) return ""
|
||||
const end = getTime(parseISO(contest.value!.endTime.toString()))
|
||||
const end = new Date(contest.value!.endTime).getTime()
|
||||
const text = secondsToDuration(Math.max(0, Math.floor((end - now.value) / 1000)))
|
||||
return text.startsWith("0:") ? text.slice(2) : text
|
||||
})
|
||||
@@ -101,7 +100,7 @@ export const useContestStore = defineStore("contest", () => {
|
||||
const res = await getContest(contestID)
|
||||
contest.value = res
|
||||
// now 是学生侧比赛专有的服务器时间,用来对齐倒计时
|
||||
now.value = getTime(parseISO(res.now ?? res.createTime))
|
||||
now.value = new Date(res.now ?? res.createTime).getTime()
|
||||
// 先停掉上一轮的表。init() 会被调第二次:detail.vue 在「未开始 → 进行中」那一刻
|
||||
// 重新 init 一次(为了把开赛后才拿得到的题目捞回来),不清的话两个 setInterval
|
||||
// 一起给 now 加 1000,倒计时变两倍速 —— 学生赛前挂着页面就会中招,一场 60 分钟的
|
||||
|
||||
@@ -311,31 +311,6 @@ export function sortFlowchartCriteria<T>(details: Record<string, T>): [string, 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" },
|
||||
{ label: "一天内", value: "days:1" },
|
||||
{ label: "一周内", value: "weeks:1" },
|
||||
{ label: "一个月内", value: "months:1" },
|
||||
{ label: "两个月内", value: "months:2" },
|
||||
{ label: "半年内", value: "months:6" },
|
||||
{ label: "一年内", value: "years:1" },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* 班级对比用的长时段,一周起步。
|
||||
*
|
||||
* 看的是长期趋势,「本节课内」这种窗口在那儿没有意义 —— 全班一小时内的
|
||||
* AC 数拉出来比不出什么。从上面那份派生,不另抄一份。
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { TIME_ZONE_OFFSET_MINUTES, toAdminType } from "@oj2/contract"
|
||||
import type { JudgeCaseResult, JudgeInfo } from "@oj2/contract"
|
||||
import { getTime, intervalToDuration, parseISO, type Duration } from "date-fns"
|
||||
import { AdminUser, Submission } from "./types"
|
||||
import { JUDGE_STATUS, USER_TYPE } from "./constants"
|
||||
import { strFromU8, strToU8, unzlibSync, zipSync, zlibSync, type Zippable } from "fflate"
|
||||
import copyTextFallback from "copy-text-to-clipboard"
|
||||
import { normalizeDate } from "@vueuse/core"
|
||||
import { customAlphabet } from "nanoid"
|
||||
|
||||
function calculateACRate(acCount: number, totalCount: number): string {
|
||||
if (totalCount === 0) return "0.00"
|
||||
@@ -80,24 +78,6 @@ export function getTagColor(tag: "Low" | "Mid" | "High" | "简单" | "中等" |
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
const OFFSET_MS = TIME_ZONE_OFFSET_MINUTES * 60_000
|
||||
|
||||
const pad2 = (value: number) => String(value).padStart(2, "0")
|
||||
@@ -169,11 +149,52 @@ export function fromPickerValue(value: number) {
|
||||
return value - OFFSET_MS - new Date(value).getTimezoneOffset() * 60_000
|
||||
}
|
||||
|
||||
function getDurationObject(start: Date | string, end: Date | string) {
|
||||
return intervalToDuration({
|
||||
start: getTime(parseISO(start.toString())),
|
||||
end: getTime(parseISO(end.toString())),
|
||||
})
|
||||
interface Duration {
|
||||
years: number
|
||||
months: number
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
}
|
||||
|
||||
/** 往后挪几个月,日子超出那个月就落到月底(1 月 31 日 + 1 个月 = 2 月 28/29 日) */
|
||||
function addMonthsUTC(date: Date, months: number) {
|
||||
const day = date.getUTCDate()
|
||||
const moved = new Date(date)
|
||||
moved.setUTCDate(1)
|
||||
moved.setUTCMonth(moved.getUTCMonth() + months)
|
||||
const lastDay = new Date(
|
||||
Date.UTC(moved.getUTCFullYear(), moved.getUTCMonth() + 1, 0),
|
||||
).getUTCDate()
|
||||
moved.setUTCDate(Math.min(day, lastDay))
|
||||
return moved
|
||||
}
|
||||
|
||||
/**
|
||||
* 两个时刻之间隔了几年几月几天几小时……年和月按**东八区**的日历数,口径和 zonedParts
|
||||
* 一样(平移固定偏移再读 UTC 部件)。先数整月,剩下的不到一个月按毫秒拆 ——
|
||||
* 东八区没有夏令时,一天恒为 24 小时。倒过来(end 早于 start)或无效日期一律全 0。
|
||||
*/
|
||||
function getDurationObject(start: Date | string, end: Date | string): Duration {
|
||||
const from = normalizeDate(start).getTime()
|
||||
const to = normalizeDate(end).getTime()
|
||||
const zero = { years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0 }
|
||||
if (!(to > from)) return zero
|
||||
|
||||
const a = new Date(from + OFFSET_MS)
|
||||
const b = new Date(to + OFFSET_MS)
|
||||
let months = (b.getUTCFullYear() - a.getUTCFullYear()) * 12 + (b.getUTCMonth() - a.getUTCMonth())
|
||||
if (addMonthsUTC(a, months) > b) months--
|
||||
const seconds = Math.floor((b.getTime() - addMonthsUTC(a, months).getTime()) / 1000)
|
||||
return {
|
||||
years: Math.floor(months / 12),
|
||||
months: months % 12,
|
||||
days: Math.floor(seconds / 86400),
|
||||
hours: Math.floor(seconds / 3600) % 24,
|
||||
minutes: Math.floor(seconds / 60) % 60,
|
||||
seconds: seconds % 60,
|
||||
}
|
||||
}
|
||||
|
||||
function formatDurationUnits(
|
||||
@@ -243,7 +264,7 @@ export function durationToDays(start: Date | string, end: Date | string): string
|
||||
}
|
||||
|
||||
export function secondsToDuration(seconds: number): string {
|
||||
// 直接算,不走 date-fns 的 intervalToDuration:那个会拆出年、月,原来只把天和小时
|
||||
// 直接算,不走 getDurationObject:那个会拆出年、月,原来只把天和小时
|
||||
// 折回小时,超过一个月就把月丢了(剩 40 天 1 小时显示成 217:00:00)
|
||||
const total = Math.max(0, Math.floor(seconds))
|
||||
const pad = (n: number) => String(n).padStart(2, "0")
|
||||
@@ -383,8 +404,3 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getRandomId() {
|
||||
const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz")
|
||||
return nanoid()
|
||||
}
|
||||
@@ -60,12 +60,10 @@
|
||||
"chartjs-chart-wordcloud": "^4.4.5",
|
||||
"codemirror": "^6.0.2",
|
||||
"copy-text-to-clipboard": "^3.2.2",
|
||||
"date-fns": "^4.4.0",
|
||||
"fflate": "^0.8.3",
|
||||
"lib0": "0.2.119",
|
||||
"markdown-it": "^14.3.0",
|
||||
"naive-ui": "^2.45.3",
|
||||
"nanoid": "^6.0.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pele": "1.6.0",
|
||||
"pinia": "^4.0.3",
|
||||
@@ -1100,7 +1098,7 @@
|
||||
|
||||
"namespace-emitter": ["namespace-emitter@2.0.1", "https://registry.npmjs.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz", {}, "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g=="],
|
||||
|
||||
"nanoid": ["nanoid@6.0.2", "https://registry.npmjs.com/nanoid/-/nanoid-6.0.2.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-hIvVOb9Qr1p/9CNNLOe0OlWvgx/ziTsVwKN/GDSsuArN2K0wDiafP3s0SWmsm7o2BdnyS8Z9Z64NFBUbn6ziBg=="],
|
||||
"nanoid": ["nanoid@5.1.16", "https://registry.npmjs.com/nanoid/-/nanoid-5.1.16.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||
|
||||
"next-tick": ["next-tick@1.1.0", "https://registry.npmjs.com/next-tick/-/next-tick-1.1.0.tgz", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
|
||||
|
||||
@@ -1352,8 +1350,6 @@
|
||||
|
||||
"@oj2/api/typescript": ["typescript@7.0.2", "https://registry.npmjs.com/typescript/-/typescript-7.0.2.tgz", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
|
||||
|
||||
"@uppy/core/nanoid": ["nanoid@5.1.16", "https://registry.npmjs.com/nanoid/-/nanoid-5.1.16.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||
|
||||
"@vitest/browser/magic-string": ["magic-string@1.4.2", "https://registry.npmjs.com/magic-string/-/magic-string-1.4.2.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="],
|
||||
|
||||
"@vitest/mocker/magic-string": ["magic-string@1.4.2", "https://registry.npmjs.com/magic-string/-/magic-string-1.4.2.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="],
|
||||
@@ -1372,16 +1368,6 @@
|
||||
|
||||
"@vue/language-core/picomatch": ["picomatch@4.0.7", "https://registry.npmjs.com/picomatch/-/picomatch-4.0.7.tgz", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||
|
||||
"@wangeditor-next/basic-modules/nanoid": ["nanoid@5.1.16", "https://registry.npmjs.com/nanoid/-/nanoid-5.1.16.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||
|
||||
"@wangeditor-next/core/nanoid": ["nanoid@5.1.16", "https://registry.npmjs.com/nanoid/-/nanoid-5.1.16.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||
|
||||
"@wangeditor-next/editor/nanoid": ["nanoid@5.1.16", "https://registry.npmjs.com/nanoid/-/nanoid-5.1.16.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||
|
||||
"@wangeditor-next/table-module/nanoid": ["nanoid@5.1.16", "https://registry.npmjs.com/nanoid/-/nanoid-5.1.16.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||
|
||||
"@wangeditor-next/video-module/nanoid": ["nanoid@5.1.16", "https://registry.npmjs.com/nanoid/-/nanoid-5.1.16.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||
|
||||
"ast-walker-scope/@babel/parser": ["@babel/parser@8.0.4", "https://registry.npmjs.com/@babel/parser/-/parser-8.0.4.tgz", { "dependencies": { "@babel/types": "^8.0.4" }, "bin": "./bin/babel-parser.js" }, "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g=="],
|
||||
|
||||
"ast-walker-scope/@babel/types": ["@babel/types@8.0.4", "https://registry.npmjs.com/@babel/types/-/types-8.0.4.tgz", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.4" } }, "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g=="],
|
||||
|
||||
Reference in new issue
Block a user