fix(ui): 修复全局时区偏移问题与日期控件手动输入崩溃 Bug
This commit is contained in:
@@ -4,6 +4,7 @@ import { db } from '@/db';
|
||||
import { exchangeRates } from '@/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { nowInShanghai } from '@/libs/utils';
|
||||
|
||||
const updateExchangeRateSchema = z.object({
|
||||
from: z.string().min(1).max(10),
|
||||
@@ -33,7 +34,7 @@ export async function updateExchangeRate(
|
||||
target: [exchangeRates.fromCurrency, exchangeRates.toCurrency],
|
||||
set: {
|
||||
rate: validation.data.rate,
|
||||
updatedAt: new Date(),
|
||||
updatedAt: nowInShanghai(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { Plus } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { createTransaction } from '@/actions/transaction';
|
||||
import { formatDateForDatetimeLocal, parseDateTimeLocalToUTC_v2 } from '@/libs/utils';
|
||||
|
||||
const addTransactionSchema = z.object({
|
||||
assetId: z.string().uuid('请选择一个资产'),
|
||||
@@ -258,7 +259,7 @@ export function AddTransactionDialog({ assets }: AddTransactionDialogProps) {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="executedAt"
|
||||
render={({ field }) => (
|
||||
@@ -269,11 +270,41 @@ export function AddTransactionDialog({ assets }: AddTransactionDialogProps) {
|
||||
type="datetime-local"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(new Date(e.target.value));
|
||||
const value = e.target.value;
|
||||
if (!value || value.trim() === '') {
|
||||
field.onChange(null);
|
||||
return;
|
||||
}
|
||||
const [datePart, timePart] = value.split('T');
|
||||
if (!datePart) {
|
||||
field.onChange(null);
|
||||
return;
|
||||
}
|
||||
const [yearStr, monthStr, dayStr] = datePart.split('-');
|
||||
const year = parseInt(yearStr, 10);
|
||||
const month = parseInt(monthStr, 10) - 1;
|
||||
const day = parseInt(dayStr, 10);
|
||||
if (isNaN(year) || isNaN(month) || isNaN(day)) {
|
||||
field.onChange(null);
|
||||
return;
|
||||
}
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
if (timePart) {
|
||||
const timeParts = timePart.split(':');
|
||||
hours = parseInt(timeParts[0], 10) || 0;
|
||||
minutes = parseInt(timeParts[1], 10) || 0;
|
||||
}
|
||||
const parsedDate = parseDateTimeLocalToUTC_v2(year, month, day, hours, minutes);
|
||||
if (isNaN(parsedDate.getTime())) {
|
||||
field.onChange(null);
|
||||
return;
|
||||
}
|
||||
field.onChange(parsedDate);
|
||||
}}
|
||||
value={
|
||||
field.value
|
||||
? new Date(field.value).toISOString().slice(0, 16)
|
||||
? formatDateForDatetimeLocal(field.value)
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,79 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { toZonedTime, fromZonedTime } from "date-fns-tz"
|
||||
|
||||
const TIMEZONE = "Asia/Shanghai"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function nowInShanghai(): Date {
|
||||
const now = new Date()
|
||||
const utcStr = now.toLocaleString("en-US", { timeZone: "UTC" })
|
||||
const utcDate = new Date(utcStr)
|
||||
const shanghaiOffset = getTimezoneOffset("Asia/Shanghai")
|
||||
const utcOffset = 0
|
||||
return new Date(utcDate.getTime() + (shanghaiOffset - utcOffset))
|
||||
}
|
||||
|
||||
export function formatDateForDatetimeLocal(date: Date): string {
|
||||
const zoned = toZonedTime(date, TIMEZONE)
|
||||
const year = zoned.getFullYear()
|
||||
const month = String(zoned.getMonth() + 1).padStart(2, "0")
|
||||
const day = String(zoned.getDate()).padStart(2, "0")
|
||||
const hours = String(zoned.getHours()).padStart(2, "0")
|
||||
const minutes = String(zoned.getMinutes()).padStart(2, "0")
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||
}
|
||||
|
||||
function getTimezoneOffset(timezone: string): number {
|
||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: timezone,
|
||||
timeZoneName: "longOffset",
|
||||
})
|
||||
const parts = formatter.formatToParts(new Date())
|
||||
const tzPart = parts.find((p) => p.type === "timeZoneName")
|
||||
if (!tzPart) return 0
|
||||
const match = tzPart.value.match(/([+-]?\d{1,2}):?(\d{2})/)
|
||||
if (!match) return 0
|
||||
const hours = parseInt(match[1], 10)
|
||||
const minutes = parseInt(match[2], 10)
|
||||
return (hours * 60 + minutes) * 60 * 1000
|
||||
}
|
||||
|
||||
export function parseDateTimeLocalToUTC(value: string): Date | null {
|
||||
if (!value) return null
|
||||
const zoned = fromZonedTime(value, TIMEZONE)
|
||||
return zoned
|
||||
}
|
||||
|
||||
export function parseDateTimeLocalToUTC_v2(year: number, month: number, day: number, hours: number, minutes: number): Date {
|
||||
const shanghaiTime = new Date()
|
||||
shanghaiTime.setFullYear(year)
|
||||
shanghaiTime.setMonth(month)
|
||||
shanghaiTime.setDate(day)
|
||||
shanghaiTime.setHours(hours, minutes, 0, 0)
|
||||
|
||||
const shanghaiOffsetMs = getShanghaiOffsetMs(shanghaiTime)
|
||||
const utcMs = shanghaiTime.getTime() - shanghaiOffsetMs
|
||||
return new Date(utcMs)
|
||||
}
|
||||
|
||||
function getShanghaiOffsetMs(date: Date): number {
|
||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: "Asia/Shanghai",
|
||||
timeZoneName: "short",
|
||||
})
|
||||
const parts = formatter.formatToParts(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
|
||||
}
|
||||
Reference in New Issue
Block a user