Files
Novault-Frontend-web/src/pages/Settings/Settings.tsx

549 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { useTheme } from '../../hooks/useTheme';
import BackupManager from '../../components/settings/BackupManager';
import AppLockSettings from '../../components/settings/AppLockSettings';
import ChangePassword from '../../components/settings/ChangePassword';
import { CapsuleSelector } from '../../components/common/CapsuleSelector/CapsuleSelector';
import { CustomSelect } from '../../components/common/CustomSelect/CustomSelect';
import { getSupportedCurrencies } from '../../services/exchangeRateService';
import { updateSettings, updateDefaultAccounts, getSettingsWithDefaults } from '../../services/settingsService';
import { getAccounts } from '../../services/accountService';
import type { UserSettings, UserSettingsWithDefaults, Account } from '../../types';
import { Icon } from '@iconify/react';
import { ThemePicker } from '../../components/settings/ThemePicker/ThemePicker';
import './Settings.css';
type SettingsTab = 'general' | 'security' | 'data' | 'about';
interface GeneralSettings {
defaultCurrency: string;
language: string;
theme: 'light' | 'dark' | 'system';
accentColor: string;
dateFormat: string;
firstDayOfWeek: number;
}
/**
* Settings Page Component
* Application settings and preferences
*/
function Settings() {
const { themeMode, setThemeMode } = useTheme();
const [activeTab, setActiveTab] = useState<SettingsTab>('general');
const [settings, setSettings] = useState<GeneralSettings>({
defaultCurrency: localStorage.getItem('defaultCurrency') || 'CNY',
language: localStorage.getItem('language') || 'zh-CN',
theme: 'system', // Placeholder, overridden by themeMode
accentColor: localStorage.getItem('accentColor') || 'amber',
dateFormat: localStorage.getItem('dateFormat') || 'YYYY-MM-DD',
firstDayOfWeek: parseInt(localStorage.getItem('firstDayOfWeek') || '1'),
});
const [userSettings, setUserSettings] = useState<UserSettingsWithDefaults | null>(null);
const [accounts, setAccounts] = useState<Account[]>([]);
const [saveMessage, setSaveMessage] = useState<string>('');
const currencies = getSupportedCurrencies();
useEffect(() => {
// Load user settings
loadUserSettings();
loadAccounts();
// Sync settings state with global theme mode
setSettings(prev => ({ ...prev, theme: themeMode }));
}, [themeMode]);
const loadUserSettings = async () => {
try {
const data = await getSettingsWithDefaults();
setUserSettings(data);
} catch (err) {
console.error('Failed to load user settings:', err);
}
};
const loadAccounts = async () => {
try {
const data = await getAccounts();
setAccounts(data);
} catch (err) {
console.error('Failed to load accounts:', err);
}
};
const handleUserSettingChange = async (key: keyof UserSettings, value: any) => {
if (!userSettings) return;
try {
const newSettings = { ...userSettings, [key]: value };
await updateSettings(newSettings);
setUserSettings(newSettings);
setSaveMessage('设置已保存');
setTimeout(() => setSaveMessage(''), 2000);
} catch (err) {
console.error('Failed to update settings:', err);
setSaveMessage('保存失败');
setTimeout(() => setSaveMessage(''), 2000);
}
};
const handleDefaultAccountChange = async (key: 'defaultExpenseAccountId' | 'defaultIncomeAccountId', value: number) => {
if (!userSettings) return;
try {
const updateData = {
[key]: value
};
await updateDefaultAccounts(updateData);
// Update local state
let updatedSettings = { ...userSettings };
if (key === 'defaultExpenseAccountId') {
updatedSettings.defaultExpenseAccountId = value;
// Update the related object for immediate UI feedback if needed
const account = accounts.find(a => a.id === value);
if (account) updatedSettings.defaultExpenseAccount = account;
} else {
updatedSettings.defaultIncomeAccountId = value;
const account = accounts.find(a => a.id === value);
if (account) updatedSettings.defaultIncomeAccount = account;
}
setUserSettings(updatedSettings);
setSaveMessage('默认账户已更新');
setTimeout(() => setSaveMessage(''), 2000);
} catch (err) {
console.error('Failed to update default account:', err);
setSaveMessage('更新失败');
setTimeout(() => setSaveMessage(''), 2000);
}
};
const handleSettingChange = (key: keyof GeneralSettings, value: string | number) => {
const newSettings = { ...settings, [key]: value };
setSettings(newSettings);
localStorage.setItem(key, value.toString());
if (key === 'theme') {
setThemeMode(value as any);
}
setSaveMessage('设置已保存');
setTimeout(() => setSaveMessage(''), 2000);
};
const handleClearCache = () => {
if (window.confirm('确定要清除所有缓存数据吗?这不会删除您的账户数据。')) {
// Clear all cache except auth tokens
const keysToKeep = ['authToken', 'refreshToken'];
const allKeys = Object.keys(localStorage);
allKeys.forEach(key => {
if (!keysToKeep.includes(key)) {
localStorage.removeItem(key);
}
});
alert('缓存已清除');
window.location.reload();
}
};
return (
<div className="settings-page">
<header className="settings-header">
<h1></h1>
<p className="settings-subtitle"></p>
</header>
<nav className="settings-tabs">
<button
className={`tab-button ${activeTab === 'general' ? 'active' : ''}`}
onClick={() => setActiveTab('general')}
>
<span className="tab-icon"><Icon icon="solar:settings-bold-duotone" width="20" /></span>
<span className="tab-label"></span>
</button>
<button
className={`tab-button ${activeTab === 'security' ? 'active' : ''}`}
onClick={() => setActiveTab('security')}
>
<span className="tab-icon"><Icon icon="solar:shield-keyhole-bold-duotone" width="20" /></span>
<span className="tab-label"></span>
</button>
<button
className={`tab-button ${activeTab === 'data' ? 'active' : ''}`}
onClick={() => setActiveTab('data')}
>
<span className="tab-icon"><Icon icon="solar:database-bold-duotone" width="20" /></span>
<span className="tab-label"></span>
</button>
<button
className={`tab-button ${activeTab === 'about' ? 'active' : ''}`}
onClick={() => setActiveTab('about')}
>
<span className="tab-icon"><Icon icon="solar:info-circle-bold-duotone" width="20" /></span>
<span className="tab-label"></span>
</button>
</nav>
{saveMessage && (
<div className="save-message">
{saveMessage}
</div>
)}
<main className="settings-content">
{activeTab === 'general' && (
<section className="general-settings">
<div className="settings-section">
<h2></h2>
<div className="settings-group">
<h3></h3>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<CustomSelect
className="settings-custom-select"
value={settings.defaultCurrency}
onChange={(value) => handleSettingChange('defaultCurrency', value)}
options={currencies.map(currency => ({
value: currency.code,
label: `${currency.symbol} ${currency.name} (${currency.code})`
}))}
/>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<CustomSelect
className="settings-custom-select"
value={themeMode}
onChange={(value) => handleSettingChange('theme', value)}
options={[
{ value: 'system', label: '跟随系统' },
{ value: 'light', label: '浅色' },
{ value: 'dark', label: '深色' },
]}
/>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<div className="theme-color-selector">
<ThemePicker />
</div>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<CustomSelect
className="settings-custom-select"
value={settings.language}
onChange={(value) => handleSettingChange('language', value)}
options={[
{ value: 'zh-CN', label: '简体中文' },
{ value: 'zh-TW', label: '繁體中文' },
{ value: 'en-US', label: 'English' },
]}
/>
</div>
</div>
<div className="settings-group">
<h3></h3>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<CustomSelect
className="settings-custom-select"
value={settings.dateFormat}
onChange={(value) => handleSettingChange('dateFormat', value)}
options={[
{ value: 'YYYY-MM-DD', label: '2024-01-22' },
{ value: 'DD/MM/YYYY', label: '22/01/2024' },
{ value: 'MM/DD/YYYY', label: '01/22/2024' },
{ value: 'YYYY年MM月DD日', label: '2024年01月22日' },
]}
/>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<CustomSelect
className="settings-custom-select"
value={settings.firstDayOfWeek}
onChange={(value) => handleSettingChange('firstDayOfWeek', value)}
options={[
{ value: 0, label: '星期日' },
{ value: 1, label: '星期一' },
]}
/>
</div>
</div>
<div className="settings-group">
<h3></h3>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<button onClick={handleClearCache} className="settings-btn settings-btn-secondary">
</button>
</div>
</div>
{/* Requirements 5.4, 6.5, 8.25-8.27 */}
<div className="settings-group">
<h3></h3>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description">AI </p>
</div>
<CustomSelect
className="settings-custom-select"
value={userSettings?.defaultExpenseAccountId || ''}
onChange={(value) => handleDefaultAccountChange('defaultExpenseAccountId', Number(value))}
options={[
{ value: '', label: '不设置' },
...accounts
.filter(a => a.type !== 'credit_card' && a.type !== 'credit_line' ? true : true) // Show all for now, maybe filter logic later
.map(account => ({
value: account.id,
label: account.name
}))
]}
/>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<CustomSelect
className="settings-custom-select"
value={userSettings?.defaultIncomeAccountId || ''}
onChange={(value) => handleDefaultAccountChange('defaultIncomeAccountId', Number(value))}
options={[
{ value: '', label: '不设置' },
...accounts
//.filter(a => a.accountType === 'asset') // Typically income goes to assets
.map(account => ({
value: account.id,
label: account.name
}))
]}
/>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<label className="settings-toggle">
<input
type="checkbox"
checked={userSettings?.preciseTimeEnabled ?? true}
onChange={(e) => handleUserSettingChange('preciseTimeEnabled', e.target.checked)}
/>
<span className="settings-toggle-slider"></span>
</label>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<CapsuleSelector
options={[
{ value: 'four', label: '四列' },
{ value: 'five', label: '五列' },
{ value: 'six', label: '六列' },
]}
value={userSettings?.iconLayout || 'five'}
onChange={(value) => handleUserSettingChange('iconLayout', value)}
/>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<CapsuleSelector
options={[
{ value: 'low', label: '标清' },
{ value: 'medium', label: '高清' },
{ value: 'high', label: '原画' },
]}
value={userSettings?.imageCompression || 'medium'}
onChange={(value) => handleUserSettingChange('imageCompression', value)}
/>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<label className="settings-toggle">
<input
type="checkbox"
checked={userSettings?.showReimbursementBtn ?? true}
onChange={(e) => handleUserSettingChange('showReimbursementBtn', e.target.checked)}
/>
<span className="settings-toggle-slider"></span>
</label>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label">退</label>
<p className="settings-item__description">退</p>
</div>
<label className="settings-toggle">
<input
type="checkbox"
checked={userSettings?.showRefundBtn ?? true}
onChange={(e) => handleUserSettingChange('showRefundBtn', e.target.checked)}
/>
<span className="settings-toggle-slider"></span>
</label>
</div>
<div className="settings-item">
<div className="settings-item-info">
<label className="settings-item__label"></label>
<p className="settings-item__description"></p>
</div>
<div className="settings-input-container">
<input
type="tel"
className="settings-text-input"
placeholder="请输入手机号"
value={userSettings?.phone || ''}
onChange={(e) => handleUserSettingChange('phone', e.target.value)}
/>
</div>
</div>
</div>
</div>
</section>
)}
{activeTab === 'security' && (
<section className="security-settings">
<ChangePassword />
<div style={{ marginTop: '2rem' }}></div>
<AppLockSettings />
</section>
)}
{activeTab === 'data' && (
<section className="data-settings">
<BackupManager />
</section>
)}
{activeTab === 'about' && (
<section className="about-settings">
<div className="settings-section">
<h2></h2>
<div className="about-content">
<div className="app-logo">
<div className="logo-icon">
<Icon icon="solar:wallet-money-bold-duotone" width="48" className="text-primary" />
</div>
<h3></h3>
<p className="version"> 1.0.0</p>
</div>
<div className="about-info">
<h4></h4>
<ul className="feature-list">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> 37</li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="about-info">
<h4></h4>
<div className="tech-stack">
<span className="tech-badge">React</span>
<span className="tech-badge">TypeScript</span>
<span className="tech-badge">Go</span>
<span className="tech-badge">PostgreSQL</span>
<span className="tech-badge">Redis</span>
</div>
</div>
<div className="about-info">
<h4></h4>
<dl className="system-info">
<dt>:</dt>
<dd>{navigator.userAgent.split(' ').slice(-2).join(' ')}</dd>
<dt>:</dt>
<dd>{navigator.platform}</dd>
<dt>:</dt>
<dd>{navigator.language}</dd>
</dl>
</div>
<div className="about-footer">
<p>© 2024 . All rights reserved.</p>
<div className="about-links">
<a href="#" onClick={(e) => e.preventDefault()}>使</a>
<span>·</span>
<a href="#" onClick={(e) => e.preventDefault()}></a>
<span>·</span>
<a href="#" onClick={(e) => e.preventDefault()}></a>
</div>
</div>
</div>
</div>
</section>
)}
</main>
</div>
);
}
export default Settings;