- server: Go 单二进制骨架,embed 内嵌前端,配置/迁移/HTTP 路由 - store: system.db + event.db 两套迁移,业务表 + 种子数据 - domain: 积分引擎(并列三口径/计分通道/流水/排名/破纪录)+ 68 个测试 - web: Vite + Vue 三入口骨架 + shared 层(类型/API/WebSocket 客户端) - console: Electron 宿主骨架(选屏/防熄屏/进程守护/托盘)
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import type { ResultType } from './types'
|
|
|
|
/**
|
|
* 成绩 / 时间的展示与录入互转。
|
|
*
|
|
* 库里同时存 result_text(给人看)和 result_value(给排序用),
|
|
* 两者必须在同一处换算,否则录入页和导出会出现「看起来一样、排出来不一样」。
|
|
*/
|
|
|
|
const DATE_PAD = (value: number): string => String(value).padStart(2, '0')
|
|
|
|
/** 秒 → `1:06.33`;不足一分钟用 `6.33`。 */
|
|
export function formatSeconds(seconds: number, fractionDigits = 2): string {
|
|
if (!Number.isFinite(seconds)) return '—'
|
|
|
|
const sign = seconds < 0 ? '-' : ''
|
|
const abs = Math.abs(seconds)
|
|
let minutes = Math.floor(abs / 60)
|
|
let rest = (abs - minutes * 60).toFixed(fractionDigits)
|
|
|
|
// 59.999 会四舍五入成 "60.00",进位到分钟,避免出现 "1:60.00"。
|
|
if (Number(rest) >= 60) {
|
|
minutes += 1
|
|
rest = (0).toFixed(fractionDigits)
|
|
}
|
|
|
|
const body = minutes > 0 ? `${minutes}:${rest.padStart(3 + fractionDigits, '0')}` : rest
|
|
return `${sign}${body}`
|
|
}
|
|
|
|
/** `1:06.33` / `66.33` → 66.33;无法解析返回 null。 */
|
|
export function parseSeconds(text: string): number | null {
|
|
const trimmed = text.trim()
|
|
if (trimmed === '') return null
|
|
|
|
const parts = trimmed.split(':')
|
|
if (parts.length > 2) return null
|
|
|
|
const numbers: number[] = []
|
|
for (const part of parts) {
|
|
const value = Number(part)
|
|
if (!Number.isFinite(value)) return null
|
|
numbers.push(value)
|
|
}
|
|
|
|
if (numbers.length === 1) return numbers[0] ?? null
|
|
|
|
const minutes = numbers[0] ?? 0
|
|
const seconds = numbers[1] ?? 0
|
|
if (seconds < 0 || seconds >= 60) return null
|
|
return minutes * 60 + seconds
|
|
}
|
|
|
|
/** 按单位把比较值渲染成展示文本,如 `66.33` + `s` → `1:06.33`。 */
|
|
export function formatResultText(
|
|
value: number | null,
|
|
unit: string,
|
|
type: ResultType = 'none',
|
|
): string {
|
|
if (value === null || !Number.isFinite(value)) return '—'
|
|
if (type === 'time' || unit === 's') return formatSeconds(value)
|
|
if (unit === 'm') return `${value.toFixed(2)}米`
|
|
if (unit === 'pts') return `${formatPoints(value)}分`
|
|
if (unit === '次') return `${Math.round(value)}次`
|
|
return `${formatPoints(value)}${unit}`
|
|
}
|
|
|
|
/** 展示文本 → 比较值,与 formatResultText 互逆。 */
|
|
export function parseResultValue(text: string, unit: string): number | null {
|
|
const trimmed = text.trim()
|
|
if (trimmed === '') return null
|
|
if (unit === 's') return parseSeconds(trimmed)
|
|
|
|
const numeric = Number(trimmed.replace(/[^\d.+-]/g, ''))
|
|
return Number.isFinite(numeric) ? numeric : null
|
|
}
|
|
|
|
/** 积分去掉无意义的 `.0`,同时避免 `12.25` 被截成 `12.3`。 */
|
|
export function formatPoints(points: number): string {
|
|
if (!Number.isFinite(points)) return '—'
|
|
return Number.isInteger(points) ? String(points) : String(Number(points.toFixed(2)))
|
|
}
|
|
|
|
/** 名次升降:正数为上升。无变化返回空串,交给调用方决定是否占位。 */
|
|
export function formatRankChange(delta: number | null): string {
|
|
if (delta === null || delta === 0) return ''
|
|
return delta > 0 ? `↑${delta}` : `↓${Math.abs(delta)}`
|
|
}
|
|
|
|
/** ISO → `YYYY-MM-DD HH:mm`(本地时区)。 */
|
|
export function formatDateTime(iso: string | null): string {
|
|
const date = toDate(iso)
|
|
if (date === null) return '—'
|
|
return (
|
|
`${date.getFullYear()}-${DATE_PAD(date.getMonth() + 1)}-${DATE_PAD(date.getDate())}` +
|
|
` ${DATE_PAD(date.getHours())}:${DATE_PAD(date.getMinutes())}`
|
|
)
|
|
}
|
|
|
|
/** ISO → `HH:mm`,大屏顶栏时钟用。 */
|
|
export function formatClock(iso: string | null): string {
|
|
const date = toDate(iso)
|
|
if (date === null) return '--:--'
|
|
return `${DATE_PAD(date.getHours())}:${DATE_PAD(date.getMinutes())}`
|
|
}
|
|
|
|
function toDate(iso: string | null): Date | null {
|
|
if (iso === null || iso === '') return null
|
|
const date = new Date(iso)
|
|
return Number.isNaN(date.getTime()) ? null : date
|
|
}
|