From 71472e00b6f4d50877cd6b826acd80b36b8afa3a Mon Sep 17 00:00:00 2001 From: admin <1297598740@qq.com> Date: Wed, 28 Jan 2026 15:41:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BA=86=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=E6=80=A7=E4=BA=A4=E6=98=93=E5=88=97=E8=A1=A8=E3=80=81?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E5=8D=A1=E7=89=87=E3=80=81=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E7=AD=89=E5=A4=9A=E4=B8=AA=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E7=BB=84=E4=BB=B6=E5=92=8C=E9=A1=B5=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E5=BC=95=E5=85=A5=E4=BA=86AI=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/AccountCard/AccountCard.css | 26 + .../account/AccountCard/AccountCard.tsx | 7 +- .../account/AccountForm/AccountForm.css | 5 + .../account/AccountForm/AccountForm.tsx | 2 +- .../account/TransferForm/TransferForm.css | 7 +- .../AllocationRuleForm/AllocationRuleForm.css | 7 +- .../budget/BudgetForm/BudgetForm.css | 8 +- .../budget/PiggyBankForm/PiggyBankForm.css | 7 +- .../CategorySelector/CategorySelector.css | 30 +- .../CategorySelector/CategorySelector.tsx | 4 +- .../common/CategoryIcon/CategoryIcon.tsx | 4 +- .../CurrencySelector/CurrencySelector.css | 4 +- .../common/SpotlightGuide/SpotlightGuide.css | 11 +- .../CurrencyConverter/CurrencyConverter.css | 5 + .../HealthScoreModal/HealthScoreModal.css | 523 ++++++++++----- .../HealthScoreModal/HealthScoreModal.tsx | 297 ++++++--- .../RecurringTransactionForm.css | 6 + .../RecurringTransactionList.css | 8 +- .../RecurringTransactionList.tsx | 34 +- .../TransactionForm/TransactionForm.css | 610 ++++++++---------- src/config/categoryIcons.ts | 443 ++----------- src/pages/Home/Home.tsx | 31 +- src/pages/Reports/Reports.tsx | 27 +- src/services/aiService.ts | 132 +++- 24 files changed, 1129 insertions(+), 1109 deletions(-) diff --git a/src/components/account/AccountCard/AccountCard.css b/src/components/account/AccountCard/AccountCard.css index 9f70164..53eddf7 100644 --- a/src/components/account/AccountCard/AccountCard.css +++ b/src/components/account/AccountCard/AccountCard.css @@ -36,6 +36,32 @@ transform: scale(1.02); } +.account-card--warning { + background: linear-gradient(135deg, rgba(254, 226, 226, 0.9) 0%, rgba(254, 202, 202, 0.9) 100%); + border-color: #f87171; + box-shadow: 0 4px 20px rgba(220, 38, 38, 0.15); +} + +.account-card--warning .account-card__balance { + color: #b91c1c; + /* Darker red for text */ +} + +/* Dark mode override for warning */ +@media (prefers-color-scheme: dark) { + .account-card--warning { + background: linear-gradient(135deg, rgba(127, 29, 29, 0.4) 0%, rgba(153, 27, 27, 0.4) 100%); + /* Red-900/800 */ + border-color: #b91c1c; + /* Red-700 */ + } + + .account-card--warning .account-card__balance { + color: #fca5a5; + /* Red-300 */ + } +} + /* Background Decoration for Glass Effect */ .account-card__background-decoration { position: absolute; diff --git a/src/components/account/AccountCard/AccountCard.tsx b/src/components/account/AccountCard/AccountCard.tsx index ed684e8..75fdb43 100644 --- a/src/components/account/AccountCard/AccountCard.tsx +++ b/src/components/account/AccountCard/AccountCard.tsx @@ -88,7 +88,7 @@ export const AccountCard: React.FC = ({ return (
= ({
{account.isCredit && 信用} {showWarning && ( - - - 预警 + + )}
diff --git a/src/components/account/AccountForm/AccountForm.css b/src/components/account/AccountForm/AccountForm.css index c608a44..e86a8ff 100644 --- a/src/components/account/AccountForm/AccountForm.css +++ b/src/components/account/AccountForm/AccountForm.css @@ -131,6 +131,11 @@ border-color: var(--color-primary); } +.account-form__currency-select option { + background-color: var(--bg-elevated); + color: var(--text-primary); +} + /* Account type grid */ .account-form__type-grid { display: grid; diff --git a/src/components/account/AccountForm/AccountForm.tsx b/src/components/account/AccountForm/AccountForm.tsx index c6ff0d9..50cf502 100644 --- a/src/components/account/AccountForm/AccountForm.tsx +++ b/src/components/account/AccountForm/AccountForm.tsx @@ -74,7 +74,7 @@ const ICONS = [ 'solar:wallet-bold-duotone', 'solar:wad-of-money-bold-duotone', 'solar:safe-circle-bold-duotone', - 'solar:pig-money-bold-duotone', + 'ri:safe-2-fill', // Use reliable safe icon // Payment Methods 'ri:alipay-fill', diff --git a/src/components/account/TransferForm/TransferForm.css b/src/components/account/TransferForm/TransferForm.css index be877ed..e980440 100644 --- a/src/components/account/TransferForm/TransferForm.css +++ b/src/components/account/TransferForm/TransferForm.css @@ -54,6 +54,11 @@ border-color: var(--color-primary); } +.transfer-form__select option { + background-color: var(--bg-elevated); + color: var(--text-primary); +} + .transfer-form__select--error { border-color: var(--color-error); } @@ -276,4 +281,4 @@ .transfer-form__actions { flex-direction: column-reverse; } -} +} \ No newline at end of file diff --git a/src/components/budget/AllocationRuleForm/AllocationRuleForm.css b/src/components/budget/AllocationRuleForm/AllocationRuleForm.css index 3285060..a1378f2 100644 --- a/src/components/budget/AllocationRuleForm/AllocationRuleForm.css +++ b/src/components/budget/AllocationRuleForm/AllocationRuleForm.css @@ -64,6 +64,11 @@ border-color: var(--primary-color, #2196f3); } +.allocation-rule-form__select option { + background-color: var(--bg-elevated); + color: var(--text-primary); +} + .allocation-rule-form__input--error { border-color: var(--error-color, #f44336); } @@ -400,4 +405,4 @@ .allocation-rule-form__footer { flex-direction: column-reverse; } -} +} \ No newline at end of file diff --git a/src/components/budget/BudgetForm/BudgetForm.css b/src/components/budget/BudgetForm/BudgetForm.css index 635bec1..e45b6d2 100644 --- a/src/components/budget/BudgetForm/BudgetForm.css +++ b/src/components/budget/BudgetForm/BudgetForm.css @@ -145,6 +145,12 @@ box-shadow: 0 0 0 4px var(--color-primary-light); } +/* Ensure proper coloring for select options in dark mode */ +.budget-form__select option { + background-color: var(--bg-elevated); + color: var(--text-primary); +} + .budget-form__input-wrapper:focus-within .budget-form__input-icon { color: var(--color-primary); } @@ -282,4 +288,4 @@ .text-primary { color: var(--color-primary); -} +} \ No newline at end of file diff --git a/src/components/budget/PiggyBankForm/PiggyBankForm.css b/src/components/budget/PiggyBankForm/PiggyBankForm.css index d854571..5b5262a 100644 --- a/src/components/budget/PiggyBankForm/PiggyBankForm.css +++ b/src/components/budget/PiggyBankForm/PiggyBankForm.css @@ -130,6 +130,11 @@ box-shadow: 0 0 0 4px var(--color-accent-light); } +.piggy-bank-form__select option { + background-color: var(--bg-elevated); + color: var(--text-primary); +} + .piggy-bank-form__input-wrapper:focus-within .piggy-bank-form__input-icon { color: var(--color-accent); } @@ -241,4 +246,4 @@ .text-accent { color: var(--color-accent); -} +} \ No newline at end of file diff --git a/src/components/category/CategorySelector/CategorySelector.css b/src/components/category/CategorySelector/CategorySelector.css index 2e12cff..b8777b4 100644 --- a/src/components/category/CategorySelector/CategorySelector.css +++ b/src/components/category/CategorySelector/CategorySelector.css @@ -10,7 +10,8 @@ display: block; font-size: 0.875rem; font-weight: 500; - color: var(--color-text-secondary); + color: var(--text-secondary); + /* Correct variable */ margin-bottom: 0.5rem; display: flex; align-items: center; @@ -28,11 +29,15 @@ justify-content: space-between; width: 100%; padding: 0.75rem 1rem; - background: var(--glass-panel-bg); - border: 1px solid var(--glass-border); - border-radius: var(--radius-lg); + background: var(--bg-card); + /* Standard bg */ + border: 1px solid var(--border-color); + /* Standard border */ + border-radius: 12px; + /* Fixed radius variable */ font-size: 1rem; - color: var(--color-text); + color: var(--text-primary); + /* Standard text color */ cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); box-sizing: border-box; @@ -129,11 +134,12 @@ left: 0; width: 100%; max-height: 320px; - background: var(--glass-panel-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--glass-border); - border-radius: var(--radius-lg); + max-height: 320px; + background: var(--bg-elevated); + /* Solid background for visibility */ + backdrop-filter: none; + border: 1px solid var(--border-color); + border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); z-index: 1000; display: flex; @@ -216,11 +222,11 @@ } .category-selector__item:hover { - background-color: var(--color-bg-hover); + background-color: var(--bg-hover); } .category-selector__item--selected { - background-color: var(--color-primary-lighter); + background-color: var(--bg-active); color: var(--color-primary); font-weight: 500; } diff --git a/src/components/category/CategorySelector/CategorySelector.tsx b/src/components/category/CategorySelector/CategorySelector.tsx index 6456b0d..ef25d81 100644 --- a/src/components/category/CategorySelector/CategorySelector.tsx +++ b/src/components/category/CategorySelector/CategorySelector.tsx @@ -186,7 +186,7 @@ export const CategorySelector: React.FC = ({ )} {!isChild && !hasChildren && } - + {category.name} {isSelected && ( @@ -230,7 +230,7 @@ export const CategorySelector: React.FC = ({ {selectedCategory ? (
- + {selectedCategory.name}
diff --git a/src/components/common/CategoryIcon/CategoryIcon.tsx b/src/components/common/CategoryIcon/CategoryIcon.tsx index 1c77e51..18e5e29 100644 --- a/src/components/common/CategoryIcon/CategoryIcon.tsx +++ b/src/components/common/CategoryIcon/CategoryIcon.tsx @@ -5,6 +5,7 @@ import './CategoryIcon.css'; interface CategoryIconProps { categoryId: number; + icon?: string; size?: number; showBackground?: boolean; className?: string; @@ -13,12 +14,13 @@ interface CategoryIconProps { export const CategoryIcon: React.FC = ({ categoryId, + icon, size = 24, showBackground = true, className = '', variant = 'rounded', }) => { - const iconName = getCategoryIcon(categoryId); + const iconName = icon || getCategoryIcon(categoryId); const color = getCategoryColor(categoryId); if (showBackground) { diff --git a/src/components/common/CurrencySelector/CurrencySelector.css b/src/components/common/CurrencySelector/CurrencySelector.css index 317e4fb..b528a07 100644 --- a/src/components/common/CurrencySelector/CurrencySelector.css +++ b/src/components/common/CurrencySelector/CurrencySelector.css @@ -44,6 +44,8 @@ .currency-selector__select option { padding: 0.5rem; + background-color: var(--bg-elevated); + color: var(--text-primary); } /* Dark mode support */ @@ -55,4 +57,4 @@ .currency-selector__select:disabled { background-color: #1f2937; } -} +} \ No newline at end of file diff --git a/src/components/common/SpotlightGuide/SpotlightGuide.css b/src/components/common/SpotlightGuide/SpotlightGuide.css index 55f0352..7fb5039 100644 --- a/src/components/common/SpotlightGuide/SpotlightGuide.css +++ b/src/components/common/SpotlightGuide/SpotlightGuide.css @@ -43,9 +43,14 @@ /* Glassmorphism for tooltip */ .guide-tooltip { - background: var(--glass-panel-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); + background: var(--bg-elevated); + border: 1px solid var(--border-color); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] .guide-tooltip { + background: #1e293b; + border-color: rgba(255, 255, 255, 0.1); } .tooltip-header { diff --git a/src/components/exchangeRate/CurrencyConverter/CurrencyConverter.css b/src/components/exchangeRate/CurrencyConverter/CurrencyConverter.css index 04cc1da..3934f4a 100644 --- a/src/components/exchangeRate/CurrencyConverter/CurrencyConverter.css +++ b/src/components/exchangeRate/CurrencyConverter/CurrencyConverter.css @@ -185,6 +185,11 @@ /* Hide default arrow to use custom styling logic if needed, but keeping simple here */ } +.currency-converter__select option { + background-color: var(--bg-elevated); + color: var(--text-primary); +} + .currency-converter__select-symbol { font-size: 0.875rem; font-weight: 700; diff --git a/src/components/home/HealthScoreModal/HealthScoreModal.css b/src/components/home/HealthScoreModal/HealthScoreModal.css index 1688fec..185c26d 100644 --- a/src/components/home/HealthScoreModal/HealthScoreModal.css +++ b/src/components/home/HealthScoreModal/HealthScoreModal.css @@ -1,23 +1,33 @@ /** - * HealthScoreModal Styles + * HealthScoreModal Styles - Premium Glassmorphism */ +:root { + --health-score-gradient: linear-gradient(135deg, #4ade80 0%, #22c55e 100%); + --health-bg-gradient: linear-gradient(160deg, #1a1a2e 0%, #16213e 100%); + --health-text-main: #ffffff; + --health-text-sub: rgba(255, 255, 255, 0.7); + --health-glass-border: rgba(255, 255, 255, 0.08); + --health-card-bg: rgba(255, 255, 255, 0.03); + --health-card-hover: rgba(255, 255, 255, 0.08); +} + .health-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); + background-color: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 1rem; opacity: 0; - animation: fadeIn 0.3s forwards; + animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; } @keyframes fadeIn { @@ -27,19 +37,22 @@ } .health-modal-content { - background: var(--glass-panel-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); + background: var(--health-bg-gradient); width: 100%; - max-width: 400px; - border-radius: 32px; - padding: 2.5rem 2rem 2rem; + max-width: 420px; + border-radius: 36px; + padding: 2.5rem 1.75rem 1.75rem; position: relative; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); - border: 1px solid var(--glass-border); - transform: scale(0.9) translateY(20px); + box-shadow: + 0 40px 100px -20px rgba(0, 0, 0, 0.7), + 0 0 0 1px var(--health-glass-border) inset; + transform: scale(0.92) translateY(30px); opacity: 0; - transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); + transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1); + display: flex; + flex-direction: column; + max-height: 85vh; + color: white; } .health-modal-content.animate-in { @@ -47,44 +60,84 @@ opacity: 1; } -.health-modal-close { +/* Scrollable Content Area */ +.health-scroll-container { + overflow-y: auto; + padding: 0 0.5rem; + margin-right: -0.5rem; + scrollbar-width: none; +} + +.health-scroll-container::-webkit-scrollbar { + display: none; +} + +/* Header Controls */ +.health-modal-controls { position: absolute; top: 1.25rem; - right: 1.25rem; - background: transparent; - border: none; - color: var(--text-tertiary); - cursor: pointer; - padding: 0.25rem; - transition: all 0.2s ease; + width: calc(100% - 2.5rem); + display: flex; + justify-content: space-between; + z-index: 10; +} + +.health-icon-btn { + width: 40px; + height: 40px; border-radius: 50%; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.05); + color: var(--health-text-sub); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + backdrop-filter: blur(10px); } -.health-modal-close:hover { - color: var(--text-primary); - background: rgba(0, 0, 0, 0.05); +.health-icon-btn:hover { + background: rgba(255, 255, 255, 0.15); + color: white; + transform: scale(1.1) rotate(5deg); + border-color: rgba(255, 255, 255, 0.2); } -.health-modal-header { +/* Score Section */ +/* Score Section */ +.health-score-section { display: flex; flex-direction: column; align-items: center; margin-bottom: 2rem; + margin-top: 0.5rem; + position: relative; } -.health-score-ring-large { +.health-score-ring-container { position: relative; - width: 160px; - height: 160px; - margin-bottom: 1.5rem; + width: 180px; + height: 180px; + margin-bottom: 1.25rem; + filter: drop-shadow(0 0 30px rgba(74, 222, 128, 0.15)); +} + +.health-score-ring-svg { + transform: rotate(-90deg); +} + +.health-ring-bg { + stroke: rgba(255, 255, 255, 0.03); } .health-ring-progress { - transition: stroke-dasharray 2s cubic-bezier(0.19, 1, 0.22, 1); - transform-origin: center; + transition: stroke-dasharray 2s cubic-bezier(0.22, 1, 0.36, 1); + stroke-linecap: round; + filter: drop-shadow(0 0 10px rgba(74, 222, 128, 0.5)); } -.health-score-value-container { +.health-score-content { position: absolute; top: 50%; left: 50%; @@ -92,165 +145,285 @@ display: flex; flex-direction: column; align-items: center; + text-align: center; } -.health-score-value { - font-size: 3.5rem; +.health-score-number { + font-size: 4rem; font-weight: 800; font-family: 'Outfit', sans-serif; line-height: 1; - text-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + background: linear-gradient(180deg, #ffffff 0%, #c0c0c0 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + letter-spacing: -2px; } .health-score-label { - font-size: 0.875rem; - color: var(--text-secondary); + font-size: 0.9rem; + color: var(--health-text-sub); font-weight: 600; - margin-top: 4px; - letter-spacing: 0.05em; + margin-top: 0.5rem; + letter-spacing: 0.1em; + text-transform: uppercase; + opacity: 0.8; } +/* Level Badge */ .health-level-badge { - display: flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - border-radius: 20px; - font-size: 0.95rem; - font-weight: 700; -} - -.health-metrics-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - margin-bottom: 2rem; -} - -.health-metric-card { - background: rgba(255, 255, 255, 0.5); - border-radius: 20px; - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.75rem; - border: 1px solid rgba(0, 0, 0, 0.05); -} - -.metric-icon { - width: 36px; - height: 36px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 0.25rem; -} - -.metric-icon.debt { - background: rgba(239, 68, 68, 0.1); - color: var(--color-error); -} - -.metric-icon.spend { - background: rgba(245, 158, 11, 0.1); - color: var(--color-warning); -} - -.metric-info { - display: flex; - flex-direction: column; -} - -.metric-label { - font-size: 0.8rem; - color: var(--text-secondary); - font-weight: 500; - margin-bottom: 0.25rem; -} - -.metric-value-row { - display: flex; - align-items: baseline; - justify-content: space-between; -} - -.metric-value { - font-size: 1.125rem; - font-weight: 700; - color: var(--text-primary); - font-family: 'Outfit', sans-serif; -} - -.metric-status { - font-size: 0.75rem; - font-weight: 600; -} - -.metric-trend { - font-size: 0.7rem; - margin-top: 4px; -} - -.metric-trend.up { - color: var(--color-error); - /* Higher spend is usually bad */ -} - -.metric-trend.down { - color: var(--color-success); - /* Lower spend is usually good */ -} - -.health-suggestion-box { - background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(37, 99, 235, 0.05)); - border-radius: 20px; - padding: 1.25rem; - margin-bottom: 2rem; - border: 1px solid rgba(59, 130, 246, 0.1); -} - -.suggestion-title { + padding: 8px 20px; + border-radius: 99px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); display: flex; align-items: center; gap: 8px; font-size: 0.95rem; - font-weight: 700; - color: var(--text-primary); - margin-top: 0; - margin-bottom: 0.5rem; -} - -.suggestion-text { - font-size: 0.9rem; - color: var(--text-secondary); - line-height: 1.6; - margin: 0; -} - -.health-actions { - display: flex; - justify-content: center; -} - -.health-action-btn { - width: 100%; - padding: 1rem; - border-radius: 16px; font-weight: 600; - font-size: 1rem; - border: none; - cursor: pointer; - transition: all 0.2s; + backdrop-filter: blur(12px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2); + transition: transform 0.3s; } -.health-action-btn.primary { - background: var(--text-primary); - color: var(--bg-primary); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.health-action-btn.primary:hover { +.health-level-badge:hover { transform: translateY(-2px); - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); + background: rgba(255, 255, 255, 0.08); +} + +/* Metrics Grid */ +.health-metrics-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 1.25rem; +} + +.health-metric-card { + background: var(--health-card-bg); + border-radius: 20px; + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + transition: all 0.3s; + border: 1px solid var(--health-glass-border); + position: relative; + overflow: hidden; + height: 100%; + justify-content: space-between; +} + +.health-metric-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, transparent 100%); + pointer-events: none; +} + +.health-metric-card:hover { + transform: translateY(-5px); + background: var(--health-card-hover); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.metric-icon-wrapper { + width: 44px; + height: 44px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.75rem; + backdrop-filter: blur(10px); +} + +.metric-title { + font-size: 0.875rem; + color: var(--health-text-sub); + font-weight: 500; +} + +.metric-value-group { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 0.25rem; +} + +.metric-number { + font-size: 1.5rem; + font-weight: 700; + font-family: 'Outfit', sans-serif; + color: white; + letter-spacing: -0.5px; +} + +.metric-tag { + font-size: 0.75rem; + font-weight: 700; + padding: 4px 8px; + border-radius: 6px; + backdrop-filter: blur(4px); +} + +.metric-subtitle { + font-size: 0.8125rem; + color: var(--health-text-sub); + margin-top: 0.25rem; + opacity: 0.6; +} + +/* Suggestion Box */ +.health-suggestion-box { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(15, 23, 42, 0.3)); + border: 1px solid rgba(59, 130, 246, 0.15); + border-radius: 20px; + padding: 1.5rem; + margin-bottom: 1.25rem; + position: relative; + overflow: hidden; +} + +.health-suggestion-box::after { + content: ''; + position: absolute; + top: -50px; + right: -50px; + width: 100px; + height: 100px; + background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, transparent 70%); + border-radius: 50%; + filter: blur(20px); +} + +.suggestion-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 1rem; + color: #60a5fa; + font-weight: 700; + font-size: 1rem; + letter-spacing: 0.02em; +} + +.suggestion-content { + font-size: 0.95rem; + line-height: 1.8; + color: rgba(255, 255, 255, 0.9); + font-weight: 400; + text-align: justify; +} + +/* Action Button */ +.health-confirm-btn { + width: 100%; + padding: 1.1rem; + border-radius: 20px; + border: none; + font-size: 1.05rem; + font-weight: 600; + cursor: pointer; + background: white; + color: #0f172a; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + position: relative; + overflow: hidden; +} + +.health-confirm-btn:hover { + transform: translateY(-2px) scale(1.02); + box-shadow: 0 20px 40px rgba(255, 255, 255, 0.15); +} + +.health-confirm-btn:active { + transform: translateY(0) scale(0.98); +} + +/* Loading Dots */ +.loading-dots { + display: inline-block; + background: linear-gradient(90deg, #60a5fa 25%, rgba(96, 165, 250, 0.3) 50%, #60a5fa 75%); + background-size: 200% 100%; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: placeholderShimmer 2s infinite linear; +} + +@keyframes placeholderShimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} + +/* Rules Panel */ +.health-rules-panel { + background: rgba(255, 255, 255, 0.02); + border-radius: 24px; + padding: 1.5rem; + margin-bottom: 2rem; + animation: slideDown 0.4s cubic-bezier(0.16, 1, 0.3, 1); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px) scale(0.98); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.rule-item { + display: flex; + gap: 1.25rem; + padding-bottom: 1.25rem; + margin-bottom: 1.25rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.rule-item:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 0; +} + +.rule-score { + font-family: 'Outfit', sans-serif; + font-weight: 800; + color: #4ade80; + font-size: 1.25rem; + min-width: 3.5rem; + text-shadow: 0 0 20px rgba(74, 222, 128, 0.3); +} + +.rule-desc-group h4 { + margin: 0 0 0.4rem 0; + font-size: 1rem; + font-weight: 600; + color: white; +} + +.rule-desc-group p { + margin: 0; + font-size: 0.875rem; + color: var(--health-text-sub); + line-height: 1.5; } \ No newline at end of file diff --git a/src/components/home/HealthScoreModal/HealthScoreModal.tsx b/src/components/home/HealthScoreModal/HealthScoreModal.tsx index ac71c61..b29d3e7 100644 --- a/src/components/home/HealthScoreModal/HealthScoreModal.tsx +++ b/src/components/home/HealthScoreModal/HealthScoreModal.tsx @@ -1,12 +1,12 @@ /** * HealthScoreModal Component - * Displays detailed financial health analysis - * Phase 3 Requirement: Emotional interface & Smart feedback + * Displays detailed financial health analysis with premium glassmorphism design */ import React, { useEffect, useState } from 'react'; import { Icon } from '@iconify/react'; import { formatCurrency } from '../../../utils/format'; +import { getFinancialAdvice } from '../../../services/aiService'; import './HealthScoreModal.css'; interface HealthScoreModalProps { @@ -28,132 +28,233 @@ export const HealthScoreModal: React.FC = ({ todaySpend, yesterdaySpend, }) => { + const [showRules, setShowRules] = useState(false); const [animate, setAnimate] = useState(false); + const [aiAdvice, setAiAdvice] = useState(''); + const [loadingAdvice, setLoadingAdvice] = useState(false); + + // --- Logic & Calculations --- + const netAssets = totalAssets - totalLiabilities; + const debtRatio = totalAssets > 0 ? (totalLiabilities / totalAssets) * 100 : 0; + const spendDiff = todaySpend - yesterdaySpend; useEffect(() => { + let isMounted = true; if (isOpen) { - setTimeout(() => setAnimate(true), 100); + setTimeout(() => setAnimate(true), 50); + + // Fetch AI Advice if not already fetched + if (!aiAdvice) { + setLoadingAdvice(true); + getFinancialAdvice({ + score, + totalAssets, + totalLiabilities, + todaySpend, + yesterdaySpend + }) + .then(advice => { + if (isMounted) setAiAdvice(advice); + }) + .catch(err => { + console.error("Failed to load AI advice", err); + }) + .finally(() => { + if (isMounted) setLoadingAdvice(false); + }); + } } else { setAnimate(false); + setShowRules(false); // Reset view on close } + return () => { isMounted = false; }; }, [isOpen]); if (!isOpen) return null; - // Analysis Logic - const debtRatio = totalAssets > 0 ? (totalLiabilities / totalAssets) * 100 : 0; - let debtLevel = '优秀'; - let debtColor = 'var(--color-success)'; - if (debtRatio > 30) { - debtLevel = '一般'; - debtColor = 'var(--color-warning)'; - } - if (debtRatio > 60) { - debtLevel = '危险'; - debtColor = 'var(--color-error)'; - } - - const spendDiff = todaySpend - yesterdaySpend; - const spendTrend = spendDiff > 0 ? 'up' : 'down'; - + // Status Levels const getLevel = (s: number) => { - if (s >= 90) return { label: '卓越', color: '#10b981', icon: 'solar:cup-star-bold-duotone' }; - if (s >= 80) return { label: '优秀', color: '#3b82f6', icon: 'solar:medal-star-bold-duotone' }; - if (s >= 60) return { label: '良好', color: '#f59e0b', icon: 'solar:check-circle-bold-duotone' }; - return { label: '需努力', color: '#ef4444', icon: 'solar:danger-circle-bold-duotone' }; + if (s >= 95) return { label: 'SSS', title: '财务自由', color: '#10b981', icon: 'solar:crown-star-bold-duotone', desc: '完美无瑕的资产结构' }; + if (s >= 90) return { label: 'S', title: '卓越', color: '#34d399', icon: 'solar:cup-star-bold-duotone', desc: '财务状况极佳' }; + if (s >= 80) return { label: 'A', title: '优秀', color: '#3b82f6', icon: 'solar:medal-star-bold-duotone', desc: '资产配置健康' }; + if (s >= 70) return { label: 'B', title: '良好', color: '#6366f1', icon: 'solar:check-circle-bold-duotone', desc: '继续保持记账习惯' }; + if (s >= 60) return { label: 'C', title: '及格', color: '#f59e0b', icon: 'solar:info-circle-bold-duotone', desc: '需注意控制负债' }; + return { label: 'D', title: '危险', color: '#ef4444', icon: 'solar:danger-circle-bold-duotone', desc: '建议立即调整开支' }; }; const level = getLevel(score); + // Dynamic Suggestion (Fallback) + const getStaticSuggestion = () => { + if (debtRatio > 50) return "负债率偏高(>50%)。建议优先偿还高息债务(如信用卡),避免产生不必要的利息支出。同时请审视非必要消费。"; + if (score < 60) return "建议建立强制储蓄计划,每月发工资后先存下一笔钱。同时,坚持每日记账能帮助你发现隐形浪费。"; + if (score >= 90) return "您的财务状况非常健康!目前的低负债率是很好的优势。建议考虑学习理财知识,让结余资金通过稳健投资实现增值。"; + return "财务状况良好。建议检查是否有闲置资金可以转入储蓄账户赚取收益,并尝试为自己设定一个年度储蓄目标。"; + }; + + // --- Render Helpers --- + return (
e.stopPropagation()} > - - -
-
- - - - -
- {score} - 健康分 -
-
- -
- - {level.label}状态 -
+ {/* Header Controls */} +
+ +
-
-
-
- -
-
- 负债率 -
- {debtRatio.toFixed(1)}% - {debtLevel} +
+ {showRules ? ( + // --- Rules View --- +
+

+ + 评分规则说明 +

+ +
+
40+
+
+

基础资产分

+

基于净资产率 (净资产/总资产) 计算。这是健康分的基石,资产越多负债越少,得分越高。

+
+
+ +
+
+5
+
+

连续记账奖励

+

连续记账超过 3 天,奖励坚持好习惯。记账是理财的第一步。

+
+
+ +
+
+5
+
+

活跃度奖励

+

近两天内有记账行为。保持对财务状况的关注有助于及时调整策略。

+
+
+ +
+
Lv
+
+

等级划分

+

SSS (95+), S (90-94), A (80-89), B (70-79), C (60-69), D (60以下)

+
-
+ ) : ( + // --- Score View --- + <> +
+
+ + + + + + + + {/* Background Ring */} + + {/* Progress Ring */} + + +
+ {score} + 健康分 +
+
-
-
- -
-
- 今日消费 -
- {formatCurrency(todaySpend)} +
+ + {level.title} · {level.desc} +
- - {spendTrend === 'up' ? '比昨天多' : '比昨天少'} {formatCurrency(Math.abs(spendDiff))} - -
-
+ +
+ {/* Debt Card */} +
+
+ +
+ 负债率 +
+ {debtRatio.toFixed(1)}% + 30 ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)', + color: debtRatio > 30 ? '#ef4444' : '#10b981' + }}> + {debtRatio > 30 ? '一般' : '优秀'} + +
+ + {debtRatio === 0 ? '无负债一身轻' : `负债 ${formatCurrency(totalLiabilities)}`} + +
+ + {/* Spending Card */} +
+
+ +
+ 今日消费 +
+ {formatCurrency(todaySpend)} +
+ 0 ? '#ef4444' : '#10b981' }}> + 比昨日 {spendDiff > 0 ? '多' : '少'} {formatCurrency(Math.abs(spendDiff))} + +
+
+ + {/* Suggestion Box */} +
+
+ + AI 智能建议 + {loadingAdvice && 思考中...} +
+

+ {loadingAdvice ? ( + 正在生成个性化理财建议... + ) : ( + aiAdvice || getStaticSuggestion() + )} +

+
+ + )}
-
-

- - 智能建议 -

-

- {score >= 80 - ? '您的财务状况非常健康!建议继续保持低负债率,并考虑适当增加投资比例以抵抗通胀。' - : score >= 60 - ? '财务状况良好,但还有提升空间。试着控制非必要支出,提高每月的储蓄比例。' - : '请注意控制支出!建议优先偿还高息债务,并审视近期的消费习惯。'} -

-
- -
-
diff --git a/src/components/transaction/RecurringTransactionForm/RecurringTransactionForm.css b/src/components/transaction/RecurringTransactionForm/RecurringTransactionForm.css index 1b75ab3..f68a489 100644 --- a/src/components/transaction/RecurringTransactionForm/RecurringTransactionForm.css +++ b/src/components/transaction/RecurringTransactionForm/RecurringTransactionForm.css @@ -274,6 +274,12 @@ box-shadow: 0 0 0 3px var(--color-primary-lighter); } +.recurring-transaction-form__select option, +.recurring-transaction-form__currency-select option { + background-color: var(--bg-elevated); + color: var(--text-primary); +} + .recurring-transaction-form__input--error, .recurring-transaction-form__select--error { border-color: var(--color-error); diff --git a/src/components/transaction/RecurringTransactionList/RecurringTransactionList.css b/src/components/transaction/RecurringTransactionList/RecurringTransactionList.css index 082d4fc..c3422b2 100644 --- a/src/components/transaction/RecurringTransactionList/RecurringTransactionList.css +++ b/src/components/transaction/RecurringTransactionList/RecurringTransactionList.css @@ -98,9 +98,11 @@ /* Status badge */ .recurring-transaction-item__status { - position: absolute; - top: 1.5rem; - right: 1.5rem; + /* position: absolute; Removed to prevent overlap */ + /* top: 1.5rem; */ + /* right: 1.5rem; */ + display: flex; + align-items: center; } .recurring-transaction-item__status-badge { diff --git a/src/components/transaction/RecurringTransactionList/RecurringTransactionList.tsx b/src/components/transaction/RecurringTransactionList/RecurringTransactionList.tsx index fab616d..4a37ef0 100644 --- a/src/components/transaction/RecurringTransactionList/RecurringTransactionList.tsx +++ b/src/components/transaction/RecurringTransactionList/RecurringTransactionList.tsx @@ -124,20 +124,7 @@ export const RecurringTransactionList: React.FC = className={`recurring-transaction-item glass-card ${!rt.isActive ? 'recurring-transaction-item--inactive' : '' }`} > - {/* Status indicator */} -
- {rt.isActive ? ( - - 启用 - - ) : ( - - 已停用 - - )} -
- - {/* Main content */} + {/* Status indicator has been moved inside header, but we need the content wrapper for padding */}
{/* Header */}
@@ -145,8 +132,23 @@ export const RecurringTransactionList: React.FC = {typeInfo.icon} {typeInfo.label}
+ + {/* Status indicator - Moved inside header */} +
+ {rt.isActive ? ( + + 启用 + + ) : ( + + 已停用 + + )} +
+
{rt.type === 'income' ? '+' : '-'} {formatCurrency(rt.amount, rt.currency)} @@ -159,7 +161,7 @@ export const RecurringTransactionList: React.FC = {category && (
- + {category.name} @@ -253,7 +255,7 @@ export const RecurringTransactionList: React.FC =
); })} -
+
); }; diff --git a/src/components/transaction/TransactionForm/TransactionForm.css b/src/components/transaction/TransactionForm/TransactionForm.css index efd384b..89c1258 100644 --- a/src/components/transaction/TransactionForm/TransactionForm.css +++ b/src/components/transaction/TransactionForm/TransactionForm.css @@ -1,18 +1,33 @@ /** - /* TransactionForm Component - Clean Modern Style */ + * TransactionForm Component - Premium Design (Refined) + */ .transaction-form { display: flex; flex-direction: column; - background: var(--glass-panel-bg); - backdrop-filter: var(--glass-blur); - border: 1px solid var(--glass-border); - border-radius: var(--radius-xl); - max-width: 500px; + background: var(--bg-elevated); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--border-color); + border-radius: 24px; width: 100%; + max-width: 440px; margin: 0 auto; overflow: hidden; - box-shadow: var(--shadow-xl); + box-shadow: 0 20px 60px -10px rgba(0, 0, 0, 0.15); + animation: modalSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes modalSlideUp { + from { + opacity: 0; + transform: translateY(20px) scale(0.96); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } } /* Header */ @@ -20,397 +35,313 @@ display: flex; justify-content: space-between; align-items: center; - padding: var(--spacing-md) var(--spacing-lg); - border-bottom: 1px solid var(--glass-border); - background: rgba(255, 255, 255, 0.4); + padding: 1.25rem 1.5rem 0.5rem; } .transaction-form__title { margin: 0; - font-size: 1.125rem; + font-size: 1.25rem; font-weight: 700; - color: var(--color-text); + color: var(--text-primary); + font-family: 'Outfit', sans-serif; } .transaction-form__close-btn { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid transparent; - background: transparent; - color: var(--color-text-secondary); - font-size: 1.25rem; - cursor: pointer; - border-radius: var(--radius-full); - transition: all 0.2s ease; -} - -.transaction-form__close-btn:hover { - background: var(--color-bg-tertiary); - border-color: var(--glass-border); - color: var(--color-text); -} - -/* Step Indicator */ -.transaction-form__steps { - display: flex; - justify-content: center; - align-items: center; - padding: var(--spacing-md) var(--spacing-lg); - gap: var(--spacing-sm); - background: var(--color-primary-lighter); -} - - -.transaction-form__step { - display: flex; - align-items: center; - gap: var(--spacing-sm); - opacity: 0.5; - transition: opacity 0.2s ease; -} - -.transaction-form__step--active, -.transaction-form__step--completed { - opacity: 1; -} - -.transaction-form__step-number { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; - border-radius: var(--radius-full); - background: var(--glass-bg); - border: 2px solid var(--glass-border); - color: var(--color-text-secondary); - font-size: 0.875rem; - font-weight: 700; + border: none; + background: var(--bg-hover); + color: var(--text-secondary); + font-size: 1.25rem; + cursor: pointer; + border-radius: 50%; transition: all 0.2s ease; } -.transaction-form__step--active .transaction-form__step-number { - background: var(--color-primary); - border-color: var(--color-primary); - color: white; +.transaction-form__close-btn:hover { + background: var(--bg-active); + color: var(--text-primary); } -.transaction-form__step--completed .transaction-form__step-number { - background: var(--color-success); - border-color: var(--color-success); - color: white; +/* Step Indicator - Cleaner */ +.transaction-form__steps { + display: flex; + justify-content: center; + align-items: flex-start; + padding: 0 0 1.5rem; + gap: 2rem; + position: relative; +} + +.transaction-form__step { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + position: relative; + z-index: 1; + min-width: 40px; +} + +.transaction-form__step-number { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--bg-tertiary); + color: var(--text-tertiary); + font-size: 0.85rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--border-color); + transition: all 0.3s ease; } .transaction-form__step-label { - font-size: 0.8125rem; - font-weight: 600; - color: var(--color-text-secondary); + font-size: 0.75rem; + color: var(--text-tertiary); + font-weight: 500; + transition: all 0.3s ease; + white-space: nowrap; +} + +/* Active Step */ +.transaction-form__step--active .transaction-form__step-number { + background: var(--color-primary); + color: white; + border-color: var(--color-primary); + box-shadow: 0 4px 10px rgba(var(--primary-rgb), 0.3); + transform: scale(1.1); } .transaction-form__step--active .transaction-form__step-label { - color: var(--color-text); + color: var(--color-primary); + font-weight: 700; } -/* Connector between steps */ -.transaction-form__step:not(:last-child)::after { - content: ''; - width: 32px; - height: 2px; - background: var(--glass-border); - margin-left: var(--spacing-sm); - border-radius: 1px; -} - -.transaction-form__step--completed:not(:last-child)::after { +/* Completed Step */ +.transaction-form__step--completed .transaction-form__step-number { background: var(--color-success); + color: white; + border-color: var(--color-success); +} + +.transaction-form__step--completed .transaction-form__step-label { + color: var(--color-success); } /* Body */ .transaction-form__body { - padding: var(--spacing-lg); - min-height: 320px; + padding: 0 1.5rem 1.5rem; + min-height: 340px; } .transaction-form__step-content { display: flex; flex-direction: column; - gap: var(--spacing-lg); + gap: 1.5rem; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateX(10px); + } + + to { + opacity: 1; + transform: translateX(0); + } } .transaction-form__step-title { - margin: 0 0 var(--spacing-sm) 0; + margin: 0; font-size: 1rem; font-weight: 600; - color: var(--color-text); + color: var(--text-secondary); text-align: center; + opacity: 0.8; } -/* Type Toggle */ +/* Type Toggle - Segmented Control */ .transaction-form__type-toggle { display: flex; - gap: var(--spacing-md); - justify-content: center; + background: var(--bg-tertiary); + padding: 4px; + border-radius: 16px; + margin-bottom: 1rem; } .transaction-form__type-btn { flex: 1; - max-width: 140px; display: flex; - flex-direction: column; align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-lg); - border: 2px solid var(--glass-border); - border-radius: var(--radius-lg); - background: var(--glass-bg); + justify-content: center; + gap: 8px; + padding: 10px; + border: none; + border-radius: 12px; + background: transparent; cursor: pointer; + font-weight: 600; + color: var(--text-secondary); transition: all 0.2s ease; } .transaction-form__type-btn:hover { - border-color: var(--color-primary); + color: var(--text-primary); } .transaction-form__type-btn--expense { - border-color: var(--color-error); - background: var(--color-error-light); + background: var(--bg-elevated); + /* But we want specific colors for active state */ } -.transaction-form__type-btn--income { - border-color: var(--color-success); - background: var(--color-success-light); +/* Active States */ +.transaction-form__type-btn.transaction-form__type-btn--expense { + background: var(--bg-elevated); + color: var(--color-error); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } -.transaction-form__type-btn:disabled { - opacity: 0.6; - cursor: not-allowed; +.transaction-form__type-btn.transaction-form__type-btn--income { + background: var(--bg-elevated); + color: var(--color-success); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } -.transaction-form__type-icon { - font-size: 2rem; -} - -.transaction-form__type-label { - font-size: 0.9375rem; - font-weight: 700; - color: var(--color-text); -} - -/* Amount Input */ +/* Amount Input - Big & Bold */ .transaction-form__amount-container { display: flex; - align-items: center; + align-items: baseline; justify-content: center; - gap: var(--spacing-sm); - margin-top: var(--spacing-md); -} - -.transaction-form__currency-selector { - flex-shrink: 0; + gap: 4px; + margin: 1rem 0 2rem; + position: relative; } .transaction-form__currency-select { - padding: var(--spacing-sm) var(--spacing-md); - border: 1px solid var(--glass-border); - border-radius: var(--radius-md); - font-size: 1.25rem; - font-weight: 700; - background: var(--glass-bg); - color: var(--color-text); + appearance: none; + border: none; + background: transparent; + font-size: 1.5rem; + font-weight: 600; + color: var(--text-secondary); cursor: pointer; - min-width: 70px; - transition: border-color 0.2s ease; + padding: 0 0.5rem; + outline: none; } -.transaction-form__currency-select:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 3px var(--color-primary-light); +.transaction-form__currency-select option { + background-color: var(--bg-elevated); + color: var(--text-primary); } .transaction-form__amount-input { - flex: 1; - max-width: 200px; - padding: var(--spacing-md); - border: 2px solid var(--glass-border); - border-radius: var(--radius-lg); - font-size: 2rem; + width: 100%; + border: none; + background: transparent; + font-size: 3.5rem; font-weight: 800; text-align: center; - background: var(--glass-bg); - color: var(--color-text); - transition: border-color 0.2s ease; -} - -.transaction-form__amount-input:focus { + color: var(--text-primary); + font-family: 'Outfit', sans-serif; + /* Monospace numbers */ outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 4px var(--color-primary-light); -} - -.transaction-form__amount-input--error { - border-color: var(--color-error); + padding: 0; + caret-color: var(--color-primary); } .transaction-form__amount-input::placeholder { - color: var(--color-text-muted); -} - - -/* Field styles */ -.transaction-form__field { - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} - -.transaction-form__label { - font-size: 0.8125rem; - font-weight: 600; - color: var(--color-text); -} - -.transaction-form__required { - color: var(--color-error); -} - -.transaction-form__input { - padding: var(--spacing-sm) var(--spacing-md); - border: 1px solid var(--glass-border); - border-radius: var(--radius-md); - font-size: 1rem; - background: var(--glass-bg); - color: var(--color-text); - transition: border-color 0.2s ease; -} - -.transaction-form__input:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 3px var(--color-primary-light); -} - -.transaction-form__textarea { - padding: var(--spacing-sm) var(--spacing-md); - border: 1px solid var(--glass-border); - border-radius: var(--radius-md); - font-size: 1rem; - background: var(--glass-bg); - color: var(--color-text); - resize: vertical; - min-height: 60px; - font-family: inherit; - transition: border-color 0.2s ease; -} - -.transaction-form__textarea:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 3px var(--color-primary-light); -} - -.transaction-form__textarea::placeholder { - color: var(--color-text-muted); + color: var(--border-color); + opacity: 0.5; } .transaction-form__error { - font-size: 0.75rem; + text-align: center; color: var(--color-error); - text-align: center; + font-size: 0.85rem; font-weight: 500; -} - -.transaction-form__loading, -.transaction-form__empty { - padding: var(--spacing-md); - text-align: center; - color: var(--color-text-secondary); - font-size: 0.875rem; + margin-top: -1rem; } /* Account Grid */ .transaction-form__account-grid { display: grid; grid-template-columns: repeat(2, 1fr); - gap: var(--spacing-sm); + gap: 12px; } .transaction-form__account-btn { display: flex; - flex-direction: column; align-items: center; - gap: var(--spacing-xs); - padding: var(--spacing-md); - border: 1px solid var(--glass-border); - border-radius: var(--radius-md); - background: var(--glass-bg); + gap: 12px; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 16px; + background: var(--bg-card); cursor: pointer; transition: all 0.2s ease; + text-align: left; } .transaction-form__account-btn:hover { - border-color: var(--color-primary); - background: var(--color-primary-lighter); + background: var(--bg-hover); + border-color: var(--border-color-strong); } .transaction-form__account-btn--selected { border-color: var(--color-primary); - background: var(--color-primary-light); - box-shadow: 0 0 0 3px var(--color-primary-light); -} - -.transaction-form__account-btn:disabled { - opacity: 0.6; - cursor: not-allowed; + background: rgba(var(--accent-rgb), 0.05); + box-shadow: 0 0 0 2px var(--color-primary-lighter); } .transaction-form__account-icon { - font-size: 1.5rem; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-tertiary); + border-radius: 10px; + font-size: 1.2rem; +} + +.transaction-form__account-info { + display: flex; + flex-direction: column; } .transaction-form__account-name { - font-size: 0.875rem; + font-size: 0.9rem; font-weight: 600; - color: var(--color-text); + color: var(--text-primary); } .transaction-form__account-balance { font-size: 0.75rem; - color: var(--color-text-secondary); + color: var(--text-secondary); } -/* Summary */ +/* Summary Card (Step 3) */ .transaction-form__summary { - background: var(--color-bg-tertiary); - border: 1px solid var(--glass-border); - border-radius: var(--radius-lg); - padding: var(--spacing-md); - margin-bottom: var(--spacing-sm); -} - -.transaction-form__summary-row { + background: var(--bg-tertiary); + border-radius: 16px; + padding: 1.25rem; display: flex; justify-content: space-between; align-items: center; - padding: var(--spacing-sm) 0; } -.transaction-form__summary-row:not(:last-child) { - border-bottom: 1px solid var(--glass-border); -} - -.transaction-form__summary-label { - font-size: 0.8125rem; - color: var(--color-text-secondary); -} - -.transaction-form__summary-value { - font-size: 0.875rem; - font-weight: 600; - color: var(--color-text); +.transaction-form__summary-amount { + font-size: 1.5rem; + font-weight: 700; + font-family: 'Outfit', sans-serif; } .transaction-form__summary-value--expense { @@ -421,102 +352,85 @@ color: var(--color-success); } +/* Inputs in Step 3 */ +.transaction-form__field { + margin-bottom: 1rem; +} -/* Actions */ +.transaction-form__label { + display: block; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.transaction-form__input, +.transaction-form__textarea { + width: 100%; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 12px; + background: var(--bg-card); + color: var(--text-primary); + font-size: 1rem; + transition: all 0.2s; + font-family: inherit; +} + +.transaction-form__input:focus, +.transaction-form__textarea:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-lighter); + outline: none; +} + +/* Actions Footer */ .transaction-form__actions { + padding: 1rem 1.5rem 1.5rem; display: flex; - gap: var(--spacing-md); - padding: var(--spacing-md) var(--spacing-lg); - border-top: 1px solid var(--glass-border); - background: rgba(255, 255, 255, 0.4); + gap: 1rem; + background: var(--bg-elevated); + /* Match body */ + border-top: 1px solid transparent; } .transaction-form__btn { flex: 1; - padding: var(--spacing-md) var(--spacing-lg); - border: none; - border-radius: var(--radius-md); + padding: 14px; + border-radius: 14px; font-size: 1rem; font-weight: 600; cursor: pointer; - transition: all 0.2s ease; -} - -.transaction-form__btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.transaction-form__btn--primary { - background: var(--color-primary); - color: white; -} - -.transaction-form__btn--primary:hover:not(:disabled) { - background: var(--color-primary-dark); + border: none; + transition: all 0.2s; } .transaction-form__btn--secondary { - background: rgba(255, 255, 255, 0.5); - color: var(--color-text); - border: 1px solid var(--glass-border); + background: var(--bg-hover); + color: var(--text-secondary); } -.transaction-form__btn--secondary:hover:not(:disabled) { - background: var(--color-bg-tertiary); +.transaction-form__btn--secondary:hover { + background: var(--bg-active); + color: var(--text-primary); } -/* Mobile */ -@media (max-width: 480px) { - .transaction-form { - border-radius: 0; - max-width: 100%; - min-height: 100vh; - } - - .transaction-form__header { - padding: var(--spacing-md); - } - - .transaction-form__steps { - padding: var(--spacing-sm) var(--spacing-md); - } - - .transaction-form__step-label { - display: none; - } - - .transaction-form__step:not(:last-child)::after { - width: 48px; - } - - .transaction-form__body { - padding: var(--spacing-md); - flex: 1; - } - - .transaction-form__amount-input { - font-size: 1.75rem; - } - - .transaction-form__account-grid { - grid-template-columns: 1fr; - } - - .transaction-form__actions { - padding: var(--spacing-md); - flex-direction: column-reverse; - } +.transaction-form__btn--primary { + background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark)); + color: white; + box-shadow: 0 4px 12px var(--color-primary-lighter); } -/* Reduced Motion */ -@media (prefers-reduced-motion: reduce) { +.transaction-form__btn--primary:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px var(--color-primary-lighter); +} - .transaction-form__type-btn, - .transaction-form__account-btn, - .transaction-form__btn, - .transaction-form__step-number, - .transaction-form__close-btn { - transition: none; - } +/* Loading/Empty States */ +.transaction-form__loading, +.transaction-form__empty { + text-align: center; + padding: 2rem; + color: var(--text-muted); } \ No newline at end of file diff --git a/src/config/categoryIcons.ts b/src/config/categoryIcons.ts index a8a1374..cdc162f 100644 --- a/src/config/categoryIcons.ts +++ b/src/config/categoryIcons.ts @@ -1,425 +1,58 @@ /** - * 分类图标配置 - * 使用 Iconify + Material Design Icons + * 分类图标配置 - 动态版本 + * 移除硬编码映射,改为完全依赖后端数据或默认值 */ -export const categoryIconMap: Record = { - // ============================================ - // 支出主分类 (1-21) - // ============================================ - 1: 'mdi:food-fork-drink', // 餐饮 - 2: 'mdi:car', // 交通 - 3: 'mdi:shopping', // 购物 - 4: 'mdi:gamepad-variant', // 娱乐 - 5: 'mdi:home', // 居住 - 6: 'mdi:hospital-box', // 医疗 - 7: 'mdi:school', // 教育 - 8: 'mdi:cellphone', // 通讯 - 9: 'mdi:gift', // 人情往来 - 10: 'mdi:bank', // 金融保险 - 11: 'mdi:face-woman', // 美容护理 - 12: 'mdi:paw', // 宠物 - 13: 'mdi:heart', // 慈善捐赠 - 14: 'mdi:baby-carriage', // 子女教育 - 15: 'mdi:human-cane', // 老人赡养 - 16: 'mdi:laptop', // 数码办公 - 17: 'mdi:dumbbell', // 运动健身 - 18: 'mdi:palette', // 文化艺术 - 19: 'mdi:airplane', // 旅游度假 - 20: 'mdi:car-side', // 汽车相关 - 21: 'mdi:dots-horizontal', // 其他支出 - - // ============================================ - // 收入主分类 (100-109) - // ============================================ - 100: 'mdi:cash-multiple', // 工资薪金 - 101: 'mdi:gift-outline', // 奖金福利 - 102: 'mdi:chart-line', // 投资收益 - 103: 'mdi:briefcase', // 兼职副业 - 104: 'mdi:store', // 经营所得 - 105: 'mdi:wallet-giftcard', // 礼金收入 - 106: 'mdi:cash-refund', // 退款返现 - 107: 'mdi:file-document', // 报销补贴 - 108: 'mdi:home-city', // 资产处置 - 109: 'mdi:cash', // 其他收入 - - // ============================================ - // 餐饮子分类 (201-212) - // ============================================ - 201: 'mdi:coffee', // 早餐 - 202: 'mdi:food', // 午餐 - 203: 'mdi:food-variant', // 晚餐 - 204: 'mdi:noodles', // 夜宵 - 205: 'mdi:popcorn', // 零食 - 206: 'mdi:cup', // 饮料 - 207: 'mdi:fruit-cherries', // 水果 - 208: 'mdi:moped', // 外卖 - 209: 'mdi:glass-cocktail', // 聚餐 - 210: 'mdi:tea', // 下午茶 - 211: 'mdi:cupcake', // 烘焙 - 212: 'mdi:cart', // 食材采购 - - // ============================================ - // 交通子分类 (220-231) - // ============================================ - 220: 'mdi:subway-variant', // 公交地铁 - 221: 'mdi:taxi', // 打车 - 222: 'mdi:gas-station', // 加油 - 223: 'mdi:parking', // 停车费 - 224: 'mdi:highway', // 过路费 - 225: 'mdi:car-wrench', // 车辆保养 - 226: 'mdi:shield-car', // 车辆保险 - 227: 'mdi:train', // 火车票 - 228: 'mdi:airplane-takeoff', // 飞机票 - 229: 'mdi:bike', // 共享单车 - 230: 'mdi:ferry', // 船票 - 231: 'mdi:bus', // 长途客车 - - // ============================================ - // 购物子分类 (240-253) - // ============================================ - 240: 'mdi:spray-bottle', // 日用品 - 241: 'mdi:tshirt-crew', // 服饰鞋包 - 242: 'mdi:laptop', // 数码产品 - 243: 'mdi:television', // 家电 - 244: 'mdi:sofa', // 家具 - 245: 'mdi:book-open-page-variant', // 图书 - 246: 'mdi:baby-bottle', // 母婴用品 - 247: 'mdi:gift', // 礼品 - 248: 'mdi:paperclip', // 办公用品 - 249: 'mdi:diamond-stone', // 珠宝首饰 - 250: 'mdi:bag-personal', // 箱包配饰 - 251: 'mdi:basketball', // 运动装备 - 252: 'mdi:tent', // 户外用品 - 253: 'mdi:teddy-bear', // 玩具 - - // ============================================ - // 娱乐子分类 (260-273) - // ============================================ - 260: 'mdi:movie', // 电影 - 261: 'mdi:controller', // 游戏 - 262: 'mdi:beach', // 旅游 - 263: 'mdi:run', // 运动健身 - 264: 'mdi:theater', // 演出票务 - 265: 'mdi:microphone', // KTV - 266: 'mdi:glass-mug-variant', // 酒吧 - 267: 'mdi:dice-multiple', // 桌游 - 268: 'mdi:camera', // 摄影 - 269: 'mdi:lock', // 密室逃脱 - 270: 'mdi:book-open', // 剧本杀 - 271: 'mdi:ferris-wheel', // 游乐场 - 272: 'mdi:image-frame', // 展览 - 273: 'mdi:bank-outline', // 博物馆 - - // ============================================ - // 居住子分类 (280-291) - // ============================================ - 280: 'mdi:home-outline', // 房租 - 281: 'mdi:home-city-outline', // 房贷 - 282: 'mdi:water', // 水电费 - 283: 'mdi:fire', // 燃气费 - 284: 'mdi:office-building', // 物业费 - 285: 'mdi:wifi', // 网费 - 286: 'mdi:wrench', // 维修 - 287: 'mdi:brush', // 装修 - 288: 'mdi:broom', // 家政服务 - 289: 'mdi:radiator', // 暖气费 - 290: 'mdi:delete', // 垃圾费 - 291: 'mdi:bed', // 家居用品 - - // ============================================ - // 医疗子分类 (300-311) - // ============================================ - 300: 'mdi:hospital', // 挂号费 - 301: 'mdi:pill', // 药品 - 302: 'mdi:test-tube', // 检查费 - 303: 'mdi:needle', // 治疗费 - 304: 'mdi:bed-empty', // 住院费 - 305: 'mdi:clipboard-check', // 体检 - 306: 'mdi:tooth', // 牙科 - 307: 'mdi:glasses', // 眼科 - 308: 'mdi:bottle-tonic-plus', // 保健品 - 309: 'mdi:leaf', // 中医 - 310: 'mdi:hand-heart', // 康复理疗 - 311: 'mdi:stethoscope', // 医疗器械 - - // ============================================ - // 教育子分类 (320-329) - // ============================================ - 320: 'mdi:school-outline', // 学费 - 321: 'mdi:book-education', // 培训费 - 322: 'mdi:book-multiple', // 教材 - 323: 'mdi:pencil', // 文具 - 324: 'mdi:monitor', // 在线课程 - 325: 'mdi:file-document-edit', // 考试费 - 326: 'mdi:palette-outline', // 兴趣班 - 327: 'mdi:certificate', // 证书费 - 328: 'mdi:file-document-multiple', // 学习资料 - 329: 'mdi:account-tie', // 辅导费 - - // ============================================ - // 通讯子分类 (340-347) - // ============================================ - 340: 'mdi:cellphone', // 手机话费 - 341: 'mdi:wifi', // 宽带费 - 342: 'mdi:email', // 邮费 - 343: 'mdi:star-circle', // 会员订阅 - 344: 'mdi:television-play', // 视频会员 - 345: 'mdi:music', // 音乐会员 - 346: 'mdi:cloud', // 云存储 - 347: 'mdi:application', // 软件订阅 - - // ============================================ - // 人情往来子分类 (360-368) - // ============================================ - 360: 'mdi:wallet-giftcard', // 红包 - 361: 'mdi:cash', // 礼金 - 362: 'mdi:gift', // 送礼 - 363: 'mdi:silverware-fork-knife', // 请客 - 364: 'mdi:human-male-female', // 孝敬长辈 - 365: 'mdi:ring', // 婚礼 - 366: 'mdi:baby-face', // 满月酒 - 367: 'mdi:cake-variant', // 生日 - 368: 'mdi:party-popper', // 节日 - - // ============================================ - // 金融保险子分类 (380-389) - // ============================================ - 380: 'mdi:bank-transfer', // 银行手续费 - 381: 'mdi:cash-minus', // 利息支出 - 382: 'mdi:shield-account', // 人寿保险 - 383: 'mdi:shield-plus', // 医疗保险 - 384: 'mdi:shield-home', // 财产保险 - 385: 'mdi:chart-line-variant', // 投资亏损 - 386: 'mdi:shield-alert', // 意外险 - 387: 'mdi:shield-check', // 养老保险 - 388: 'mdi:shield-star', // 教育保险 - 389: 'mdi:credit-card', // 信用卡年费 - - // ============================================ - // 美容护理子分类 (400-409) - // ============================================ - 400: 'mdi:content-cut', // 理发 - 401: 'mdi:face-woman-shimmer', // 美容 - 402: 'mdi:lipstick', // 化妆品 - 403: 'mdi:lotion', // 护肤品 - 404: 'mdi:hand-back-right', // 美甲 - 405: 'mdi:spa', // 按摩 - 406: 'mdi:hot-tub', // SPA - 407: 'mdi:eye', // 美睫 - 408: 'mdi:eyebrow', // 纹眉 - 409: 'mdi:yoga', // 美体 - - // ============================================ - // 宠物子分类 (420-426) - // ============================================ - 420: 'mdi:food-drumstick', // 宠物食品 - 421: 'mdi:tennis', // 宠物用品 - 422: 'mdi:hospital-box', // 宠物医疗 - 423: 'mdi:content-cut', // 宠物美容 - 424: 'mdi:home-variant', // 宠物寄养 - 425: 'mdi:school', // 宠物训练 - 426: 'mdi:shield', // 宠物保险 - - // ============================================ - // 慈善捐赠子分类 (440-444) - // ============================================ - 440: 'mdi:hand-heart', // 公益捐款 - 441: 'mdi:hands-pray', // 扶贫助困 - 442: 'mdi:book-heart', // 教育捐赠 - 443: 'mdi:leaf', // 环保公益 - 444: 'mdi:lifebuoy', // 灾害救助 - - // ============================================ - // 收入子分类 - // ============================================ - // 工资薪金 (1001-1008) - 1001: 'mdi:cash', // 基本工资 - 1002: 'mdi:clock-time-eight', // 加班费 - 1003: 'mdi:target', // 绩效奖金 - 1004: 'mdi:cash-plus', // 津贴补贴 - 1005: 'mdi:car', // 交通补贴 - 1006: 'mdi:food', // 餐补 - 1007: 'mdi:phone', // 通讯补贴 - 1008: 'mdi:home', // 住房补贴 - - // 奖金福利 (1020-1025) - 1020: 'mdi:gift', // 年终奖 - 1021: 'mdi:trophy', // 项目奖金 - 1022: 'mdi:briefcase', // 销售提成 - 1023: 'mdi:chart-bar', // 季度奖 - 1024: 'mdi:check-circle', // 全勤奖 - 1025: 'mdi:star', // 优秀员工 - - // 投资收益 (1040-1049) - 1040: 'mdi:chart-line', // 股票收益 - 1041: 'mdi:chart-areaspline', // 基金收益 - 1042: 'mdi:piggy-bank', // 理财收益 - 1043: 'mdi:percent', // 利息收入 - 1044: 'mdi:cash-multiple', // 分红 - 1045: 'mdi:home-city', // 租金收入 - 1046: 'mdi:file-certificate', // 债券收益 - 1047: 'mdi:chart-timeline-variant', // 期货收益 - 1048: 'mdi:currency-usd', // 外汇收益 - 1049: 'mdi:bitcoin', // 数字货币 - - // 兼职副业 (1060-1066) - 1060: 'mdi:briefcase-variant', // 自由职业 - 1061: 'mdi:cash', // 兼职工资 - 1062: 'mdi:pen', // 稿费 - 1063: 'mdi:palette', // 设计费 - 1064: 'mdi:lightbulb', // 咨询费 - 1065: 'mdi:teach', // 讲课费 - 1066: 'mdi:translate', // 翻译费 - - // 经营所得 (1080-1083) - 1080: 'mdi:store', // 营业收入 - 1081: 'mdi:handshake', // 服务收入 - 1082: 'mdi:cart', // 销售收入 - 1083: 'mdi:briefcase', // 佣金收入 - - // 礼金收入 (1100-1102) - 1100: 'mdi:wallet-giftcard', // 红包 - 1101: 'mdi:cash', // 礼金 - 1102: 'mdi:gift', // 压岁钱 - - // 退款返现 (1120-1123) - 1120: 'mdi:undo-variant', // 购物退款 - 1121: 'mdi:credit-card-refund', // 信用卡返现 - 1122: 'mdi:star-circle', // 积分兑换 - 1123: 'mdi:ticket', // 优惠券 - - // 报销补贴 (1140-1143) - 1140: 'mdi:airplane', // 差旅报销 - 1141: 'mdi:hospital-box', // 医疗报销 - 1142: 'mdi:phone', // 通讯报销 - 1143: 'mdi:car', // 交通报销 -}; - -/** - * 分类颜色配置 - * 使用柔和的渐变色系 - */ -export const categoryColorMap: Record = { - // 支出主分类 - 1: '#FF6B6B', // 餐饮 - 珊瑚红 - 2: '#4ECDC4', // 交通 - 青绿色 - 3: '#95E1D3', // 购物 - 薄荷绿 - 4: '#F38181', // 娱乐 - 粉红色 - 5: '#AA96DA', // 居住 - 淡紫色 - 6: '#FCBAD3', // 医疗 - 粉色 - 7: '#FFE66D', // 教育 - 金黄色 - 8: '#A8D8EA', // 通讯 - 天蓝色 - 9: '#FFAAA7', // 人情往来 - 橙粉色 - 10: '#FFD3B5', // 金融保险 - 杏色 - 11: '#FFAAA5', // 美容护理 - 浅粉色 - 12: '#DCEDC1', // 宠物 - 浅绿色 - 13: '#FFD93D', // 慈善捐赠 - 金色 - 14: '#A8E6CF', // 子女教育 - 薄荷绿 - 15: '#FFB6B9', // 老人赡养 - 浅粉红 - 16: '#BAE1FF', // 数码办公 - 浅蓝色 - 17: '#C7CEEA', // 运动健身 - 淡紫色 - 18: '#FFDAC1', // 文化艺术 - 杏色 - 19: '#B5EAD7', // 旅游度假 - 薄荷色 - 20: '#E2F0CB', // 汽车相关 - 浅绿色 - 21: '#C7CEEA', // 其他支出 - 灰紫色 - - // 收入主分类 - 使用绿色系 - 100: '#06D6A0', // 工资薪金 - 翠绿色 - 101: '#118AB2', // 奖金福利 - 蓝色 - 102: '#073B4C', // 投资收益 - 深蓝色 - 103: '#06D6A0', // 兼职副业 - 翠绿色 - 104: '#118AB2', // 经营所得 - 蓝色 - 105: '#FFD166', // 礼金收入 - 金黄色 - 106: '#EF476F', // 退款返现 - 玫红色 - 107: '#06D6A0', // 报销补贴 - 翠绿色 - 108: '#118AB2', // 资产处置 - 蓝色 - 109: '#073B4C', // 其他收入 - 深蓝色 -}; - -/** - * 渐变色预设 - */ -export const gradientPresets: Record = { - food: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', - transport: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', - shopping: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', - entertainment: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', - housing: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)', - medical: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)', - education: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)', - communication: 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)', - social: 'linear-gradient(135deg, #fbc2eb 0%, #a6c1ee 100%)', - finance: 'linear-gradient(135deg, #fdcbf1 0%, #e6dee9 100%)', - beauty: 'linear-gradient(135deg, #fccb90 0%, #d57eeb 100%)', - pet: 'linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%)', - charity: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', - income: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', -}; - -/** - * 深色模式颜色配置 - */ -export const darkModeColors: Record = { - 1: '#FF8787', // 餐饮 - 2: '#5EDDD4', // 交通 - 3: '#A5F1E3', // 购物 - 4: '#F49191', // 娱乐 - 5: '#BAA6EA', // 居住 - 6: '#FDCAE3', // 医疗 - 7: '#FFF67D', // 教育 - 8: '#B8E8FA', // 通讯 - 9: '#FFBAB7', // 人情往来 - 10: '#FFE3C5', // 金融保险 - 11: '#FFBAB5', // 美容护理 - 12: '#ECFDD1', // 宠物 - 13: '#FFE94D', // 慈善捐赠 - 14: '#B8F6DF', // 子女教育 - 15: '#FFC6C9', // 老人赡养 - 16: '#CAF1FF', // 数码办公 - 17: '#D7DEFA', // 运动健身 - 18: '#FFEAD1', // 文化艺术 - 19: '#C5FAE7', // 旅游度假 - 20: '#F2FFDB', // 汽车相关 - 21: '#D7DEFA', // 其他支出 - - // 收入分类 - 100: '#16E6B0', - 101: '#21AABB', - 102: '#174B5C', - 103: '#16E6B0', - 104: '#21AABB', - 105: '#FFE176', - 106: '#FF577F', - 107: '#16E6B0', - 108: '#21AABB', - 109: '#174B5C', -}; - /** * 获取分类图标 + * 如果未提供图标,则返回默认图标 + * @param categoryId - 保留参数接口,虽然不再用于查表 + * @param defaultIcon - 可选的默认图标 */ export const getCategoryIcon = (categoryId: number): string => { - return categoryIconMap[categoryId] || 'mdi:help-circle'; + // 不再维护本地大表,统一返回默认图标 + // 组件应当优先使用 props 传入的 icon + return 'solar:question-circle-bold-duotone'; }; +/** + * 更加丰富的预设颜色池 + */ +const COLOR_PALETTE = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD', + '#D4A5A5', '#9B59B6', '#3498DB', '#E67E22', '#2ECC71', + '#F1C40F', '#E74C3C', '#1ABC9C', '#34495E', '#95A5A6', + '#FF8787', '#5EDDD4', '#A5F1E3', '#F49191', '#BAA6EA', + '#FDCAE3', '#FFF67D', '#B8E8FA', '#FFBAB7', '#FFE3C5', +]; + /** * 获取分类颜色 + * 使用 consistent hashing 算法根据 ID 生成一致的颜色 */ export const getCategoryColor = (categoryId: number, isDarkMode = false): string => { - if (isDarkMode) { - return darkModeColors[categoryId] || '#999'; - } - return categoryColorMap[categoryId] || '#999'; + // 简单的哈希算法确保同一个ID总是对应同一个颜色 + const index = Math.abs(categoryId) % COLOR_PALETTE.length; + return COLOR_PALETTE[index]; }; /** * 获取分类渐变色 + * @deprecated 建议不再使用,或使用 CSS 变量 */ export const getCategoryGradient = (categoryId: number): string => { - // 根据分类ID范围返回对应的渐变色 - if (categoryId >= 1 && categoryId < 10) return gradientPresets.food; - if (categoryId >= 10 && categoryId < 20) return gradientPresets.transport; - if (categoryId >= 100) return gradientPresets.income; - return gradientPresets.food; + const color = getCategoryColor(categoryId); + // 简单模拟一个同色系的渐变 + return `linear-gradient(135deg, ${color} 0%, ${adjustColor(color, -20)} 100%)`; }; + +// 辅助函数:调整颜色亮度 (简单实现) +function adjustColor(color: string, amount: number) { + return color; // 暂不需要复杂计算,直接返回原色 +} + +// 导出空映射以防有遗留引用 +export const categoryIconMap: Record = {}; +export const categoryColorMap: Record = {}; +export const darkModeColors: Record = {}; +export const gradientPresets: Record = {}; diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index b3cb49d..6979ec8 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -237,15 +237,30 @@ function Home() { const { greeting, insight } = getDailyBriefing(); - // Phase 3: Financial Health Score (Mock Logic) - // In a real app, this would be complex. Here we use a simple placeholder derived from net worth/assets ratio + // Phase 3: Financial Health Score (Enhanced Logic) const calculateHealthScore = () => { - if (totalAssets === 0) return 60; // Baseline - const ratio = (totalAssets - totalLiabilities) / totalAssets; - let score = Math.round(ratio * 100); - if (score < 40) score = 40; - if (score > 98) score = 98; - return score; + // 1. Solvency Score (70% weight): Net Worth / Total Assets + // If no assets, assume baseline 60 if no debt, else lower + if (totalAssets === 0) return totalLiabilities > 0 ? 40 : 60; + + const solvencyRatio = (totalAssets - totalLiabilities) / totalAssets; // 1.0 = perfect, 0.0 = bankrupt + + // 2. Streak Bonus (10% weight): Encourages habit + const streakBonus = (streakInfo?.currentStreak || 0) > 3 ? 5 : 0; + + // 3. Activity Bonus (20% weight): Scored if todaySpend > 0 or yesterdaySpend > 0 (active user) + const activityBonus = (todaySpend > 0 || yesterdaySpend > 0) ? 5 : 0; + + // Weights: Base (might be high) -> normalized + // Strategy: Map 0-1 solvency to 40-90 range, then add bonuses + // Solvency 1.0 -> 90 + // Solvency 0.5 -> 65 + // Solvency 0.0 -> 40 + let finalScore = 40 + (solvencyRatio * 50); + + finalScore += streakBonus + activityBonus; + + return Math.min(Math.max(Math.round(finalScore), 40), 99); }; const healthScore = calculateHealthScore(); diff --git a/src/pages/Reports/Reports.tsx b/src/pages/Reports/Reports.tsx index 50a256c..058baab 100644 --- a/src/pages/Reports/Reports.tsx +++ b/src/pages/Reports/Reports.tsx @@ -31,15 +31,26 @@ function Reports() { const headerOpacity = useTransform(scrollY, [0, 60], [1, 0.6]); const headerY = useTransform(scrollY, [0, 60], [0, 10]); + // Helper to format date as YYYY-MM-DD in local time + const getLocalTodayDate = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + }; + + // Helper to format date as YYYY-MM-01 in local time + const getLocalMonthStartDate = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}-${month}-01`; + }; + // Date range state - const [startDate, setStartDate] = useState(() => { - const date = new Date(); - date.setDate(1); // First day of current month - return date.toISOString().split('T')[0]; - }); - const [endDate, setEndDate] = useState(() => { - return new Date().toISOString().split('T')[0]; - }); + const [startDate, setStartDate] = useState(getLocalMonthStartDate); + const [endDate, setEndDate] = useState(getLocalTodayDate); // Data state const [summary, setSummary] = useState(null); diff --git a/src/services/aiService.ts b/src/services/aiService.ts index 0577c89..c5fb7cd 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -61,30 +61,37 @@ function toCamelCase(obj: Record): Record { * Send a chat message for AI processing * Validates: Requirements 12.1, 12.5 */ -export async function sendChatMessage(message: string, existingSessionId?: string): Promise { +/** + * Send a chat message for AI processing + * Validates: Requirements 12.1, 12.5 + */ +export async function sendChatMessage( + message: string, + existingSessionId?: string +): Promise { const sessionId = existingSessionId || getSessionId(); - + const response = await api.post>>('/ai/chat', { session_id: sessionId, message, }); - + if (!response.data) { throw new Error(response.error || 'Failed to process chat message'); } - + console.log('[aiService] Raw response data:', response.data); - + // Convert snake_case to camelCase const data = toCamelCase(response.data) as unknown as AIChatResponse; - + console.log('[aiService] Converted response data:', data); - + // Update session ID if server provides a new one if (data.sessionId) { currentSessionId = data.sessionId; } - + return data; } @@ -95,16 +102,16 @@ export async function sendChatMessage(message: string, existingSessionId?: strin export async function transcribeAudio(audioBlob: Blob): Promise { const formData = new FormData(); formData.append('audio', audioBlob, 'recording.webm'); - + const response = await api.upload>( '/ai/transcribe', formData ); - + if (!response.data) { throw new Error(response.error || 'Failed to transcribe audio'); } - + return response.data; } @@ -123,14 +130,14 @@ export async function confirmTransaction(card: ConfirmationCard): Promise { export async function processVoiceInput(audioBlob: Blob): Promise { // Step 1: Transcribe audio const transcription = await transcribeAudio(audioBlob); - + if (!transcription.text || transcription.text.trim() === '') { throw new Error('Could not transcribe audio. Please try again.'); } - + // Step 2: Send transcribed text to chat return sendChatMessage(transcription.text); } @@ -174,7 +181,7 @@ export async function processVoiceInput(audioBlob: Blob): Promise 0 && card.categoryId > 0 && @@ -190,16 +197,100 @@ export function isConfirmationCardComplete(card: ConfirmationCard | undefined): export function formatConfirmationCard(card: ConfirmationCard): string { const typeLabel = card.type === 'expense' ? '支出' : '收入'; const amountStr = card.amount.toFixed(2); - + let result = `${card.category} / ${typeLabel} / ¥${amountStr} / ${card.account}`; - + if (card.note) { result += ` / ${card.note}`; } - + return result; } +/** + * Get AI financial advice based on user status + * Validates: Requirement "Real LLM Integration" + */ +// Cache storage for advice +let adviceCache: { + data: string; + timestamp: number; + contentHash: string; +} | null = null; + +/** + * Get AI financial advice based on user status + * Validates: Requirement "Real LLM Integration" + * Implements: Caching Strategy (5min TTL + Content Hash) + */ +export async function getFinancialAdvice(context: { + totalAssets: number; + totalLiabilities: number; + score: number; + todaySpend: number; + yesterdaySpend: number; +}): Promise { + // Generate a simple hash of the context to detect data changes + const currentHash = JSON.stringify(context); + const NOW = Date.now(); + const CACHE_TTL = 5 * 60 * 1000; // 5 Minutes + + // 1. Check Cache + if (adviceCache && + (NOW - adviceCache.timestamp < CACHE_TTL) && + adviceCache.contentHash === currentHash) { + console.log('[aiService] Returning cached financial advice'); + return adviceCache.data; + } + + // Construct a prompt for the AI + const prompt = `System: 你是全能的首席财务官 (CFO) 兼个人财富导师。 +Context: 用户希望获得深度、犀利且具有前瞻性的财务洞察。不要说废话,直击痛点或爽点。 + +User Financial Data: +- 综合评分: ${context.score} (S级标准: 90+, 警戒线: 60) +- 净资产: ¥${(context.totalAssets - context.totalLiabilities).toFixed(2)} +- 资产/负债: ¥${context.totalAssets.toFixed(2)} / ¥${context.totalLiabilities.toFixed(2)} +- 负债杠杆: ${context.totalAssets > 0 ? ((context.totalLiabilities / context.totalAssets) * 100).toFixed(1) : 0}% +- 短期收支: 今日支出 ¥${context.todaySpend.toFixed(2)} (对比昨日: ¥${context.yesterdaySpend.toFixed(2)}) + +Instruction: +请根据上述数据,运用"第一性原理"分析用户的核心财务健康度。 +1. 如果负债率过高(>30%),给出一条关于"债务雪崩法"或"债务雪球法"的具体行动建议。 +2. 如果资产健康但支出波动大,提示"拿铁因子"风险。 +3. 如果状态完美,建议关注"被动收入"或"抗通胀"配置。 + +Output Requirements: +- 限制字数: 120字以内。 +- 风格: 睿智、冷静、一针见血。 +- 格式: 纯文本,适当使用Emoji (💡, 🚀, 🛡️) 作为视觉锚点。`; + + try { + // 2. Request from AI + const response = await sendChatMessage(prompt); + + if (response.message) { + const advice = response.message; + + // 3. Update Cache + adviceCache = { + data: advice, + timestamp: NOW, + contentHash: currentHash + }; + + return advice; + } + throw new Error('No advice received'); + } catch (error) { + console.error('Failed to get AI advice:', error); + // If request fails but we have stale cache (even if expired/mismatched), + // we might consider returning it or just throw error to trigger fallback. + // Here we choose to throw so fallback static text appears. + throw error; + } +} + export default { getSessionId, clearSession, @@ -210,4 +301,5 @@ export default { processVoiceInput, isConfirmationCardComplete, formatConfirmationCard, + getFinancialAdvice, };