feat: enhance draw status and scheduling display

- Updated DrawStatusHud to include a new countdown kind "start" for better status representation.
- Refactored HallDrawPanel to improve time display logic, differentiating between scheduled start and close times.
- Added new translations for scheduled start and close times in multiple languages.
- Enhanced time formatting functions to support new scheduling features and improve user experience.
This commit is contained in:
2026-05-25 18:01:26 +08:00
parent 3b83c6627c
commit 3c2664e02c
10 changed files with 205 additions and 31 deletions

View File

@@ -2,7 +2,13 @@
export function formatSecondsClock(total: number): string {
const s = Math.max(0, Math.floor(total));
const mm = String(Math.floor(s / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
if (h > 0) {
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
}
const mm = String(m).padStart(2, "0");
const ss = String(sec).padStart(2, "0");
return `${mm}:${ss}`;
}

View File

@@ -0,0 +1,5 @@
/**
* PRD服务端计划时刻按 UTCGMT存储与下发。
* 管理端展示/录入仍按此时区;玩家端界面将 API 时刻换算为浏览器本地时区显示。
*/
export const LOTTERY_SCHEDULE_TIMEZONE = "UTC";

View File

@@ -1,10 +1,74 @@
function pad2(n: number): string {
return String(n).padStart(2, "0");
function formatParts(date: Date, timeZone?: string): string {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
}).formatToParts(date);
const map = new Map(parts.map((part) => [part.type, part.value]));
const year = map.get("year") ?? "0000";
const month = map.get("month") ?? "00";
const day = map.get("day") ?? "00";
const hour = map.get("hour") ?? "00";
const minute = map.get("minute") ?? "00";
const second = map.get("second") ?? "00";
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
const NAIVE_SCHEDULE_CLOCK_RE =
/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/;
function parseScheduleClockToMs(
clock: string,
scheduleTimezone: string,
): number | null {
const trimmed = clock.trim();
if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(trimmed) || trimmed.includes("T")) {
const ms = Date.parse(trimmed);
return Number.isNaN(ms) ? null : ms;
}
const match = NAIVE_SCHEDULE_CLOCK_RE.exec(trimmed);
if (!match) {
const ms = Date.parse(trimmed);
return Number.isNaN(ms) ? null : ms;
}
const [, y, mo, d, h, mi, s] = match;
if (scheduleTimezone !== "UTC") {
const ms = Date.parse(`${y}-${mo}-${d}T${h}:${mi}:${s}`);
return Number.isNaN(ms) ? null : ms;
}
return Date.UTC(
Number(y),
Number(mo) - 1,
Number(d),
Number(h),
Number(mi),
Number(s),
);
}
/** 浏览器本地时区短标签(如 CST、GMT+8用于界面说明。 */
export function getBrowserTimeZoneLabel(date = new Date()): string {
try {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const parts = new Intl.DateTimeFormat(undefined, {
timeZone,
timeZoneName: "short",
}).formatToParts(date);
return parts.find((part) => part.type === "timeZoneName")?.value ?? timeZone;
} catch {
return "Local";
}
}
/**
* 将接口 ISO 时间串格式化为 **浏览器本地时区** 下的 `YYYY-MM-DD HH:mm:ss`
* 与后台 `lotteryadmin/src/lib/admin-datetime.ts` {@link formatAdminInstant} 行为一致。
* 将接口 ISO 时间串格式化为 **浏览器本地时区** 下的 `YYYY-MM-DD HH:mm:ss`
*/
export function formatLotteryInstant(iso: string | null | undefined): string {
if (iso == null || iso === "") {
@@ -14,12 +78,44 @@ export function formatLotteryInstant(iso: string | null | undefined): string {
if (Number.isNaN(ms)) {
return "—";
}
const date = new Date(ms);
const y = date.getFullYear();
const m = pad2(date.getMonth() + 1);
const d = pad2(date.getDate());
const h = pad2(date.getHours());
const min = pad2(date.getMinutes());
const s = pad2(date.getSeconds());
return `${y}-${m}-${d} ${h}:${min}:${s}`;
return formatParts(new Date(ms));
}
/**
* 将服务端计划时刻(无时区的 `YYYY-MM-DD HH:mm:ss`,按 schedule_timezone 解释,默认 UTC
* 格式化为浏览器本地时区的 `YYYY-MM-DD HH:mm:ss`。
*/
export function formatLotteryScheduleClock(
clock: string | null | undefined,
scheduleTimezone = "UTC",
): string {
if (clock == null || clock === "") {
return "—";
}
const ms = parseScheduleClockToMs(clock, scheduleTimezone);
if (ms == null) {
return "—";
}
return formatParts(new Date(ms));
}
/**
* 按指定 IANA 时区格式化(管理端等场景);玩家端展示请用 {@link formatLotteryInstant}。
*/
export function formatLotteryInstantInTimeZone(
iso: string | null | undefined,
timeZone: string,
): string {
if (iso == null || iso === "") {
return "—";
}
const ms = Date.parse(iso);
if (Number.isNaN(ms)) {
return "—";
}
try {
return formatParts(new Date(ms), timeZone);
} catch {
return formatParts(new Date(ms));
}
}