From 5e587837e84f1212e4dfe043d1f8bb7849887f44 Mon Sep 17 00:00:00 2001
From: admin <1297598740@qq.com>
Date: Fri, 30 Jan 2026 15:40:59 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=A0=B8=E5=BF=83?=
=?UTF-8?q?=E5=BA=94=E7=94=A8=E7=BB=93=E6=9E=84=E3=80=81=E5=85=A8=E5=B1=80?=
=?UTF-8?q?=E5=91=BD=E4=BB=A4=E9=9D=A2=E6=9D=BF=E5=92=8C=E5=88=9D=E6=AD=A5?=
=?UTF-8?q?=E4=BA=A4=E6=98=93=E7=9B=B8=E5=85=B3=E9=A1=B5=E9=9D=A2=E5=8F=8A?=
=?UTF-8?q?=E6=9C=8D=E5=8A=A1=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/App.tsx | 5 +-
.../common/CommandPalette/CommandPalette.tsx | 5 +-
.../TransactionForm/TransactionForm.tsx | 11 +++
src/contexts/SettingsContext.tsx | 68 +++++++++++++++++++
src/pages/Home/Home.tsx | 34 ++++++----
src/pages/Transactions/Transactions.tsx | 11 ++-
src/services/transactionService.ts | 27 ++------
src/types/index.ts | 18 +++++
walkthrough.md | 62 +++++++++++++++++
9 files changed, 201 insertions(+), 40 deletions(-)
create mode 100644 src/contexts/SettingsContext.tsx
create mode 100644 walkthrough.md
diff --git a/src/App.tsx b/src/App.tsx
index fcb80f4..81b2f33 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,6 +3,7 @@ import { router } from './router';
import { ThemeProvider, PrivacyProvider, NotificationProvider, GuideProvider, useAutoTokenRefresh } from './hooks';
+import { SettingsProvider } from './contexts/SettingsContext';
/**
* App Content Component
@@ -25,7 +26,9 @@ function App() {
-
+
+
+
diff --git a/src/components/common/CommandPalette/CommandPalette.tsx b/src/components/common/CommandPalette/CommandPalette.tsx
index 2e5bb98..800a447 100644
--- a/src/components/common/CommandPalette/CommandPalette.tsx
+++ b/src/components/common/CommandPalette/CommandPalette.tsx
@@ -6,6 +6,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '../../../hooks';
+import { useSettings } from '../../../contexts/SettingsContext';
import { Icon } from '@iconify/react';
import { useKey } from 'react-use';
import { getTransactions } from '../../../services/transactionService';
@@ -32,6 +33,8 @@ export const CommandPalette: React.FC = () => {
const navigate = useNavigate();
const { toggleTheme, isDark } = useTheme();
+ const { settings } = useSettings();
+ const currentLedgerId = settings?.currentLedgerId;
// Toggle open with Ctrl+K or Cmd+K
useEffect(() => {
@@ -129,7 +132,7 @@ export const CommandPalette: React.FC = () => {
const timer = setTimeout(async () => {
try {
- const res = await getTransactions({ search: query, pageSize: 5 });
+ const res = await getTransactions({ search: query, pageSize: 5, ledgerId: currentLedgerId });
setTransactionResults(res.items);
} catch (err) {
console.error('Search transactions failed', err);
diff --git a/src/components/transaction/TransactionForm/TransactionForm.tsx b/src/components/transaction/TransactionForm/TransactionForm.tsx
index c09e3fe..cecf973 100644
--- a/src/components/transaction/TransactionForm/TransactionForm.tsx
+++ b/src/components/transaction/TransactionForm/TransactionForm.tsx
@@ -56,6 +56,8 @@ interface TransactionFormProps {
isEditing?: boolean;
/** Whether to automatically skip steps if data is pre-filled */
smartSkipConfirmedSteps?: boolean;
+ /** Current active ledger ID */
+ currentLedgerId?: number;
}
export const TransactionForm: React.FC = ({
@@ -65,6 +67,7 @@ export const TransactionForm: React.FC = ({
loading = false,
isEditing = false,
smartSkipConfirmedSteps = false,
+ currentLedgerId,
}) => {
// Current step (1, 2, or 3)
const [currentStep, setCurrentStep] = useState(1);
@@ -89,6 +92,7 @@ export const TransactionForm: React.FC = ({
: toLocalISOString(new Date()),
note: '',
tagIds: [],
+ ledgerId: currentLedgerId,
...initialData,
// Override transactionDate again after spreading initialData
...(initialData?.transactionDate
@@ -96,6 +100,13 @@ export const TransactionForm: React.FC = ({
: {}),
});
+ // Update ledgerId when currentLedgerId changes (only if not editing or explicit override needed)
+ useEffect(() => {
+ if (currentLedgerId && !isEditing) {
+ setFormData(prev => ({ ...prev, ledgerId: currentLedgerId }));
+ }
+ }, [currentLedgerId, isEditing]);
+
// Data State
const [accounts, setAccounts] = useState([]);
const [categories, setCategories] = useState([]);
diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx
new file mode 100644
index 0000000..d3f37ce
--- /dev/null
+++ b/src/contexts/SettingsContext.tsx
@@ -0,0 +1,68 @@
+import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
+import type { UserSettings } from '../types';
+import { getSettings, updateSettings as apiUpdateSettings } from '../services/settingsService';
+
+interface SettingsContextType {
+ settings: UserSettings | null;
+ loading: boolean;
+ error: string | null;
+ refreshSettings: () => Promise;
+ updateSettings: (data: Partial) => Promise;
+}
+
+const SettingsContext = createContext(undefined);
+
+export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [settings, setSettings] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchSettings = useCallback(async () => {
+ try {
+ const data = await getSettings();
+ setSettings(data);
+ setError(null);
+ } catch (err) {
+ console.error('Failed to load settings:', err);
+ setError('Failed to load settings');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchSettings();
+ }, [fetchSettings]);
+
+ const updateSettings = useCallback(async (data: Partial) => {
+ try {
+ const updated = await apiUpdateSettings(data);
+ setSettings(updated);
+ } catch (err) {
+ console.error('Failed to update settings:', err);
+ throw err;
+ }
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useSettings = (): SettingsContextType => {
+ const context = useContext(SettingsContext);
+ if (context === undefined) {
+ throw new Error('useSettings must be used within a SettingsProvider');
+ }
+ return context;
+};
diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx
index d63d3e8..7abe508 100644
--- a/src/pages/Home/Home.tsx
+++ b/src/pages/Home/Home.tsx
@@ -7,7 +7,8 @@ import { getTransactions, calculateTotalExpense } from '../../services/transacti
import { getCategories } from '../../services/categoryService';
import { getLedgers, reorderLedgers } from '../../services/ledgerService';
import { getStreakInfo, type StreakInfoFormatted } from '../../services/streakService';
-import { getSettings, updateSettings } from '../../services/settingsService';
+import { updateSettings } from '../../services/settingsService';
+import { useSettings } from '../../contexts/SettingsContext';
import { Icon } from '@iconify/react';
import { SpendingTrendChart } from '../../components/charts/SpendingTrendChart';
import { Skeleton } from '../../components/common/Skeleton/Skeleton';
@@ -24,7 +25,7 @@ import { HealthScoreCard } from '../../components/home/HealthScoreCard/HealthSco
import { QuickActionsBox } from '../../components/home/QuickActionsBox/QuickActionsBox';
import { getBudgets } from '../../services/budgetService';
import { toLocalDateString, isSameDay } from '../../utils/dateUtils';
-import type { Account, Transaction, Category, Ledger, UserSettings } from '../../types';
+import type { Account, Transaction, Category, Ledger } from '../../types';
/**
@@ -36,7 +37,10 @@ function Home() {
const [recentTransactions, setRecentTransactions] = useState([]);
const [categories, setCategories] = useState([]);
const [ledgers, setLedgers] = useState([]);
- const [settings, setSettings] = useState(null);
+
+ const { settings, loading: settingsLoading } = useSettings();
+ const currentLedgerId = settings?.currentLedgerId || 0;
+
const [ledgerSelectorOpen, setLedgerSelectorOpen] = useState(false);
const [voiceModalOpen, setVoiceModalOpen] = useState(false);
const [showAccountForm, setShowAccountForm] = useState(false);
@@ -59,8 +63,9 @@ function Home() {
useEffect(() => {
+ if (settingsLoading) return;
loadData();
- }, []);
+ }, [currentLedgerId, settingsLoading]);
const loadData = async () => {
try {
@@ -82,22 +87,20 @@ function Home() {
const rangeStartStr = toLocalDateString(rangeStart);
const rangeEndStr = toLocalDateString(rangeEnd);
- const [accountsData, recentTxData, categoriesData, ledgersData, settingsData, budgetsData, streakData, rangeTxData] = await Promise.all([
+ const [accountsData, recentTxData, categoriesData, ledgersData, budgetsData, streakData, rangeTxData] = await Promise.all([
getAccounts(),
- getTransactions({ page: 1, pageSize: 5 }), // Recent list (server sort)
+ getTransactions({ page: 1, pageSize: 5, ledgerId: currentLedgerId }), // Recent list (server sort)
getCategories(),
getLedgers().catch(() => []),
- getSettings().catch(() => null),
getBudgets().catch(() => []),
getStreakInfo().catch(() => null),
- getTransactions({ startDate: rangeStartStr, endDate: rangeEndStr, pageSize: 1000 }), // Bulk fetch
+ getTransactions({ startDate: rangeStartStr, endDate: rangeEndStr, pageSize: 1000, ledgerId: currentLedgerId }), // Bulk fetch
]);
setAccounts(accountsData || []);
setRecentTransactions(recentTxData?.items || []);
setCategories(categoriesData || []);
setLedgers(ledgersData || []);
- setSettings(settingsData);
setStreakInfo(streakData);
// In-Memory Aggregation fix
@@ -198,12 +201,13 @@ function Home() {
const handleLedgerSelect = async (ledgerId: number) => {
try {
- if (settings) {
- await updateSettings({ ...settings, currentLedgerId: ledgerId });
- setSettings({ ...settings, currentLedgerId: ledgerId });
- }
- setLedgerSelectorOpen(false);
- await loadData();
+ try {
+ if (settings) {
+ await updateSettings({ currentLedgerId: ledgerId });
+ }
+ setLedgerSelectorOpen(false);
+ // Data reload triggered by useEffect on currentLedgerId change
+ } catch (err) { setError('切换账本失败'); }
} catch (err) { setError('切换账本失败'); }
};
const handleLedgerReorder = async (reorderedLedgers: Ledger[]) => {
diff --git a/src/pages/Transactions/Transactions.tsx b/src/pages/Transactions/Transactions.tsx
index 6739a98..6d4843d 100644
--- a/src/pages/Transactions/Transactions.tsx
+++ b/src/pages/Transactions/Transactions.tsx
@@ -23,6 +23,7 @@ import { getCategories } from '../../services/categoryService';
import { getAccounts } from '../../services/accountService';
import { suggestAllocationForIncome } from '../../services/allocationRuleService';
import type { AllocationResult } from '../../services/allocationRuleService';
+import { useSettings } from '../../contexts/SettingsContext';
import { formatCurrency } from '../../utils';
import { Icon } from '@iconify/react';
import { TransactionForm } from '../../components/transaction';
@@ -66,6 +67,9 @@ export const Transactions: React.FC = () => {
return params;
});
+ const { settings, loading: settingsLoading } = useSettings();
+ const currentLedgerId = settings?.currentLedgerId || 0;
+
const [selectedTransaction, setSelectedTransaction] = useState(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deleting, setDeleting] = useState(false);
@@ -115,6 +119,7 @@ export const Transactions: React.FC = () => {
let params: any = {
...filterValues,
+ ledgerId: currentLedgerId,
};
if (viewMode === 'list') {
@@ -143,7 +148,7 @@ export const Transactions: React.FC = () => {
setLoading(false);
}
},
- [filterValues, viewMode, calendarMonth]
+ [filterValues, viewMode, calendarMonth, currentLedgerId]
);
// Fetch categories and accounts on mount
@@ -163,8 +168,9 @@ export const Transactions: React.FC = () => {
// Fetch transactions when filters change
useEffect(() => {
+ if (settingsLoading) return;
fetchTransactions(1);
- }, [fetchTransactions]);
+ }, [fetchTransactions, settingsLoading]);
// Handle filter changes
const handleFilterChange = useCallback((values: FilterValues) => {
@@ -511,6 +517,7 @@ export const Transactions: React.FC = () => {
onSubmit={handleCreateTransaction}
onCancel={() => setShowTransactionForm(false)}
loading={formLoading}
+ currentLedgerId={currentLedgerId}
/>
diff --git a/src/services/transactionService.ts b/src/services/transactionService.ts
index 30250fa..a41f13b 100644
--- a/src/services/transactionService.ts
+++ b/src/services/transactionService.ts
@@ -7,6 +7,7 @@ import api from './api';
import type {
Transaction,
TransactionFormInput,
+ TransactionFilter,
ApiResponse,
PaginatedResponse,
TransactionType,
@@ -16,27 +17,7 @@ import type {
RefundStatus,
} from '../types';
-/**
- * Transaction filter parameters
- */
-export interface TransactionFilter {
- /** Filter by start date (inclusive) */
- startDate?: string;
- /** Filter by end date (inclusive) */
- endDate?: string;
- /** Filter by category ID */
- categoryId?: number;
- /** Filter by account ID */
- accountId?: number;
- /** Filter by transaction type */
- type?: TransactionType;
- /** Search in notes */
- search?: string;
- /** Page number (1-based) */
- page?: number;
- /** Items per page */
- pageSize?: number;
-}
+// TransactionFilter is imported from types/index.ts
/**
* Transform API response transaction to frontend Transaction type
@@ -79,6 +60,8 @@ export async function getTransactions(
if (filter.categoryId) params.category_id = filter.categoryId;
if (filter.accountId) params.account_id = filter.accountId;
if (filter.type) params.type = filter.type;
+ if (filter.currency) params.currency = filter.currency;
+ if (filter.ledgerId) params.ledger_id = filter.ledgerId;
if (filter.search) params.note_search = filter.search;
if (filter.page) params.offset = ((filter.page - 1) * (filter.pageSize || 20)).toString();
if (filter.pageSize) params.limit = filter.pageSize;
@@ -153,6 +136,7 @@ export async function createTransaction(data: TransactionFormInput): Promise>('/transactions', payload);
if (!response.data) {
@@ -178,6 +162,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;
+ if (data.ledgerId !== undefined) payload.ledger_id = data.ledgerId;
const response = await api.put>(`/transactions/${id}`, payload);
if (!response.data) {
diff --git a/src/types/index.ts b/src/types/index.ts
index 7912aec..96324c4 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -286,6 +286,7 @@ export interface TransactionFormInput {
transactionDate: string;
note?: string;
tagIds?: number[];
+ ledgerId?: number;
}
export interface AccountFormInput {
@@ -517,3 +518,20 @@ export interface UpdateDefaultAccountsInput {
defaultExpenseAccountId?: number | null;
defaultIncomeAccountId?: number | null;
}
+
+// Transaction List Filter Params
+export interface TransactionFilter {
+ startDate?: string;
+ endDate?: string;
+ categoryId?: number;
+ accountId?: number;
+ ledgerId?: number;
+ type?: TransactionType;
+ currency?: CurrencyCode;
+ noteSearch?: string;
+ sortField?: string;
+ sortAsc?: boolean;
+ search?: string; // Alias/Helper for noteSearch
+ page?: number; // Helper for offset calculation
+ pageSize?: number; // Helper for limit
+}
diff --git a/walkthrough.md b/walkthrough.md
new file mode 100644
index 0000000..2ca7905
--- /dev/null
+++ b/walkthrough.md
@@ -0,0 +1,62 @@
+# Ledger Integration Walkthrough
+
+This document outlines the changes made to integrate Ledger functionality into the Transaction flow and provides steps for verification.
+
+## Changes Overview
+
+### Backend (Go)
+- **`TransactionInput`**: Added `LedgerID` field to potential input.
+- **`TransactionService`**:
+ - Updated `CreateTransaction` to assign the user's default ledger if no `LedgerID` is provided.
+ - Updated `CreateTransaction` and `UpdateTransaction` to validate and link the `LedgerID`.
+ - Injected `LedgerRepository` to allow fetching default ledgers and validating IDs.
+- **`Router`**: Updated dependency injection to pass `LedgerRepository` to `TransactionService`.
+
+### Frontend (React/TypeScript)
+- **Global State**: Created `SettingsContext` to provide global access to the currently selected `ledgerId`.
+- **Types**: Updated `TransactionFormInput`, `TransactionFilter`, and other interfaces to support `this.ledgerId`.
+- **Service (`transactionService.ts`)**: Updated `getTransactions`, `createTransaction`, and `updateTransaction` to map `ledgerId` to the backend API parameters (`ledger_id`).
+- **Components**:
+ - **`TransactionForm`**: Now accepts `currentLedgerId` and submits it with new transactions.
+ - **`CommandPalette`**: Now filters transaction search results by the current active ledger.
+- **Pages**:
+ - **`Home`**: Updated to use `SettingsContext` for ledger selection and data refreshing.
+ - **`Transactions`**: Updated to filter the transaction list by the global `currentLedgerId` and pass it to the creation form.
+
+## Verification Steps
+
+### 1. Ledger Switching
+1. Navigate to the **Home** page.
+2. Click the Ledger selector in the header (top left pill).
+3. Select a different ledger (e.g., "Business" vs "Personal").
+4. **Verify**: The "Recent Transactions" list update to show only transactions for the selected ledger.
+5. **Verify**: The Dashboard stats (Income, Expense) update to reflect the selected ledger.
+
+### 2. Transaction Creation with Ledger
+1. Ensure you are in a specific ledger (e.g., "Personal").
+2. Click **"记一笔" (Add Transaction)**.
+3. Create a new transaction.
+4. **Verify**: The transaction appears in the "Personal" ledger list.
+5. Switch to a "Business" ledger.
+6. **Verify**: The newly created transaction does **NOT** appear in the "Business" ledger list.
+
+### 3. Transaction List Filtering
+1. Go to the **Transactions** page.
+2. **Verify**: The list only shows transactions for the currently selected ledger.
+3. Switch ledger using the global context (if accessible) or go back Home to switch, then return.
+4. **Verify**: The list refreshes to show the new ledger's transactions.
+
+### 4. Global Search
+1. Press `Ctrl+K` (or `Cmd+K`) to open the Command Palette.
+2. Type a search term for a transaction that exists in the *current* ledger.
+3. **Verify**: The transaction appears in results.
+4. Type a search term for a transaction that exists in a *different* ledger.
+5. **Verify**: The transaction does **NOT** appear in results.
+
+### 5. Exchange Rates (Regression Test)
+1. Go to **Exchange Rates** page.
+2. **Verify**: Net Worth and History still load correctly (this calculates based on *global* accounts, so it should remain unaffected by ledger selection).
+
+## Technical Notes
+- **Default Behavior**: If a transaction is created via API without a `ledger_id` (e.g., older clients), the backend now automatically assigns it to the user's **Default Ledger**.
+- **Context Persistence**: The selected ledger ID is stored in User Settings, so it persists across sessions if the backend supports saving settings (which it does).