Initial commit: accountbook project
- Node.js backend server - Frontend application - Backup script - Project specification Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { isLoggedIn, exportData, importData, logout as apiLogout } from '../lib/api';
|
||||
import AppShell from '../components/AppShell';
|
||||
import { Icon } from '../components/icons';
|
||||
import { useToast } from '../components/ToastProvider';
|
||||
import { useConfirm } from '../components/ConfirmProvider';
|
||||
|
||||
export default function Settings() {
|
||||
const router = useRouter();
|
||||
const fileInputRef = useRef(null);
|
||||
const { showToast } = useToast();
|
||||
const { confirm } = useConfirm();
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [loggingOut, setLoggingOut] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn()) {
|
||||
router.push('/login');
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
const shouldLogout = await confirm({
|
||||
title: '退出当前账号?',
|
||||
description: '退出后需要重新登录才能继续使用。',
|
||||
confirmText: '退出登录',
|
||||
tone: 'info',
|
||||
});
|
||||
if (!shouldLogout) return;
|
||||
|
||||
setLoggingOut(true);
|
||||
apiLogout();
|
||||
showToast('已退出登录', { tone: 'success' });
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const data = await exportData();
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `accountbook_backup_${new Date().toISOString().split('T')[0]}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('导出成功', { tone: 'success' });
|
||||
} catch (err) {
|
||||
showToast(`导出失败: ${err.message}`, { tone: 'error' });
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setImporting(true);
|
||||
try {
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
await importData(data);
|
||||
showToast('导入成功', { tone: 'success' });
|
||||
e.target.value = '';
|
||||
} catch (err) {
|
||||
showToast(`导入失败: ${err.message}`, { tone: 'error' });
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppShell title="设置" subtitle="数据、账号与应用信息" compactHeader>
|
||||
<section className="card settings-section settings-section--refresh">
|
||||
<div className="section-heading section-heading--compact">
|
||||
<div>
|
||||
<p className="eyebrow">数据管理</p>
|
||||
<h2>导入与导出</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-item">
|
||||
<div className="settings-info">
|
||||
<div className="settings-title">导出数据</div>
|
||||
<div className="settings-desc">将所有账目导出为 JSON 文件,方便备份。</div>
|
||||
</div>
|
||||
<button type="button" className="btn btn-primary btn-inline" onClick={handleExport} disabled={exporting || importing}>
|
||||
<span className="button-content">
|
||||
<Icon name="download" size={16} />
|
||||
{exporting ? '导出中...' : '导出'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="settings-item">
|
||||
<div className="settings-info">
|
||||
<div className="settings-title">导入数据</div>
|
||||
<div className="settings-desc">从 JSON 文件恢复账目数据。</div>
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary btn-inline" onClick={() => fileInputRef.current?.click()} disabled={exporting || importing}>
|
||||
<span className="button-content">
|
||||
<Icon name="upload" size={16} />
|
||||
{importing ? '导入中...' : '导入'}
|
||||
</span>
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleImport}
|
||||
accept=".json"
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card settings-section settings-section--refresh">
|
||||
<div className="section-heading section-heading--compact">
|
||||
<div>
|
||||
<p className="eyebrow">账号</p>
|
||||
<h2>登录状态</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-item settings-item--stacked">
|
||||
<div className="settings-info">
|
||||
<div className="settings-title">退出登录</div>
|
||||
<div className="settings-desc">从当前设备安全退出,并返回登录页。</div>
|
||||
</div>
|
||||
<button type="button" className="btn btn-danger btn-inline" onClick={handleLogout} disabled={loggingOut}>
|
||||
<span className="button-content">
|
||||
<Icon name="logout" size={16} />
|
||||
{loggingOut ? '退出中...' : '退出登录'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card settings-section settings-section--refresh">
|
||||
<div className="section-heading section-heading--compact">
|
||||
<div>
|
||||
<p className="eyebrow">关于</p>
|
||||
<h2>应用信息</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="about-info">
|
||||
<p>简易记账本 v1.0</p>
|
||||
<p className="sub">基于 React + Next.js 开发</p>
|
||||
</div>
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user