Files
selfrelease 5278190750 feat: 凭证生成、成本分析、AI问答、前端页面、集成测试与E2E测试
- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务
- 后端: 成本分析服务、AI问答服务
- 后端: 科目映射CRUD API、分析API、QA API
- 后端: 集成测试(认证/任务/凭证) 49个测试全部通过
- 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面
- 前端: AuthGuard认证守卫、Dashboard AI聊天功能
- 前端: Playwright E2E测试 16 passed, 1 skipped
- 基础设施: Docker Compose、Nginx反向代理、.env.example
- 文档: 用户手册、管理员手册、发布检查清单
2026-07-07 21:21:29 +08:00

389 lines
16 KiB
TypeScript
Raw Permalink 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.
"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>
);
}