refactor: 统一玩家端时间格式化方法并移除冗余本地时间工具
This commit is contained in:
@@ -15,7 +15,7 @@ import type { HallDrawLiveSnapshot } from "@/features/hall/use-hall-draw-live";
|
|||||||
import { formatSecondsClock } from "@/lib/format-gmt";
|
import { formatSecondsClock } from "@/lib/format-gmt";
|
||||||
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
|
import { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone";
|
||||||
import {
|
import {
|
||||||
formatLotteryInstant,
|
formatPlayerInstant,
|
||||||
formatLotteryScheduleClock,
|
formatLotteryScheduleClock,
|
||||||
getBrowserTimeZoneLabel,
|
getBrowserTimeZoneLabel,
|
||||||
} from "@/lib/player-datetime";
|
} from "@/lib/player-datetime";
|
||||||
@@ -34,7 +34,7 @@ function ScheduleAnchorTime({ payload }: { payload: DrawCurrentPayload }) {
|
|||||||
: isOpen
|
: isOpen
|
||||||
? "draw.scheduledEnd"
|
? "draw.scheduledEnd"
|
||||||
: "draw.scheduledClose";
|
: "draw.scheduledClose";
|
||||||
const formatted = source ? formatLotteryInstant(source) : null;
|
const formatted = source ? formatPlayerInstant(source) : null;
|
||||||
if (!formatted) {
|
if (!formatted) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
|
|
||||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
|
|
||||||
type OrderMetaLineProps = {
|
type OrderMetaLineProps = {
|
||||||
orderNo: string | null | undefined;
|
orderNo: string | null | undefined;
|
||||||
@@ -21,7 +21,7 @@ export function OrderMetaLine({ orderNo, placedAt, t, className }: OrderMetaLine
|
|||||||
<span>{t("orders.orderNoLabel")}</span>{" "}
|
<span>{t("orders.orderNoLabel")}</span>{" "}
|
||||||
<span className="font-mono font-semibold text-slate-600">{displayNo}</span>
|
<span className="font-mono font-semibold text-slate-600">{displayNo}</span>
|
||||||
<span aria-hidden> · </span>
|
<span aria-hidden> · </span>
|
||||||
<span>{formatLotteryInstant(placedAt ?? null)}</span>
|
<span>{formatPlayerInstant(placedAt ?? null)}</span>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-
|
|||||||
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
||||||
import { orderGroupPath } from "@/features/orders/group-ticket-items";
|
import { orderGroupPath } from "@/features/orders/group-ticket-items";
|
||||||
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
|
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
|
||||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
import { norm4d } from "@/lib/norm-4d";
|
import { norm4d } from "@/lib/norm-4d";
|
||||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
@@ -274,7 +274,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
|||||||
<div className="flex items-baseline justify-between gap-3">
|
<div className="flex items-baseline justify-between gap-3">
|
||||||
<span className="shrink-0 text-slate-500">{t("orders.placedAt")}</span>
|
<span className="shrink-0 text-slate-500">{t("orders.placedAt")}</span>
|
||||||
<span className="text-right font-medium text-slate-800">
|
<span className="text-right font-medium text-slate-800">
|
||||||
{formatLotteryInstant(data.placed_at ?? null)}
|
{formatPlayerInstant(data.placed_at ?? null)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-baseline justify-between gap-3">
|
<div className="flex items-baseline justify-between gap-3">
|
||||||
@@ -412,7 +412,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
|||||||
<p className="mt-0.5 font-mono text-[11px] text-slate-500">{row.code}</p>
|
<p className="mt-0.5 font-mono text-[11px] text-slate-500">{row.code}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="shrink-0 font-mono text-[11px] text-slate-500">
|
<p className="shrink-0 font-mono text-[11px] text-slate-500">
|
||||||
{formatLotteryInstant(row.time)}
|
{formatPlayerInstant(row.time)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -422,7 +422,7 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
|||||||
|
|
||||||
{data.settled_at ? (
|
{data.settled_at ? (
|
||||||
<p className="text-[11px] text-slate-500">
|
<p className="text-[11px] text-slate-500">
|
||||||
{t("orders.settledAt", { time: formatLotteryInstant(data.settled_at) })}
|
{t("orders.settledAt", { time: formatPlayerInstant(data.settled_at) })}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||||
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
|
import { usePendingWalletReconcile } from "@/hooks/use-pending-wallet-reconcile";
|
||||||
import { formatLocalDateTime } from "@/lib/format-local-datetime";
|
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
import {
|
import {
|
||||||
pendingReconcileDescriptionKey,
|
pendingReconcileDescriptionKey,
|
||||||
@@ -74,7 +74,7 @@ export function NotificationsScreen() {
|
|||||||
{t(pendingReconcileTitleKey(item.type))}
|
{t(pendingReconcileTitleKey(item.type))}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-0.5 text-xs text-slate-500">
|
<p className="mt-0.5 text-xs text-slate-500">
|
||||||
{formatLocalDateTime(item.created_at)}
|
{formatPlayerInstant(item.created_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||||
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
import { playLabel } from "@/lib/play-labels";
|
import { playLabel } from "@/lib/play-labels";
|
||||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
import type { DrawResultListItem } from "@/types/api/draw-results";
|
import type { DrawResultListItem } from "@/types/api/draw-results";
|
||||||
@@ -276,7 +276,7 @@ function WinningResultDialog({
|
|||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<p className="text-xs">{t("results.drawTime", { time: "" }).replace(":", "").trim()}</p>
|
<p className="text-xs">{t("results.drawTime", { time: "" }).replace(":", "").trim()}</p>
|
||||||
<p className="mt-1 font-semibold text-slate-900">
|
<p className="mt-1 font-semibold text-slate-900">
|
||||||
{formatLotteryInstant(data.draw.draw_time_iso ?? data.draw.draw_time ?? null)}
|
{formatPlayerInstant(data.draw.draw_time_iso ?? data.draw.draw_time ?? null)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip";
|
|||||||
import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-grid";
|
import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-grid";
|
||||||
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
||||||
import { getPlayerBearerTokenPayload } from "@/lib/lottery-auth";
|
import { getPlayerBearerTokenPayload } from "@/lib/lottery-auth";
|
||||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
import { norm4d } from "@/lib/norm-4d";
|
import { norm4d } from "@/lib/norm-4d";
|
||||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
@@ -182,7 +182,7 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="mt-1 font-mono text-xs text-slate-500">
|
<CardDescription className="mt-1 font-mono text-xs text-slate-500">
|
||||||
{t("results.drawTime", {
|
{t("results.drawTime", {
|
||||||
time: formatLotteryInstant(data.draw_time_iso ?? data.draw_time ?? null),
|
time: formatPlayerInstant(data.draw_time_iso ?? data.draw_time ?? null),
|
||||||
})}
|
})}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { PlayerPanel } from "@/components/layout/player-panel";
|
|||||||
import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip";
|
import { JackpotResultsStrip } from "@/features/results/jackpot-results-strip";
|
||||||
import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-grid";
|
import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-grid";
|
||||||
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
||||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||||
import type { DrawResultListItem } from "@/types/api/draw-results";
|
import type { DrawResultListItem } from "@/types/api/draw-results";
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ export function DrawResultsListScreen() {
|
|||||||
</p>
|
</p>
|
||||||
<p className="mt-1 font-mono text-xs text-slate-500">
|
<p className="mt-1 font-mono text-xs text-slate-500">
|
||||||
{t("results.drawTime", {
|
{t("results.drawTime", {
|
||||||
time: formatLotteryInstant(
|
time: formatPlayerInstant(
|
||||||
featured.draw_time_iso ?? featured.draw_time ?? null,
|
featured.draw_time_iso ?? featured.draw_time ?? null,
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
@@ -283,7 +283,7 @@ export function DrawResultsListScreen() {
|
|||||||
{row.draw_no}
|
{row.draw_no}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-[11px] text-slate-500">
|
<p className="mt-1 text-[11px] text-slate-500">
|
||||||
{formatLotteryInstant(row.draw_time_iso ?? row.draw_time ?? null)}
|
{formatPlayerInstant(row.draw_time_iso ?? row.draw_time ?? null)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 rounded-full bg-[#f2f6ff] px-2.5 py-1 text-xs font-bold text-[#0b56b7]">
|
<span className="shrink-0 rounded-full bg-[#f2f6ff] px-2.5 py-1 text-xs font-bold text-[#0b56b7]">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { formatLocalDateTime } from "@/lib/format-local-datetime";
|
import { formatPlayerInstant } from "@/lib/player-datetime";
|
||||||
import { formatMinorAsCurrency } from "@/lib/money";
|
import { formatMinorAsCurrency } from "@/lib/money";
|
||||||
import type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs";
|
import type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs";
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ export function LogRow({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 text-xs font-medium text-slate-500">
|
<p className="mt-1.5 text-xs font-medium text-slate-500">
|
||||||
{formatLocalDateTime(item.created_at)}
|
{formatPlayerInstant(item.created_at)}
|
||||||
</p>
|
</p>
|
||||||
{item.ref_id ? (
|
{item.ref_id ? (
|
||||||
<p className="mt-1 truncate font-mono text-[11px] text-slate-400">
|
<p className="mt-1 truncate font-mono text-[11px] text-slate-400">
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
function pad2(n: number): string {
|
|
||||||
return String(n).padStart(2, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将接口 ISO 8601 时间格式化为浏览器 **本地时区** 下的 `YYYY-MM-DD HH:mm:ss`。
|
|
||||||
*
|
|
||||||
* 避免对原始字符串仅 `slice(0,19)`(易把 UTC 刻度误当本地钟面)。
|
|
||||||
*/
|
|
||||||
export function formatLocalDateTime(iso: string | null | undefined): string {
|
|
||||||
if (iso == null || iso === "") {
|
|
||||||
return "—";
|
|
||||||
}
|
|
||||||
const ms = Date.parse(iso);
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
@@ -126,9 +126,12 @@ export function getBrowserTimeZoneLabel(date = new Date()): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将接口 ISO 时间串格式化为 **浏览器本地时区** 下的 `YYYY-MM-DD HH:mm:ss`。
|
* 将接口 ISO 8601 时间格式化为浏览器 **本地时区** 下的 `YYYY-MM-DD HH:mm:ss`。
|
||||||
|
*
|
||||||
|
* 玩家端订单、钱包、通知、开奖结果等“已发生事件时间”统一走这里。
|
||||||
|
* 避免对原始字符串仅 `slice(0,19)`(易把 UTC 刻度误当本地钟面)。
|
||||||
*/
|
*/
|
||||||
export function formatLotteryInstant(iso: string | null | undefined): string {
|
export function formatPlayerInstant(iso: string | null | undefined): string {
|
||||||
if (iso == null || iso === "") {
|
if (iso == null || iso === "") {
|
||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
@@ -139,6 +142,9 @@ export function formatLotteryInstant(iso: string | null | undefined): string {
|
|||||||
return formatParts(new Date(ms));
|
return formatParts(new Date(ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated 玩家端新代码请使用 {@link formatPlayerInstant}。 */
|
||||||
|
export const formatLotteryInstant = formatPlayerInstant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将服务端计划时刻(无时区的 `YYYY-MM-DD HH:mm:ss`,按 schedule_timezone 解释,默认 UTC)
|
* 将服务端计划时刻(无时区的 `YYYY-MM-DD HH:mm:ss`,按 schedule_timezone 解释,默认 UTC)
|
||||||
* 格式化为浏览器本地时区的 `YYYY-MM-DD HH:mm:ss`。
|
* 格式化为浏览器本地时区的 `YYYY-MM-DD HH:mm:ss`。
|
||||||
@@ -158,7 +164,7 @@ export function formatLotteryScheduleClock(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按指定 IANA 时区格式化(管理端等场景);玩家端展示请用 {@link formatLotteryInstant}。
|
* 按指定 IANA 时区格式化(管理端等场景);玩家端展示请用 {@link formatPlayerInstant}。
|
||||||
*/
|
*/
|
||||||
export function formatLotteryInstantInTimeZone(
|
export function formatLotteryInstantInTimeZone(
|
||||||
iso: string | null | undefined,
|
iso: string | null | undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user