5278190750
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务 - 后端: 成本分析服务、AI问答服务 - 后端: 科目映射CRUD API、分析API、QA API - 后端: 集成测试(认证/任务/凭证) 49个测试全部通过 - 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面 - 前端: AuthGuard认证守卫、Dashboard AI聊天功能 - 前端: Playwright E2E测试 16 passed, 1 skipped - 基础设施: Docker Compose、Nginx反向代理、.env.example - 文档: 用户手册、管理员手册、发布检查清单
389 lines
16 KiB
TypeScript
389 lines
16 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { motion } from "motion/react";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import {
|
||
Settings,
|
||
User,
|
||
Building2,
|
||
Key,
|
||
Bell,
|
||
Palette,
|
||
Save,
|
||
CheckCircle,
|
||
} from "lucide-react";
|
||
|
||
const settingsTabs = [
|
||
{ key: "profile", label: "个人资料", icon: User },
|
||
{ key: "company", label: "企业信息", icon: Building2 },
|
||
{ key: "ai", label: "AI 配置", icon: Key },
|
||
{ key: "notifications", label: "通知设置", icon: Bell },
|
||
{ key: "appearance", label: "外观偏好", icon: Palette },
|
||
];
|
||
|
||
export default function SettingsPage() {
|
||
const [activeTab, setActiveTab] = useState("profile");
|
||
const [saving, setSaving] = useState(false);
|
||
const [saved, setSaved] = useState(false);
|
||
|
||
const [profile, setProfile] = useState({
|
||
name: "",
|
||
email: "",
|
||
phone: "",
|
||
});
|
||
|
||
const [company, setCompany] = useState({
|
||
name: "",
|
||
tax_id: "",
|
||
contact: "",
|
||
plan: "standard",
|
||
});
|
||
|
||
const [aiConfig, setAiConfig] = useState({
|
||
provider: "zhipu",
|
||
model: "glm-4",
|
||
temperature: "0.3",
|
||
max_tokens: "2000",
|
||
});
|
||
|
||
const [notifications, setNotifications] = useState({
|
||
email_alert: true,
|
||
exception_alert: true,
|
||
weekly_report: false,
|
||
cost_threshold: "10000",
|
||
});
|
||
|
||
const [appearance, setAppearance] = useState({
|
||
theme: "system",
|
||
density: "comfortable",
|
||
language: "zh-CN",
|
||
});
|
||
|
||
function handleSave() {
|
||
setSaving(true);
|
||
setTimeout(() => {
|
||
setSaving(false);
|
||
setSaved(true);
|
||
setTimeout(() => setSaved(false), 2000);
|
||
}, 800);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-full p-6 lg:p-8 max-w-7xl mx-auto space-y-6">
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
||
>
|
||
<h1 className="text-heading-1 text-foreground">系统设置</h1>
|
||
<p className="text-muted-foreground mt-1">管理个人资料、企业信息和系统配置</p>
|
||
</motion.div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||
{/* Settings Nav */}
|
||
<motion.div
|
||
initial={{ opacity: 0, x: -8 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
<Card>
|
||
<CardContent className="p-3">
|
||
<nav className="space-y-1">
|
||
{settingsTabs.map((tab) => {
|
||
const Icon = tab.icon;
|
||
return (
|
||
<button
|
||
key={tab.key}
|
||
onClick={() => setActiveTab(tab.key)}
|
||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all text-sm font-medium ${
|
||
activeTab === tab.key
|
||
? "bg-primary/10 text-primary"
|
||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||
}`}
|
||
>
|
||
<Icon className="w-4 h-4 shrink-0" />
|
||
{tab.label}
|
||
</button>
|
||
);
|
||
})}
|
||
</nav>
|
||
</CardContent>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
{/* Settings Content */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.4, delay: 0.1 }}
|
||
className="lg:col-span-3"
|
||
>
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-lg font-medium">
|
||
{settingsTabs.find((t) => t.key === activeTab)?.label}
|
||
</CardTitle>
|
||
<Button onClick={handleSave} disabled={saving} className="btn-press" size="sm">
|
||
{saving ? (
|
||
<span className="flex items-center gap-2">
|
||
<svg className="animate-spin w-4 h-4" viewBox="0 0 24 24">
|
||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||
</svg>
|
||
保存中
|
||
</span>
|
||
) : saved ? (
|
||
<span className="flex items-center gap-2">
|
||
<CheckCircle className="w-4 h-4" />
|
||
已保存
|
||
</span>
|
||
) : (
|
||
<span className="flex items-center gap-2">
|
||
<Save className="w-4 h-4" />
|
||
保存
|
||
</span>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
{activeTab === "profile" && (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">姓名</label>
|
||
<Input
|
||
value={profile.name}
|
||
onChange={(e) => setProfile({ ...profile, name: e.target.value })}
|
||
placeholder="请输入姓名"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">邮箱</label>
|
||
<Input
|
||
type="email"
|
||
value={profile.email}
|
||
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
|
||
placeholder="user@example.com"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">手机号</label>
|
||
<Input
|
||
value={profile.phone}
|
||
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
|
||
placeholder="138****1234"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "company" && (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">企业名称</label>
|
||
<Input
|
||
value={company.name}
|
||
onChange={(e) => setCompany({ ...company, name: e.target.value })}
|
||
placeholder="请输入企业名称"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">税号</label>
|
||
<Input
|
||
value={company.tax_id}
|
||
onChange={(e) => setCompany({ ...company, tax_id: e.target.value })}
|
||
placeholder="请输入统一社会信用代码"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">联系人</label>
|
||
<Input
|
||
value={company.contact}
|
||
onChange={(e) => setCompany({ ...company, contact: e.target.value })}
|
||
placeholder="请输入联系人姓名"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">套餐类型</label>
|
||
<Select value={company.plan} onValueChange={(v) => setCompany({ ...company, plan: v })}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="free">免费版</SelectItem>
|
||
<SelectItem value="standard">标准版</SelectItem>
|
||
<SelectItem value="professional">专业版</SelectItem>
|
||
<SelectItem value="enterprise">企业版</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "ai" && (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">AI 服务商</label>
|
||
<Select value={aiConfig.provider} onValueChange={(v) => setAiConfig({ ...aiConfig, provider: v })}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="zhipu">智谱 AI (GLM-4)</SelectItem>
|
||
<SelectItem value="openai">OpenAI (GPT-4)</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">模型</label>
|
||
<Input
|
||
value={aiConfig.model}
|
||
onChange={(e) => setAiConfig({ ...aiConfig, model: e.target.value })}
|
||
placeholder="glm-4 / gpt-4-turbo-preview"
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">Temperature</label>
|
||
<Input
|
||
value={aiConfig.temperature}
|
||
onChange={(e) => setAiConfig({ ...aiConfig, temperature: e.target.value })}
|
||
placeholder="0.3"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">Max Tokens</label>
|
||
<Input
|
||
value={aiConfig.max_tokens}
|
||
onChange={(e) => setAiConfig({ ...aiConfig, max_tokens: e.target.value })}
|
||
placeholder="2000"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="p-4 rounded-lg bg-amber-50 dark:bg-amber-950/30">
|
||
<p className="text-sm text-amber-700 dark:text-amber-400">
|
||
API Key 在 .env 文件中配置,不建议在此页面修改。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "notifications" && (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between p-4 rounded-lg border border-border">
|
||
<div>
|
||
<p className="font-medium text-foreground">邮件通知</p>
|
||
<p className="text-caption text-muted-foreground">接收任务完成、异常提醒等邮件</p>
|
||
</div>
|
||
<Button
|
||
variant={notifications.email_alert ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setNotifications({ ...notifications, email_alert: !notifications.email_alert })}
|
||
>
|
||
{notifications.email_alert ? "已开启" : "已关闭"}
|
||
</Button>
|
||
</div>
|
||
<div className="flex items-center justify-between p-4 rounded-lg border border-border">
|
||
<div>
|
||
<p className="font-medium text-foreground">异常提醒</p>
|
||
<p className="text-caption text-muted-foreground">检测到异常时发送通知</p>
|
||
</div>
|
||
<Button
|
||
variant={notifications.exception_alert ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setNotifications({ ...notifications, exception_alert: !notifications.exception_alert })}
|
||
>
|
||
{notifications.exception_alert ? "已开启" : "已关闭"}
|
||
</Button>
|
||
</div>
|
||
<div className="flex items-center justify-between p-4 rounded-lg border border-border">
|
||
<div>
|
||
<p className="font-medium text-foreground">周报</p>
|
||
<p className="text-caption text-muted-foreground">每周一发送成本分析周报</p>
|
||
</div>
|
||
<Button
|
||
variant={notifications.weekly_report ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setNotifications({ ...notifications, weekly_report: !notifications.weekly_report })}
|
||
>
|
||
{notifications.weekly_report ? "已开启" : "已关闭"}
|
||
</Button>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">成本预警阈值(元)</label>
|
||
<Input
|
||
value={notifications.cost_threshold}
|
||
onChange={(e) => setNotifications({ ...notifications, cost_threshold: e.target.value })}
|
||
placeholder="10000"
|
||
/>
|
||
<p className="text-caption text-muted-foreground mt-1">当月成本超过此值时发送预警</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "appearance" && (
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">主题</label>
|
||
<Select value={appearance.theme} onValueChange={(v) => setAppearance({ ...appearance, theme: v })}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="light">浅色</SelectItem>
|
||
<SelectItem value="dark">深色</SelectItem>
|
||
<SelectItem value="system">跟随系统</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">界面密度</label>
|
||
<Select value={appearance.density} onValueChange={(v) => setAppearance({ ...appearance, density: v })}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="comfortable">舒适</SelectItem>
|
||
<SelectItem value="compact">紧凑</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<label className="text-caption text-muted-foreground mb-1.5 block">语言</label>
|
||
<Select value={appearance.language} onValueChange={(v) => setAppearance({ ...appearance, language: v })}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="zh-CN">简体中文</SelectItem>
|
||
<SelectItem value="en">English</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|