87f97a2f9b
build / build (push) Successful in 1m7s
详情使用共享主表字典,稀疏修改所选字段并保留原始问答及旧复核审计。 月周统计提供文本和下拉双入口;每租户每周一份持久草稿,按技能六节双语模板真实预填。 加入幂等自动保存、冲突保留、未知响应重试、切周及离开保护和schema58非破坏降级保护。 验证:完整make verify、隔离PostgreSQL全mirror竞态回归、真实恢复库浏览器验收通过。 浏览器实测字段精确读回、月周双输入、模板预填、MD/CSV、自动保存、冲突、失败重试与API重启恢复。 未连接或改写DobotAI生产源库,未运行付费模型;提交时尚未部署。
75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
export interface QAWeek {
|
||
year: number
|
||
week: number
|
||
start: string
|
||
end: string
|
||
label: string
|
||
}
|
||
|
||
const day = 86_400_000
|
||
const dateOnly = (value: number) => new Date(value).toISOString().slice(0, 10)
|
||
|
||
export function qaWeekValue(value: Pick<QAWeek, 'year' | 'week'>): string {
|
||
return `${value.year}-W${String(value.week).padStart(2, '0')}`
|
||
}
|
||
export function currentQAWeekValue(now = new Date()): string {
|
||
return qaWeekValue(currentQAWeek(now))
|
||
}
|
||
|
||
export function currentQAWeek(now = new Date()): QAWeek {
|
||
const beijing = new Date(now.getTime() + 8 * 3_600_000)
|
||
beijing.setUTCHours(0, 0, 0, 0)
|
||
beijing.setUTCDate(beijing.getUTCDate() + 3 - ((beijing.getUTCDay() + 6) % 7))
|
||
const year = beijing.getUTCFullYear()
|
||
const week = Math.floor((beijing.getTime() - firstMonday(year)) / (7 * day)) + 1
|
||
return qaWeek(year, week)
|
||
}
|
||
|
||
export function qaDateTimeToISO(value: string): string {
|
||
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) throw new Error('时间格式无效')
|
||
const parsed = new Date(`${value}:00+08:00`)
|
||
if (
|
||
!Number.isFinite(parsed.getTime()) ||
|
||
new Date(parsed.getTime() + 8 * 3_600_000).toISOString().slice(0, 16) !== value
|
||
) {
|
||
throw new Error('时间格式无效')
|
||
}
|
||
return parsed.toISOString()
|
||
}
|
||
|
||
export function qaWeekQueryRange(year: number, week: number, now = new Date()): QAWeek {
|
||
const range = qaWeek(year, week)
|
||
if (
|
||
Date.parse(qaDateTimeToISO(range.start)) < now.getTime() &&
|
||
now.getTime() < Date.parse(qaDateTimeToISO(range.end))
|
||
) {
|
||
return { ...range, end: new Date(now.getTime() + 8 * 3_600_000).toISOString().slice(0, 16) }
|
||
}
|
||
return range
|
||
}
|
||
|
||
const firstMonday = (year: number) => {
|
||
const jan4 = new Date(Date.UTC(year, 0, 4))
|
||
return jan4.getTime() - ((jan4.getUTCDay() + 6) % 7) * day
|
||
}
|
||
|
||
export function qaWeeksInYear(year: number): number {
|
||
if (!Number.isInteger(year) || year < 1900 || year > 9998) throw new Error('年份无效')
|
||
return (firstMonday(year + 1) - firstMonday(year)) / (7 * day)
|
||
}
|
||
|
||
export function qaWeek(year: number, week: number): QAWeek {
|
||
if (!Number.isInteger(week) || week < 1 || week > qaWeeksInYear(year)) throw new Error('周次无效')
|
||
const start = firstMonday(year) + (week - 1) * 7 * day
|
||
const first = dateOnly(start)
|
||
const last = dateOnly(start + 6 * day)
|
||
const crossYear = first.slice(0, 4) !== String(year) || last.slice(0, 4) !== String(year)
|
||
return {
|
||
year,
|
||
week,
|
||
start: `${first}T00:00`,
|
||
end: `${dateOnly(start + 7 * day)}T00:00`,
|
||
label: `第${week}周(${crossYear ? first : first.slice(5)} 到 ${crossYear ? last : last.slice(5)})`,
|
||
}
|
||
}
|