From 31ca101914fd8f0410b708518cdd60940d16fa1e Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Tue, 28 Apr 2026 17:51:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=BE=A9=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E6=8E=A7=E4=BB=B6=E6=99=82=E5=8D=80=E5=B1=95=E7=A4=BA=E5=81=8F?= =?UTF-8?q?=E7=A7=BB=20Bug=EF=BC=8C=E4=B8=A6=E4=BF=AE=E6=AD=A3=E8=B2=A0?= =?UTF-8?q?=E6=88=90=E6=9C=AC=E7=9A=84=E6=A0=BC=E5=BC=8F=E5=8C=96=E9=A1=AF?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 2 +- app/dashboard/page.tsx | 4 ++-- src/lib/formatters.ts | 6 +++++- src/libs/utils.ts | 48 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/Memory.md b/Memory.md index 012fa5c..c780040 100644 --- a/Memory.md +++ b/Memory.md @@ -35,7 +35,7 @@ - 重构 `` 并将其提升至 Dashboard 首页,实现总资产大盘的全局一键实盘刷新。 ## 修复记录 -- 修复了全局时区偏移问题,并解决了日期控件手动输入导致的崩溃 Bug。在 `src/libs/utils.ts` 中新增 `nowInShanghai()`、`formatDateForDatetimeLocal()` 和 `parseDateTimeLocalToUTC_v2()` 函数,强制使用 `Asia/Shanghai` (UTC+8) 时区。所有 `new Date()` 调用(包括 Server Actions)均已对齐至东八区。日期选择器增加字符串格式校验与解析逻辑,避免 `Invalid time value` 错误。 +- 解决了日期选择控件的时区偏移 Bug,确保全球通用:在 `src/libs/utils.ts` 中重写 `formatDateForDatetimeLocal()` 与 `parseDateTimeLocalToUTC_v2()` 函数,采用 `Intl.DateTimeFormat` 动态获取 `Asia/Shanghai` 时区偏移量,确保 UTC 时间到本地时间的双向转换精确无误,修复了用户选 10 点展示为 2 点的问题。修正了前端数据格式化逻辑,在 `src/lib/formatters.ts` 中增加空值/NaN 兜底处理,在 `src/app/dashboard/page.tsx` 中将平均成本与摊薄成本的显示条件从 `.gt(0)` 改为 `.ne(0)`,支持英特尔负成本等极端场景下的精确数字展示。 ## 资产分布图表按市场维度升级 (Task 32) - 优化资产分布图表,升级为按市场维度聚合展示,并增强了 Tooltip 的颜色指代与明细交互。 diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index fbe4db7..965bd60 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -71,8 +71,8 @@ export default async function DashboardPage() { const posPnlNative = new Big(pos.pnlNative); const posPnlNativePositive = posPnlNative.gte(0); - const avgCostFormatted = new Big(pos.avgCost).gt(0) ? formatAmount(pos.avgCost) : '-'; - const dilutedCostFormatted = new Big(pos.dilutedCost).gt(0) ? formatAmount(pos.dilutedCost) : '-'; + const avgCostFormatted = new Big(pos.avgCost).ne(0) && pos.avgCost !== '0' ? formatAmount(pos.avgCost) : '-'; + const dilutedCostFormatted = new Big(pos.dilutedCost).ne(0) && pos.dilutedCost !== '0' ? formatAmount(pos.dilutedCost) : '-'; return ( diff --git a/src/lib/formatters.ts b/src/lib/formatters.ts index e8b5965..40e4f0c 100644 --- a/src/lib/formatters.ts +++ b/src/lib/formatters.ts @@ -1,7 +1,11 @@ import Big from 'big.js'; export function formatAmount(value: string): string { - return new Big(value).toFixed(2); + if (value === null || value === undefined || value === '' || isNaN(Number(value))) { + return '-'; + } + const result = new Big(value).toFixed(2); + return result; } export function formatQuantity(value: string, assetType: string): string { diff --git a/src/libs/utils.ts b/src/libs/utils.ts index 0d521a9..df629e0 100644 --- a/src/libs/utils.ts +++ b/src/libs/utils.ts @@ -1,6 +1,5 @@ import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" -import { toZonedTime, fromZonedTime } from "date-fns-tz" const TIMEZONE = "Asia/Shanghai" @@ -8,6 +7,53 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } +function getShanghaiOffsetMs(): number { + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: TIMEZONE, + timeZoneName: "short", + }) + const parts = formatter.formatToParts(new Date()) + const tzPart = parts.find((p) => p.type === "timeZoneName") + if (!tzPart) return 8 * 60 * 60 * 1000 + + const match = tzPart.value.match(/^([+-])(\d{2}):?(\d{2})$/) + if (!match) return 8 * 60 * 60 * 1000 + + const sign = match[1] === "+" ? 1 : -1 + const offsetHours = parseInt(match[2], 10) + const offsetMinutes = parseInt(match[3], 10) + return sign * (offsetHours * 60 + offsetMinutes) * 60 * 1000 +} + +export function formatDateForDatetimeLocal(date: Date): string { + const shanghaiOffsetMs = getShanghaiOffsetMs() + const localTimeMs = date.getTime() + shanghaiOffsetMs + const localDate = new Date(localTimeMs) + + const year = localDate.getUTCFullYear() + const month = String(localDate.getUTCMonth() + 1).padStart(2, "0") + const day = String(localDate.getUTCDate()).padStart(2, "0") + const hours = String(localDate.getUTCHours()).padStart(2, "0") + const minutes = String(localDate.getUTCMinutes()).padStart(2, "0") + return `${year}-${month}-${day}T${hours}:${minutes}` +} + +export function parseDateTimeLocalToUTC_v2(year: number, month: number, day: number, hours: number, minutes: number): Date { + const localTimeMs = Date.UTC(year, month, day, hours, minutes) + const shanghaiOffsetMs = getShanghaiOffsetMs() + const utcMs = localTimeMs - shanghaiOffsetMs + return new Date(utcMs) +} + +export function parseDateTimeLocalToUTC(value: string): Date | null { + if (!value) return null + const [datePart, timePart] = value.split("T") + if (!datePart) return null + const [y, m, d] = datePart.split("-").map(Number) + const [h = 0, min = 0] = timePart ? timePart.split(":").map(Number) : [0, 0] + return parseDateTimeLocalToUTC_v2(y, m - 1, d, h, min) +} + export function nowInShanghai(): Date { const now = new Date() const utcStr = now.toLocaleString("en-US", { timeZone: "UTC" })