feat: 新增汇率管理页面,支持净资产历史趋势、货币转换,并引入交易相关组件和预算页面
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import type { Transaction } from '../../types';
|
||||
import { formatCurrency } from '../../utils/format';
|
||||
import { toLocalDateString } from '../../utils/dateUtils';
|
||||
|
||||
interface SpendingTrendChartProps {
|
||||
transactions: Transaction[];
|
||||
@@ -20,11 +21,11 @@ export const SpendingTrendChart: React.FC<SpendingTrendChartProps> = ({
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
const dateStr = toLocalDateString(d);
|
||||
const displayDate = `${d.getMonth() + 1}/${d.getDate()}`;
|
||||
|
||||
const dailyTotal = transactions
|
||||
.filter(t => t.type === 'expense' && t.transactionDate.startsWith(dateStr))
|
||||
.filter(t => t.type === 'expense' && toLocalDateString(t.transactionDate) === dateStr)
|
||||
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
|
||||
|
||||
data.push({
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useMemo } from 'react';
|
||||
import { Icon } from '@iconify/react';
|
||||
import type { Transaction } from '../../../types';
|
||||
import { formatCurrency } from '../../../utils/format';
|
||||
import { toLocalDateString } from '../../../utils/dateUtils';
|
||||
import './TransactionCalendar.css';
|
||||
|
||||
interface TransactionCalendarProps {
|
||||
@@ -83,7 +84,7 @@ export const TransactionCalendar: React.FC<TransactionCalendarProps> = ({
|
||||
|
||||
transactions.forEach(t => {
|
||||
// Handle timezone issues by taking date part only
|
||||
const dateStr = t.transactionDate.split('T')[0]; // Assuming ISO string
|
||||
const dateStr = toLocalDateString(t.transactionDate);
|
||||
if (!stats.has(dateStr)) {
|
||||
stats.set(dateStr, { income: 0, expense: 0, transactions: [] });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Category, Account, TransactionType } from '../../../types';
|
||||
import { getCategories } from '../../../services/categoryService';
|
||||
import { getAccounts } from '../../../services/accountService';
|
||||
import { getDisplayIcon } from '../../../utils/iconUtils';
|
||||
import { toLocalDateString } from '../../../utils/dateUtils';
|
||||
import './TransactionFilter.css';
|
||||
|
||||
export interface FilterValues {
|
||||
@@ -58,35 +59,37 @@ const DATE_PRESETS = [
|
||||
* Get date range based on preset
|
||||
*/
|
||||
function getDateRange(preset: 'today' | 'week' | 'month' | 'year'): {
|
||||
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
} {
|
||||
const now = new Date();
|
||||
const today = now.toISOString().split('T')[0];
|
||||
const today = toLocalDateString(now);
|
||||
|
||||
switch (preset) {
|
||||
case 'today':
|
||||
return { startDate: today, endDate: today };
|
||||
case 'week': {
|
||||
const dayOfWeek = now.getDay();
|
||||
const dayOfWeek = now.getDay() || 7; // Monday = 1, Sunday = 7
|
||||
const startOfWeek = new Date(now);
|
||||
startOfWeek.setDate(now.getDate() - dayOfWeek);
|
||||
// Adjust to Monday of current week
|
||||
startOfWeek.setDate(now.getDate() - dayOfWeek + 1);
|
||||
return {
|
||||
startDate: startOfWeek.toISOString().split('T')[0],
|
||||
startDate: toLocalDateString(startOfWeek),
|
||||
endDate: today,
|
||||
};
|
||||
}
|
||||
case 'month': {
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
return {
|
||||
startDate: startOfMonth.toISOString().split('T')[0],
|
||||
startDate: toLocalDateString(startOfMonth),
|
||||
endDate: today,
|
||||
};
|
||||
}
|
||||
case 'year': {
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 1);
|
||||
return {
|
||||
startDate: startOfYear.toISOString().split('T')[0],
|
||||
startDate: toLocalDateString(startOfYear),
|
||||
endDate: today,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,22 +43,30 @@ interface TransactionListProps {
|
||||
* Format date header for grouping
|
||||
*/
|
||||
function formatDateHeader(dateString: string): string {
|
||||
// dateString is YYYY-MM-DD (local) from grouping logic
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
const todayStr = `${year}-${month}-${day}`;
|
||||
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yYear = yesterday.getFullYear();
|
||||
const yMonth = String(yesterday.getMonth() + 1).padStart(2, '0');
|
||||
const yDay = String(yesterday.getDate()).padStart(2, '0');
|
||||
const yesterdayStr = `${yYear}-${yMonth}-${yDay}`;
|
||||
|
||||
const dateOnly = dateString.split('T')[0];
|
||||
const todayOnly = today.toISOString().split('T')[0];
|
||||
const yesterdayOnly = yesterday.toISOString().split('T')[0];
|
||||
|
||||
if (dateOnly === todayOnly) {
|
||||
if (dateString === todayStr) {
|
||||
return '今天';
|
||||
}
|
||||
if (dateOnly === yesterdayOnly) {
|
||||
if (dateString === yesterdayStr) {
|
||||
return '昨天';
|
||||
}
|
||||
|
||||
return formatDate(dateString, {
|
||||
// Construct a date object that represents this local date specifically for formatting
|
||||
// Using T00:00:00 to ensure it's treated as local time, not UTC
|
||||
return formatDate(dateString + 'T00:00:00', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
|
||||
@@ -33,6 +33,7 @@ import { createTransaction } from '../../services/transactionService';
|
||||
import { TransactionForm } from '../../components/transaction';
|
||||
import { getCategories } from '../../services/categoryService';
|
||||
import { getAccounts } from '../../services/accountService';
|
||||
import { toLocalDateString } from '../../utils/dateUtils';
|
||||
import { Icon } from '@iconify/react';
|
||||
import './Budget.css';
|
||||
|
||||
@@ -305,7 +306,7 @@ function Budget() {
|
||||
type: 'expense',
|
||||
categoryId: budget.categoryId || 0,
|
||||
accountId: budget.accountId || 0,
|
||||
transactionDate: new Date().toISOString().split('T')[0],
|
||||
transactionDate: toLocalDateString(new Date()),
|
||||
amount: 0
|
||||
});
|
||||
setShowTransactionForm(true);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../../services/exchangeRateService';
|
||||
import { getAccounts, calculateTotalBalance } from '../../services/accountService';
|
||||
import { getTransactions } from '../../services/transactionService';
|
||||
import { toLocalDateString } from '../../utils/dateUtils';
|
||||
import { ExchangeRateCard } from '../../components/exchangeRate/ExchangeRateCard';
|
||||
import NetWorthCard from '../../components/exchangeRate/NetWorthCard/NetWorthCard';
|
||||
import { SyncStatusBar } from '../../components/exchangeRate/SyncStatusBar';
|
||||
@@ -107,8 +108,8 @@ const ExchangeRates: React.FC = () => {
|
||||
startDate.setDate(startDate.getDate() - 30);
|
||||
|
||||
const { items: transactions } = await getTransactions({
|
||||
startDate: startDate.toISOString().split('T')[0],
|
||||
endDate: endDate.toISOString().split('T')[0],
|
||||
startDate: toLocalDateString(startDate),
|
||||
endDate: toLocalDateString(endDate),
|
||||
pageSize: 1000 // Ensure we get all relevant transactions
|
||||
});
|
||||
|
||||
@@ -122,7 +123,7 @@ const ExchangeRates: React.FC = () => {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
const dateStr = toLocalDateString(date); // YYYY-MM-DD
|
||||
|
||||
// Push current day's End-of-Day balance
|
||||
// Note: Array is being built backwards (Today ... 30 days ago)
|
||||
|
||||
@@ -178,7 +178,7 @@ export async function updateTransaction(
|
||||
if (data.transactionDate !== undefined) payload.transaction_date = data.transactionDate;
|
||||
if (data.note !== undefined) payload.note = data.note;
|
||||
if (data.tagIds !== undefined) payload.tag_ids = data.tagIds;
|
||||
|
||||
|
||||
const response = await api.put<ApiResponse<Transaction>>(`/transactions/${id}`, payload);
|
||||
if (!response.data) {
|
||||
throw new Error(response.error || 'Failed to update transaction');
|
||||
@@ -209,7 +209,14 @@ export function groupTransactionsByDate(transactions: Transaction[] | undefined)
|
||||
if (!dateValue) {
|
||||
continue; // Skip transactions without a date
|
||||
}
|
||||
const date = dateValue.split('T')[0];
|
||||
|
||||
// Fix: Use local time for grouping instead of UTC string split
|
||||
const d = new Date(dateValue);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const date = `${year}-${month}-${day}`;
|
||||
|
||||
const existing = groups.get(date) || [];
|
||||
existing.push(transaction);
|
||||
groups.set(date, existing);
|
||||
|
||||
80
src/utils/dateUtils.ts
Normal file
80
src/utils/dateUtils.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Date Utilities
|
||||
* Handles local date formatting and manipulation to avoid UTC timezone issues.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Formats a date object to 'YYYY-MM-DD' string using local time.
|
||||
* Replaces: date.toISOString().split('T')[0]
|
||||
*/
|
||||
export function toLocalDateString(date: Date | string | number): string {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two dates refer to the same day in local time.
|
||||
*/
|
||||
export function isSameDay(date1: Date | string, date2: Date | string): boolean {
|
||||
const d1 = new Date(date1);
|
||||
const d2 = new Date(date2);
|
||||
return (
|
||||
d1.getFullYear() === d2.getFullYear() &&
|
||||
d1.getMonth() === d2.getMonth() &&
|
||||
d1.getDate() === d2.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Date object set to the start of the day (00:00:00.000) in local time.
|
||||
*/
|
||||
export function getStartOfDay(date: Date = new Date()): Date {
|
||||
const d = new Date(date);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Date object set to the end of the day (23:59:59.999) in local time.
|
||||
*/
|
||||
export function getEndOfDay(date: Date = new Date()): Date {
|
||||
const d = new Date(date);
|
||||
d.setHours(23, 59, 59, 999);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates date range helpers
|
||||
*/
|
||||
export const DateRanges = {
|
||||
today: () => {
|
||||
const start = getStartOfDay();
|
||||
const end = getEndOfDay();
|
||||
return { start, end, startStr: toLocalDateString(start), endStr: toLocalDateString(end) };
|
||||
},
|
||||
yesterday: () => {
|
||||
const start = getStartOfDay();
|
||||
start.setDate(start.getDate() - 1);
|
||||
const end = getEndOfDay(start);
|
||||
return { start, end, startStr: toLocalDateString(start), endStr: toLocalDateString(end) };
|
||||
},
|
||||
thisWeek: () => {
|
||||
const now = new Date();
|
||||
const day = now.getDay() || 7; // Sunday is 0, make it 7 for ISO week (Mon-Sun)? Or standard week. Let's assume Mon start.
|
||||
const start = getStartOfDay();
|
||||
if (day !== 1) {
|
||||
start.setDate(now.getDate() - day + 1 + (day === 0 ? -7 : 0)); // Move to Monday
|
||||
}
|
||||
const end = getEndOfDay();
|
||||
return { start, end, startStr: toLocalDateString(start), endStr: toLocalDateString(end) };
|
||||
},
|
||||
thisMonth: () => {
|
||||
const now = new Date();
|
||||
const start = getStartOfDay(new Date(now.getFullYear(), now.getMonth(), 1));
|
||||
const end = getEndOfDay(new Date(now.getFullYear(), now.getMonth() + 1, 0));
|
||||
return { start, end, startStr: toLocalDateString(start), endStr: toLocalDateString(end) };
|
||||
}
|
||||
};
|
||||
@@ -3,4 +3,4 @@
|
||||
*/
|
||||
|
||||
export { formatCurrency, formatDate, formatRelativeTime, formatPercentage } from './format';
|
||||
|
||||
export * from './dateUtils';
|
||||
|
||||
Reference in New Issue
Block a user