1 line
89 KiB
JSON
1 line
89 KiB
JSON
{"repo": "19125119617/ai-customer-service", "files": {"src/app/api/ai/chat/route.ts": {"db4291e5eaaede2c19cffd573e01d953bafb54e5": {"t": 1785746034.3805614, "n": 3100, "h": "5ebe05ef50393fcf", "c": "// AI 聊天 API\nimport { NextRequest, NextResponse } from 'next/server';\nimport { ragQuery } from '@/lib/rag';\nimport prisma from '@/lib/prisma';\n\n// AI 提供商配置\nconst AI_PROVIDERS = {\n deepseek: {\n baseURL: 'https://api.deepseek.com/v1',\n model: 'deepseek-chat',\n },\n qwen: {\n baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n model: 'qwen-turbo',\n },\n moonshot: {\n baseURL: 'https://api.moonshot.cn/v1',\n model: 'moonshot-v1-8k',\n },\n zhipu: {\n baseURL: 'https://open.bigmodel.cn/api/paas/v4',\n model: 'glm-4-flash',\n },\n};\n\n// 调用 AI 接口\nasync function callAI(prompt: string, provider: keyof typeof AI_PROVIDERS = 'deepseek', apiKey?: string) {\n const config = AI_PROVIDERS[provider];\n const key = apiKey || process.env.AI_API_KEY;\n\n if (!key) {\n throw new Error('未配置 API Key');\n }\n\n const response = await fetch(`${config.baseURL}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: config.model,\n messages: [\n { role: 'system', content: '你是一个专业的客服助手,请用友好的语气回答用户的问题。' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.7,\n max_tokens: 1000,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`AI 调用失败: ${error}`);\n }\n\n const data = await response.json();\n return data.choices[0]?.message?.content || '抱歉,我暂时无法回答这个问题。';\n}\n\n// POST /api/ai/chat - AI 聊天\nexport async function POST(request: NextRequest) {\n try {\n const body = await request.json();\n const { conversationId, message, provider = 'deepseek' } = body;\n\n if (!message) {\n return NextResponse.json({ error: '缺少消息内容' }, { status: 400 });\n }\n\n // 1. 从知识库检索相关内容\n const teamId = 'default-team';\n const ragResult = await ragQuery(message, teamId);\n\n // 2. 构建 prompt\n let prompt = message;\n if (ragResult.sources.length > 0) {\n const context = ragResult.sources\n .map((s, i) => `[参考${i + 1}]: ${s.content}`)\n .join('\\n\\n');\n prompt = `参考以下知识库内容回答用户问题:\\n\\n${context}\\n\\n用户问题:${message}\\n\\n请根据知识库内容给出回答,如果没有相关信息,请如实说明。`;\n }\n\n // 3. 调用 AI\n const aiResponse = await callAI(prompt, provider as any);\n\n // 4. 如果有会话ID,保存 AI 回复到数据库\n if (conversationId) {\n // 获取默认用户或创建系统消息\n const aiMessage = await prisma.message.create({\n data: {\n conversationId,\n senderId: 'ai-assistant',\n senderType: 'AI',\n content: aiResponse,\n isAiGenerated: true,\n },\n });\n\n return NextResponse.json({\n message: aiMessage,\n sources: ragResult.sources,\n elapsed: ragResult.elapsed,\n });\n }\n\n return NextResponse.json({\n response: aiResponse,\n sources: ragResult.sources,\n elapsed: ragResult.elapsed,\n });\n } catch (error) {\n console.error('Error in AI chat:', error);\n return NextResponse.json(\n { error: error instanceof Error ? error.message : 'AI 聊天失败' },\n { status: 500 }\n );\n }\n}\n"}}, "src/app/settings/page.tsx": {"db4291e5eaaede2c19cffd573e01d953bafb54e5": {"t": 1785747362.861727, "n": 78799, "h": "3bfce95adb7c851c", "c": "\"use client\";\nimport { useState } from \"react\";\nimport PageLayout from \"@/components/PageLayout\";\nimport {\n Settings, Users, Bell, Shield, Globe, Palette, Database, Key,\n Check, X, Plus, Trash2, Copy, Eye, EyeOff, RefreshCw, Download,\n Mail, Smartphone, Webhook, Trash, AlertTriangle,\n CheckCircle2, Image as ImageIcon, Moon, Sun, Monitor,\n Store, Link, TestTube, Edit2, ToggleLeft, ToggleRight,\n Book, ExternalLink,\n} from \"lucide-react\";\n\nconst menuItems = [\n { icon: Book, label: \"使用教程\", desc: \"各模块使用指南、API配置教程\" },\n { icon: Settings, label: \"项目设置\", desc: \"基本信息、时区、语言\" },\n { icon: Users, label: \"成员管理\", desc: \"添加成员、设置角色\" },\n { icon: Shield, label: \"权限配置\", desc: \"角色权限、访问控制\" },\n { icon: Bell, label: \"通知设置\", desc: \"邮件、短信、推送通知\" },\n { icon: Globe, label: \"渠道配置\", desc: \"渠道接入、Webhook\" },\n { icon: Palette, label: \"界面定制\", desc: \"主题、Logo、颜色\" },\n { icon: Database, label: \"数据管理\", desc: \"数据导出、清理\" },\n { icon: Key, label: \"API密钥\", desc: \"管理API访问密钥\" },\n];\n\nconst permissionModules = [\n { id: \"dashboard\", name: \"首页仪表盘\", permissions: [\"查看\", \"编辑\"] },\n { id: \"conversations\", name: \"聊天会话\", permissions: [\"查看\", \"回复\", \"转接\", \"关闭\"] },\n { id: \"customers\", name: \"客户管理\", permissions: [\"查看\", \"编辑\", \"删除\", \"导入导出\"] },\n { id: \"marketing\", name: \"营销模块\", permissions: [\"查看\", \"创建\", \"编辑\", \"删除\", \"发送\"] },\n { id: \"ai\", name: \"AI机器人\", permissions: [\"查看\", \"配置\", \"训练\", \"启停\"] },\n { id: \"knowledge\", name: \"知识库\", permissions: [\"查看\", \"上传\", \"编辑\", \"删除\"] },\n { id: \"channels\", name: \"渠道管理\", permissions: [\"查看\", \"添加\", \"编辑\", \"删除\"] },\n { id: \"reports\", name: \"报告分析\", permissions: [\"查看\", \"导出\"] },\n { id: \"settings\", name: \"系统设置\", permissions: [\"查看\", \"编辑\"] },\n];\n\nconst roles = [\n { id: \"admin\", name: \"管理员\", description: \"拥有所有权限\", color: \"bg-red-100 text-red-700\" },\n { id: \"supervisor\", name: \"主管\", description: \"管理团队和查看报告\", color: \"bg-purple-100 text-purple-700\" },\n { id: \"agent\", name: \"客服\", description: \"处理会话和客户\", color: \"bg-blue-100 text-blue-700\" },\n { id: \"viewer\", name: \"访客\", description: \"仅查看权限\", color: \"bg-gray-100 text-gray-700\" },\n];\n\nconst notificationTypes = [\n { id: \"new_message\", name: \"新消息提醒\", description: \"当有新客户消息时\", email: true, push: true, sms: false },\n { id: \"assigned\", name: \"会话分配\", description: \"当有新会话分配给你时\", email: true, push: true, sms: false },\n { id: \"ai_handover\", name: \"AI转人工\", description: \"当AI无法处理转人工时\", email: true, push: true, sms: true },\n { id: \"customer_join\", name: \"客户接入\", description: \"当高价值客户接入时\", email: false, push: true, sms: false },\n { id: \"daily_report\", name: \"日报\", description: \"每日工作汇总\", email: true, push: false, sms: false },\n { id: \"weekly_report\", name: \"周报\", description: \"每周工作汇总\", email: true, push: false, sms: false },\n { id: \"system_alert\", name: \"系统告警\", description: \"系统异常或维护通知\", email: true, push: true, sms: true },\n];\n\ntype ChannelConfig = {\n id: string; name: string; type: string; status: string; icon: string;\n webhook: string; appId: string; appSecret: string; bindStore: string; enabled: boolean;\n};\n\nconst channelConfigs: ChannelConfig[] = [\n { id: \"whatsapp\", name: \"WhatsApp\", type: \"即时通讯\", status: \"已连接\", icon: \"💬\", webhook: \"https://api.example.com/webhook/whatsapp\", appId: \"WA_APP_12345\", appSecret: \"whasec_xxxx\", bindStore: \"主店铺\", enabled: true },\n { id: \"messenger\", name: \"Messenger\", type: \"即时通讯\", status: \"已连接\", icon: \"📘\", webhook: \"https://api.example.com/webhook/messenger\", appId: \"MSG_APP_67890\", appSecret: \"msgsec_xxxx\", bindStore: \"主店铺\", enabled: true },\n { id: \"telegram\", name: \"Telegram\", type: \"即时通讯\", status: \"已连接\", icon: \"✈️\", webhook: \"https://api.example.com/webhook/telegram\", appId: \"TG_BOT_11111\", appSecret: \"tgsec_xxxx\", bindStore: \"\", enabled: true },\n { id: \"email\", name: \"Email\", type: \"邮件\", status: \"已连接\", icon: \"📧\", webhook: \"https://api.example.com/webhook/email\", appId: \"support@company.com\", appSecret: \"\", bindStore: \"主店铺\", enabled: true },\n { id: \"line\", name: \"LINE\", type: \"即时通讯\", status: \"未连接\", icon: \"🟢\", webhook: \"\", appId: \"\", appSecret: \"\", bindStore: \"\", enabled: false },\n { id: \"wechat\", name: \"微信公众号\", type: \"即时通讯\", status: \"未连接\", icon: \"💚\", webhook: \"\", appId: \"\", appSecret: \"\", bindStore: \"\", enabled: false },\n];\n\nconst apiKeys = [\n { id: \"1\", name: \"生产环境密钥\", key: \"ak_live_xxxxxxxxxxxx\", createdAt: \"2026-01-15\", lastUsed: \"2小时前\", status: \"active\" },\n { id: \"2\", name: \"测试环境密钥\", key: \"ak_test_xxxxxxxxxxxx\", createdAt: \"2026-02-01\", lastUsed: \"1天前\", status: \"active\" },\n { id: \"3\", name: \"开发环境密钥\", key: \"ak_dev_xxxxxxxxxxxx\", createdAt: \"2026-01-20\", lastUsed: \"3天前\", status: \"inactive\" },\n];\n\n// ---- Toast 组件 ----\nfunction Toast({ msg, type = \"success\", onClose }: { msg: string; type?: \"success\" | \"error\" | \"info\"; onClose: () => void }) {\n const colorMap = { success: \"bg-gray-900 text-white\", error: \"bg-red-600 text-white\", info: \"bg-blue-600 text-white\" };\n return (\n <div className={`fixed top-5 right-5 z-[100] flex items-center gap-2 px-4 py-2.5 rounded-xl shadow-xl text-sm ${colorMap[type]}`}>\n {type === \"success\" && <CheckCircle2 className=\"w-4 h-4 text-green-400 flex-shrink-0\" />}\n {type === \"error\" && <AlertTriangle className=\"w-4 h-4 text-red-200 flex-shrink-0\" />}\n <span>{msg}</span>\n <button onClick={onClose} className=\"ml-1 opacity-60 hover:opacity-100\"><X className=\"w-3.5 h-3.5\" /></button>\n </div>\n );\n}\n\nexport default function SettingsPage() {\n const [activeMenu, setActiveMenu] = useState(\"使用教程\");\n\n // ---- 全局 Toast ----\n const [toastMsg, setToastMsg] = useState(\"\");\n const [toastType, setToastType] = useState<\"success\" | \"error\" | \"info\">(\"success\");\n const [showToast, setShowToast] = useState(false);\n const toast = (msg: string, type: \"success\" | \"error\" | \"info\" = \"success\") => {\n setToastMsg(msg); setToastType(type); setShowToast(true);\n setTimeout(() => setShowToast(false), 2500);\n };\n\n // ---- 项目设置 ----\n const [projectName, setProjectName] = useState(\"我的AI客服系统\");\n const [timezone, setTimezone] = useState(\"Asia/Shanghai\");\n const [language, setLanguage] = useState(\"zh-CN\");\n\n // ---- AI 模型配置 ----\n type AIModel = { name: string; provider: string; type: string; status: string; badge: string; apiKey: string; configured: boolean };\n const [aiModels, setAIModels] = useState<AIModel[]>([\n { name: \"DeepSeek R1\", provider: \"DeepSeek\", type: \"推理模型\", status: \"已配置\", badge: \"主用\", apiKey: \"sk-••••••••••••••••\", configured: true },\n { name: \"通义千问 Max\", provider: \"阿里云\", type: \"对话模型\", status: \"已配置\", badge: \"\", apiKey: \"sk-••••••••••••••••\", configured: true },\n { name: \"GLM-4 Flash\", provider: \"智谱AI\", type: \"快速模型\", status: \"已配置\", badge: \"免费\", apiKey: \"sk-••••••••••••••••\", configured: true },\n ]);\n const [showAIConfigModal, setShowAIConfigModal] = useState(false);\n const [editingAIModel, setEditingAIModel] = useState<AIModel | null>(null);\n const [showAddModelModal, setShowAddModelModal] = useState(false);\n const [newModel, setNewModel] = useState({ name: \"\", provider: \"DeepSeek\", type: \"对话模型\", apiKey: \"\" });\n const [showAIKeyValue, setShowAIKeyValue] = useState<Record<string, boolean>>({});\n\n const handleEditAIModel = (m: AIModel) => { setEditingAIModel({ ...m, apiKey: \"\" }); setShowAIConfigModal(true); };\n const handleSaveAIModel = () => {\n if (!editingAIModel) return;\n setAIModels(prev => prev.map(m => m.name === editingAIModel.name ? { ...m, apiKey: editingAIModel.apiKey || m.apiKey, configured: true, status: \"已配置\" } : m));\n setShowAIConfigModal(false); setEditingAIModel(null); toast(\"模型配置已保存\");\n };\n const handleSaveNewModel = () => {\n if (!newModel.name.trim() || !newModel.apiKey.trim()) return;\n setAIModels(prev => [...prev, { name: newModel.name, provider: newModel.provider, type: newModel.type, status: \"已配置\", badge: \"\", apiKey: \"sk-••••••••••••••••\", configured: true }]);\n setNewModel({ name: \"\", provider: \"DeepSeek\", type: \"对话模型\", apiKey: \"\" });\n setShowAddModelModal(false); toast(\"模型添加成功\");\n };\n\n // ---- 成员管理 ----\n const members = [\n { name: \"张三\", email: \"zhang@company.com\", role: \"管理员\", status: \"在线\", joined: \"2026-01-01\" },\n { name: \"李四\", email: \"li@company.com\", role: \"客服\", status: \"在线\", joined: \"2026-01-15\" },\n { name: \"王五\", email: \"wang@company.com\", role: \"客服\", status: \"离线\", joined: \"2026-02-01\" },\n { name: \"赵六\", email: \"zhao@company.com\", role: \"主管\", status: \"忙碌\", joined: \"2026-01-20\" },\n ];\n type Member = typeof members[0];\n const [memberList, setMemberList] = useState<Member[]>(members);\n const [showEditMemberModal, setShowEditMemberModal] = useState(false);\n const [editingMember, setEditingMember] = useState<Member | null>(null);\n const [showDeleteMemberConfirm, setShowDeleteMemberConfirm] = useState(false);\n const [deletingMember, setDeletingMember] = useState<Member | null>(null);\n const [showInviteModal, setShowInviteModal] = useState(false);\n const [inviteEmail, setInviteEmail] = useState(\"\");\n const [inviteRole, setInviteRole] = useState(\"客服\");\n\n // ---- 权限配置 ----\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n const [rolePermissions, setRolePermissions] = useState<Record<string, string[]>>({\n admin: permissionModules.flatMap(m => m.permissions.map(p => `${m.id}:${p}`)),\n supervisor: [\"dashboard:查看\", \"conversations:查看\", \"conversations:回复\", \"conversations:转接\", \"customers:查看\", \"customers:编辑\", \"marketing:查看\", \"ai:查看\", \"knowledge:查看\", \"channels:查看\", \"reports:查看\", \"reports:导出\"],\n agent: [\"dashboard:查看\", \"conversations:查看\", \"conversations:回复\", \"conversations:转接\", \"customers:查看\", \"customers:编辑\", \"knowledge:查看\"],\n viewer: [\"dashboard:查看\", \"conversations:查看\", \"customers:查看\", \"reports:查看\"],\n });\n const togglePermission = (moduleId: string, permission: string) => {\n const key = `${moduleId}:${permission}`;\n setRolePermissions(prev => ({ ...prev, [selectedRole]: prev[selectedRole].includes(key) ? prev[selectedRole].filter(p => p !== key) : [...prev[selectedRole], key] }));\n };\n\n // ---- 通知设置 ----\n const [notifications, setNotifications] = useState(notificationTypes);\n const [notifyEmail, setNotifyEmail] = useState(\"admin@company.com\");\n const [notifyPhone, setNotifyPhone] = useState(\"+86 138 0000 0000\");\n const [notifyEmailEnabled, setNotifyEmailEnabled] = useState(true);\n const [notifySmsEnabled, setNotifySmsEnabled] = useState(true);\n const [notifyPushEnabled, setNotifyPushEnabled] = useState(true);\n const toggleNotification = (id: string, type: \"email\" | \"push\" | \"sms\") => {\n setNotifications(prev => prev.map(n => n.id === id ? { ...n, [type]: !n[type] } : n));\n };\n\n // ---- 渠道配置 ----\n const [channels, setChannels] = useState<ChannelConfig[]>(channelConfigs);\n const [showChannelModal, setShowChannelModal] = useState(false);\n const [editingChannel, setEditingChannel] = useState<ChannelConfig | null>(null);\n const [webhookTestResult, setWebhookTestResult] = useState<Record<string, \"idle\" | \"testing\" | \"ok\" | \"fail\">>({});\n const [channelSecretVisible, setChannelSecretVisible] = useState<Record<string, boolean>>({});\n const openChannelEdit = (c: ChannelConfig) => { setEditingChannel({ ...c }); setShowChannelModal(true); };\n const saveChannel = () => {\n if (!editingChannel) return;\n setChannels(prev => prev.map(c => c.id === editingChannel.id ? { ...editingChannel, status: editingChannel.appId ? \"已连接\" : \"未连接\" } : c));\n setShowChannelModal(false); setEditingChannel(null); toast(\"渠道配置已保存\");\n };\n const testWebhook = (id: string) => {\n setWebhookTestResult(prev => ({ ...prev, [id]: \"testing\" }));\n setTimeout(() => setWebhookTestResult(prev => ({ ...prev, [id]: Math.random() > 0.2 ? \"ok\" : \"fail\" })), 1500);\n };\n const toggleChannel = (id: string) => {\n setChannels(prev => prev.map(c => c.id === id ? { ...c, enabled: !c.enabled } : c));\n };\n\n // ---- 界面定制 ----\n const [theme, setTheme] = useState(\"light\");\n const [primaryColor, setPrimaryColor] = useState(\"#3B82F6\");\n\n // ---- 数据管理 ----\n const [showExportConfirm, setShowExportConfirm] = useState<{ name: string; format: string } | null>(null);\n const [showCleanConfirm, setShowCleanConfirm] = useState<{ name: string; desc: string } | null>(null);\n const [exportRetentionDays, setExportRetentionDays] = useState(\"30 天\");\n const handleExport = (name: string, format: string) => { setShowExportConfirm({ name, format }); };\n const confirmExport = () => { toast(`${showExportConfirm?.name} 导出任务已提交,稍后将发送到您的邮箱`); setShowExportConfirm(null); };\n const handleClean = (name: string, desc: string) => { setShowCleanConfirm({ name, desc }); };\n const confirmClean = () => { toast(`${showCleanConfirm?.name} 已清理完成`); setShowCleanConfirm(null); };\n\n // ---- API密钥 ----\n const [keys, setKeys] = useState(apiKeys);\n const [showKeyId, setShowKeyId] = useState<string | null>(null);\n const [newKeyName, setNewKeyName] = useState(\"\");\n const [showNewKeyModal, setShowNewKeyModal] = useState(false);\n const [generatedKey, setGeneratedKey] = useState(\"\");\n const generateNewKey = () => {\n const k = `ak_live_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;\n setGeneratedKey(k); setShowNewKeyModal(true);\n };\n const saveNewKey = () => {\n if (newKeyName && generatedKey) {\n setKeys(prev => [...prev, { id: Date.now().toString(), name: newKeyName, key: generatedKey, createdAt: new Date().toISOString().split(\"T\")[0], lastUsed: \"从未\", status: \"active\" }]);\n setNewKeyName(\"\"); setGeneratedKey(\"\"); setShowNewKeyModal(false); toast(\"密钥已保存\");\n }\n };\n const deleteKey = (id: string) => { setKeys(prev => prev.filter(k => k.id !== id)); toast(\"密钥已删除\"); };\n const copyKey = (key: string) => { navigator.clipboard.writeText(key); toast(\"已复制到剪贴板\"); };\n\n return (\n <PageLayout>\n <div className=\"flex h-screen\">\n {/* Settings Nav */}\n <div className=\"w-56 border-r border-gray-200 bg-white p-4 flex-shrink-0\">\n <div className=\"text-xs font-medium text-gray-400 uppercase tracking-wider mb-3 px-2\">设置</div>\n <nav className=\"space-y-1\">\n {menuItems.map(({ icon: Icon, label }) => (\n <button\n key={label}\n onClick={() => setActiveMenu(label)}\n className={`w-full text-left flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${activeMenu === label ? \"bg-blue-50 text-blue-700\" : \"text-gray-700 hover:bg-gray-50\"}`}\n >\n <Icon className=\"w-4 h-4 flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n </button>\n ))}\n </nav>\n </div>\n\n {/* Settings Content */}\n <div className=\"flex-1 overflow-auto p-8\">\n\n {/* ───── 使用教程 ───── */}\n {activeMenu === \"使用教程\" && (\n <div className=\"max-w-4xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">使用教程</h2>\n <p className=\"text-sm text-gray-500\">学习系统各功能的使用方法和API配置指南</p>\n </div>\n\n {/* 教程分类卡片 */}\n <div className=\"grid grid-cols-2 gap-4\">\n {[\n {\n icon: \"🎯\",\n title: \"快速入门\",\n desc: \"5分钟快速上手系统核心功能\",\n color: \"bg-green-50 border-green-200\",\n href: \"/tutorials\",\n chapters: [\"创建第一个AI机器人\", \"连接第一个渠道\", \"配置知识库\", \"测试对话\"],\n },\n {\n icon: \"📚\",\n title: \"模块教程\",\n desc: \"各功能模块详细使用指南\",\n color: \"bg-blue-50 border-blue-200\",\n href: \"/tutorials\",\n chapters: [\"首页仪表盘\", \"聊天会话管理\", \"客户管理\", \"AI机器人配置\"],\n },\n {\n icon: \"🔌\",\n title: \"API配置教程\",\n desc: \"各平台API接入详细步骤\",\n color: \"bg-purple-50 border-purple-200\",\n href: \"/tutorials\",\n chapters: [\"WhatsApp Business\", \"LINE Messaging API\", \"企业邮箱配置\", \"Telegram Bot\"],\n },\n {\n icon: \"⚡\",\n title: \"高级配置\",\n desc: \"自动化工作流和高级功能\",\n color: \"bg-orange-50 border-orange-200\",\n href: \"/tutorials\",\n chapters: [\"自动化工作流\", \"AI员工配置\", \"代理IP配置\", \"指纹浏览器\"],\n },\n ].map((item, idx) => (\n <a key={idx} href={item.href} className={`${item.color} border rounded-xl p-5 hover:shadow-md transition-shadow block`}>\n <div className=\"flex items-start gap-3 mb-3\">\n <span className=\"text-3xl\">{item.icon}</span>\n <div>\n <h3 className=\"font-semibold text-gray-900\">{item.title}</h3>\n <p className=\"text-sm text-gray-500 mt-0.5\">{item.desc}</p>\n </div>\n </div>\n <ul className=\"space-y-1.5\">\n {item.chapters.map((ch, i) => (\n <li key={i} className=\"flex items-center gap-2 text-sm text-gray-600\">\n <span className=\"w-1.5 h-1.5 rounded-full bg-gray-400\"></span>\n {ch}\n </li>\n ))}\n </ul>\n <div className=\"mt-3 flex items-center text-sm text-blue-600 font-medium\">\n 查看完整教程\n <ExternalLink className=\"w-4 h-4 ml-1\" />\n </div>\n </a>\n ))}\n </div>\n\n {/* 快速链接 */}\n <div className=\"bg-gray-50 rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2\">\n <Book className=\"w-4 h-4\" />\n 热门教程\n </h3>\n <div className=\"grid grid-cols-2 gap-3\">\n {[\n { title: \"WhatsApp Business API 配置指南\", icon: \"💬\", page: \"/binding\" },\n { title: \"LINE 官方账号接入教程\", icon: \"🟢\", page: \"/binding\" },\n { title: \"企业邮箱 SMTP/IMAP 配置\", icon: \"📧\", page: \"/binding\" },\n { title: \"代理IP配置与轮换策略\", icon: \"🌐\", page: \"/proxies\" },\n { title: \"AI模型选择与配置\", icon: \"🤖\", page: \"/ai\" },\n { title: \"指纹浏览器集成指南\", icon: \"🔐\", page: \"/fingerprint\" },\n { title: \"知识库构建与训练\", icon: \"📖\", page: \"/knowledge\" },\n { title: \"健康监控与告警配置\", icon: \"📊\", page: \"/monitoring\" },\n ].map((item, idx) => (\n <a key={idx} href={item.page} className=\"flex items-center gap-3 p-3 bg-white rounded-lg border border-gray-100 hover:border-blue-200 hover:bg-blue-50/30 transition-colors\">\n <span className=\"text-xl\">{item.icon}</span>\n <span className=\"text-sm text-gray-700\">{item.title}</span>\n </a>\n ))}\n </div>\n </div>\n\n {/* 视频教程入口 */}\n <div className=\"bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl p-6 text-white\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h3 className=\"font-semibold text-lg mb-1\">视频教程中心</h3>\n <p className=\"text-blue-100 text-sm\">观看完整操作视频,手把手教学</p>\n </div>\n <button className=\"px-4 py-2 bg-white text-blue-600 rounded-lg text-sm font-medium hover:bg-blue-50 flex items-center gap-2\">\n <Book className=\"w-4 h-4\" />\n 观看视频\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* ───── 项目设置 ───── */}\n {activeMenu === \"项目设置\" && (\n <div className=\"max-w-2xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">项目设置</h2>\n <p className=\"text-sm text-gray-500\">管理你的客服系统基本配置</p>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 divide-y divide-gray-100\">\n <div className=\"p-5\">\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">项目名称</label>\n <input value={projectName} onChange={e => setProjectName(e.target.value)} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-blue-400\" />\n </div>\n <div className=\"p-5\">\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">时区</label>\n <select value={timezone} onChange={e => setTimezone(e.target.value)} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none\">\n <option value=\"Asia/Shanghai\">Asia/Shanghai (UTC+8)</option>\n <option value=\"Asia/Hong_Kong\">Asia/Hong_Kong (UTC+8)</option>\n <option value=\"UTC\">UTC</option>\n </select>\n </div>\n <div className=\"p-5\">\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">系统语言</label>\n <select value={language} onChange={e => setLanguage(e.target.value)} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none\">\n <option value=\"zh-CN\">简体中文</option>\n <option value=\"zh-TW\">繁体中文</option>\n <option value=\"en-US\">English</option>\n </select>\n </div>\n <div className=\"p-5 flex justify-end\">\n <button onClick={() => toast(\"项目设置已保存\")} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存更改</button>\n </div>\n </div>\n\n {/* AI 模型配置 */}\n <div>\n <h3 className=\"text-base font-semibold text-gray-900 mb-3\">AI 模型配置</h3>\n <div className=\"bg-white rounded-xl border border-gray-200 divide-y divide-gray-100\">\n {aiModels.map(m => (\n <div key={m.name} className=\"p-4 flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-gray-900\">{m.name}</span>\n {m.badge && <span className=\"text-xs px-1.5 py-0.5 bg-blue-50 text-blue-600 rounded\">{m.badge}</span>}\n </div>\n <div className=\"text-xs text-gray-400 mt-0.5\">{m.provider} · {m.type}</div>\n </div>\n <div className=\"flex items-center gap-3\">\n <span className={`text-xs ${m.configured ? \"text-green-600\" : \"text-gray-400\"}`}>{m.status}</span>\n <button onClick={() => handleEditAIModel(m)} className=\"text-xs text-blue-600 border border-blue-200 rounded-lg px-2.5 py-1 hover:bg-blue-50\">配置 API Key</button>\n </div>\n </div>\n ))}\n <div className=\"p-4\">\n <button onClick={() => setShowAddModelModal(true)} className=\"text-sm text-blue-600 hover:underline flex items-center gap-1\">\n <Plus className=\"w-4 h-4\" /> 添加模型\n </button>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* ───── 成员管理 ───── */}\n {activeMenu === \"成员管理\" && (\n <div className=\"max-w-3xl space-y-4\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900\">成员管理</h2>\n <p className=\"text-sm text-gray-500 mt-0.5\">管理团队成员和权限</p>\n </div>\n <button onClick={() => setShowInviteModal(true)} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2\">\n <Plus className=\"w-4 h-4\" /> 邀请成员\n </button>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <table className=\"w-full\">\n <thead className=\"bg-gray-50\">\n <tr>{[\"成员\", \"角色\", \"状态\", \"加入时间\", \"操作\"].map(h => <th key={h} className=\"px-4 py-3 text-left text-xs font-medium text-gray-500\">{h}</th>)}</tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {memberList.map((m, i) => (\n <tr key={i} className=\"hover:bg-gray-50\">\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-700\">{m.name[0]}</div>\n <div>\n <div className=\"text-sm font-medium text-gray-900\">{m.name}</div>\n <div className=\"text-xs text-gray-400\">{m.email}</div>\n </div>\n </div>\n </td>\n <td className=\"px-4 py-3\">\n <span className={`text-xs px-2 py-1 rounded-full ${m.role === \"管理员\" ? \"bg-red-50 text-red-600\" : m.role === \"主管\" ? \"bg-purple-50 text-purple-600\" : \"bg-gray-100 text-gray-600\"}`}>{m.role}</span>\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-1\">\n <span className={`w-2 h-2 rounded-full ${m.status === \"在线\" ? \"bg-green-500\" : m.status === \"忙碌\" ? \"bg-yellow-500\" : \"bg-gray-300\"}`}></span>\n <span className=\"text-xs text-gray-500\">{m.status}</span>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm text-gray-400\">{m.joined}</td>\n <td className=\"px-4 py-3\">\n <button onClick={() => { setEditingMember({ ...m }); setShowEditMemberModal(true); }} className=\"text-xs text-blue-600 hover:underline mr-2\">编辑</button>\n <button onClick={() => { setDeletingMember(m); setShowDeleteMemberConfirm(true); }} className=\"text-xs text-red-400 hover:underline\">移除</button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n )}\n\n {/* ───── 权限配置 ───── */}\n {activeMenu === \"权限配置\" && (\n <div className=\"max-w-4xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">权限配置</h2>\n <p className=\"text-sm text-gray-500\">管理角色权限和访问控制</p>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-4\">\n <div className=\"flex gap-2\">\n {roles.map(role => (\n <button key={role.id} onClick={() => setSelectedRole(role.id)}\n className={`flex-1 p-3 rounded-lg border text-left transition-colors ${selectedRole === role.id ? \"border-blue-500 bg-blue-50\" : \"border-gray-200 hover:bg-gray-50\"}`}>\n <div className=\"flex items-center gap-2 mb-1\">\n <span className={`text-xs px-2 py-0.5 rounded-full ${role.color}`}>{role.name}</span>\n </div>\n <div className=\"text-xs text-gray-500\">{role.description}</div>\n </button>\n ))}\n </div>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <div className=\"px-4 py-3 bg-gray-50 border-b border-gray-200 flex items-center justify-between\">\n <h3 className=\"font-medium text-gray-900\">权限矩阵</h3>\n <span className=\"text-xs text-gray-400\">已勾选 {rolePermissions[selectedRole]?.length ?? 0} 项权限</span>\n </div>\n <table className=\"w-full\">\n <thead className=\"bg-gray-50\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-gray-500 w-32\">功能模块</th>\n {permissionModules[0]?.permissions.map(p => <th key={p} className=\"px-4 py-3 text-center text-xs font-medium text-gray-500\">{p}</th>)}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {permissionModules.map(module => (\n <tr key={module.id}>\n <td className=\"px-4 py-3 text-sm font-medium text-gray-900\">{module.name}</td>\n {module.permissions.map(permission => {\n const key = `${module.id}:${permission}`;\n const has = rolePermissions[selectedRole]?.includes(key);\n return (\n <td key={permission} className=\"px-4 py-3 text-center\">\n <button onClick={() => togglePermission(module.id, permission)}\n className={`w-6 h-6 rounded flex items-center justify-center mx-auto transition-colors ${has ? \"bg-blue-500 text-white\" : \"bg-gray-100 text-gray-300 hover:bg-gray-200\"}`}>\n {has ? <Check className=\"w-4 h-4\" /> : <X className=\"w-4 h-4\" />}\n </button>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <div className=\"flex justify-end gap-3\">\n <button onClick={() => toast(\"权限已重置为默认\", \"info\")} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">重置</button>\n <button onClick={() => toast(\"权限配置已保存\")} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存权限配置</button>\n </div>\n </div>\n )}\n\n {/* ───── 通知设置 ───── */}\n {activeMenu === \"通知设置\" && (\n <div className=\"max-w-3xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">通知设置</h2>\n <p className=\"text-sm text-gray-500\">配置邮件、短信和推送通知</p>\n </div>\n\n {/* 渠道开关 + 绑定地址 */}\n <div className=\"grid grid-cols-3 gap-4\">\n {/* 邮件 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Mail className=\"w-5 h-5 text-blue-600\" />\n <span className=\"font-medium text-gray-900\">邮件通知</span>\n </div>\n <button onClick={() => setNotifyEmailEnabled(v => !v)}\n className={`w-10 h-6 rounded-full relative transition-colors ${notifyEmailEnabled ? \"bg-blue-500\" : \"bg-gray-300\"}`}>\n <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${notifyEmailEnabled ? \"left-[18px]\" : \"left-0.5\"}`} />\n </button>\n </div>\n <div>\n <label className=\"text-xs text-gray-500 mb-1 block\">接收邮箱</label>\n <input value={notifyEmail} onChange={e => setNotifyEmail(e.target.value)} disabled={!notifyEmailEnabled}\n className=\"w-full px-2 py-1.5 border border-gray-200 rounded-lg text-xs focus:outline-none focus:border-blue-400 disabled:bg-gray-50 disabled:text-gray-400\" />\n </div>\n </div>\n {/* 短信 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Smartphone className=\"w-5 h-5 text-green-600\" />\n <span className=\"font-medium text-gray-900\">短信通知</span>\n </div>\n <button onClick={() => setNotifySmsEnabled(v => !v)}\n className={`w-10 h-6 rounded-full relative transition-colors ${notifySmsEnabled ? \"bg-blue-500\" : \"bg-gray-300\"}`}>\n <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${notifySmsEnabled ? \"left-[18px]\" : \"left-0.5\"}`} />\n </button>\n </div>\n <div>\n <label className=\"text-xs text-gray-500 mb-1 block\">接收手机</label>\n <input value={notifyPhone} onChange={e => setNotifyPhone(e.target.value)} disabled={!notifySmsEnabled}\n className=\"w-full px-2 py-1.5 border border-gray-200 rounded-lg text-xs focus:outline-none focus:border-blue-400 disabled:bg-gray-50 disabled:text-gray-400\" />\n </div>\n </div>\n {/* 推送 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Bell className=\"w-5 h-5 text-purple-600\" />\n <span className=\"font-medium text-gray-900\">推送通知</span>\n </div>\n <button onClick={() => setNotifyPushEnabled(v => !v)}\n className={`w-10 h-6 rounded-full relative transition-colors ${notifyPushEnabled ? \"bg-blue-500\" : \"bg-gray-300\"}`}>\n <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${notifyPushEnabled ? \"left-[18px]\" : \"left-0.5\"}`} />\n </button>\n </div>\n <p className=\"text-xs text-gray-400\">浏览器 / APP 推送通知,无需额外配置</p>\n </div>\n </div>\n\n {/* 通知类型 */}\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <div className=\"px-4 py-3 bg-gray-50 border-b border-gray-200\">\n <h3 className=\"font-medium text-gray-900\">通知类型配置</h3>\n </div>\n <table className=\"w-full\">\n <thead className=\"bg-gray-50\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-gray-500\">通知类型</th>\n <th className=\"px-4 py-3 text-center text-xs font-medium text-gray-500\">邮件</th>\n <th className=\"px-4 py-3 text-center text-xs font-medium text-gray-500\">推送</th>\n <th className=\"px-4 py-3 text-center text-xs font-medium text-gray-500\">短信</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {notifications.map(n => (\n <tr key={n.id}>\n <td className=\"px-4 py-3\">\n <div className=\"text-sm font-medium text-gray-900\">{n.name}</div>\n <div className=\"text-xs text-gray-500\">{n.description}</div>\n </td>\n {([\"email\", \"push\", \"sms\"] as const).map(type => (\n <td key={type} className=\"px-4 py-3 text-center\">\n <input type=\"checkbox\" checked={n[type]} onChange={() => toggleNotification(n.id, type)} className=\"rounded text-blue-600\" />\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <div className=\"flex justify-end\">\n <button onClick={() => toast(\"通知设置已保存\")} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存通知设置</button>\n </div>\n </div>\n )}\n\n {/* ───── 渠道配置 ───── */}\n {activeMenu === \"渠道配置\" && (\n <div className=\"max-w-3xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">渠道配置</h2>\n <p className=\"text-sm text-gray-500\">管理各渠道接入、绑定店铺和 Webhook 设置</p>\n </div>\n\n {/* Webhook 基础配置 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2\">\n <Webhook className=\"w-4 h-4\" /> Webhook 基础配置\n </h3>\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">Webhook URL</label>\n <div className=\"flex gap-2\">\n <input value=\"https://api.example.com/webhook\" readOnly className=\"flex-1 px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm\" />\n <button onClick={() => copyKey(\"https://api.example.com/webhook\")} className=\"px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50\"><Copy className=\"w-4 h-4\" /></button>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">Webhook Secret</label>\n <div className=\"flex gap-2\">\n <input type=\"password\" value=\"whsec_xxxxxxxxxxxxxxxx\" readOnly className=\"flex-1 px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm\" />\n <button onClick={() => toast(\"Secret 已重置\")} className=\"px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50\"><RefreshCw className=\"w-4 h-4\" /></button>\n </div>\n </div>\n </div>\n </div>\n\n {/* 渠道列表 */}\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <div className=\"px-4 py-3 bg-gray-50 border-b border-gray-200 flex items-center justify-between\">\n <h3 className=\"font-medium text-gray-900\">渠道列表</h3>\n <span className=\"text-xs text-gray-400\">{channels.filter(c => c.status === \"已连接\").length}/{channels.length} 已连接</span>\n </div>\n <div className=\"divide-y divide-gray-100\">\n {channels.map(channel => (\n <div key={channel.id} className=\"p-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <span className=\"text-2xl\">{channel.icon}</span>\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium text-gray-900\">{channel.name}</span>\n <span className={`text-xs px-1.5 py-0.5 rounded-full ${channel.status === \"已连接\" ? \"bg-green-100 text-green-700\" : \"bg-gray-100 text-gray-500\"}`}>{channel.status}</span>\n </div>\n <div className=\"text-xs text-gray-400 mt-0.5\">{channel.type}{channel.bindStore ? ` · 绑定店铺: ${channel.bindStore}` : \"\"}</div>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {/* 启用/禁用开关 */}\n <button onClick={() => toggleChannel(channel.id)}\n className={`w-9 h-5 rounded-full relative transition-colors ${channel.enabled ? \"bg-blue-500\" : \"bg-gray-300\"}`}>\n <span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all ${channel.enabled ? \"left-[18px]\" : \"left-0.5\"}`} />\n </button>\n {/* Webhook 测试 */}\n {channel.status === \"已连接\" && (\n <button\n onClick={() => testWebhook(channel.id)}\n className={`text-xs px-2 py-1 rounded border flex items-center gap-1 transition-colors ${\n webhookTestResult[channel.id] === \"testing\" ? \"border-yellow-300 text-yellow-600 bg-yellow-50\" :\n webhookTestResult[channel.id] === \"ok\" ? \"border-green-300 text-green-600 bg-green-50\" :\n webhookTestResult[channel.id] === \"fail\" ? \"border-red-300 text-red-600 bg-red-50\" :\n \"border-gray-200 text-gray-600 hover:bg-gray-50\"\n }`}\n >\n <TestTube className=\"w-3 h-3\" />\n {webhookTestResult[channel.id] === \"testing\" ? \"测试中...\" : webhookTestResult[channel.id] === \"ok\" ? \"正常\" : webhookTestResult[channel.id] === \"fail\" ? \"失败\" : \"测试\"}\n </button>\n )}\n {/* 配置按钮 */}\n <button onClick={() => openChannelEdit(channel)} className=\"text-xs text-blue-600 border border-blue-200 rounded-lg px-2.5 py-1 hover:bg-blue-50 flex items-center gap-1\">\n <Edit2 className=\"w-3 h-3\" /> 配置\n </button>\n </div>\n </div>\n {/* 绑定店铺快捷信息 */}\n {channel.appId && (\n <div className=\"mt-2 ml-11 flex items-center gap-4 text-xs text-gray-400\">\n <span className=\"flex items-center gap-1\"><Link className=\"w-3 h-3\" /> App ID: {channel.appId.substring(0, 12)}{channel.appId.length > 12 ? \"...\" : \"\"}</span>\n {channel.bindStore && <span className=\"flex items-center gap-1\"><Store className=\"w-3 h-3\" /> {channel.bindStore}</span>}\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n </div>\n )}\n\n {/* ───── 界面定制 ───── */}\n {activeMenu === \"界面定制\" && (\n <div className=\"max-w-2xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">界面定制</h2>\n <p className=\"text-sm text-gray-500\">自定义主题、Logo和品牌颜色</p>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4\">主题模式</h3>\n <div className=\"grid grid-cols-3 gap-4\">\n {[{ key: \"light\", icon: <Sun className=\"w-8 h-8 mx-auto mb-2 text-orange-500\" />, label: \"浅色模式\", desc: \"默认明亮主题\" },\n { key: \"dark\", icon: <Moon className=\"w-8 h-8 mx-auto mb-2 text-indigo-500\" />, label: \"深色模式\", desc: \"护眼暗色主题\" },\n { key: \"auto\", icon: <Monitor className=\"w-8 h-8 mx-auto mb-2 text-blue-500\" />, label: \"跟随系统\", desc: \"自动切换\" }].map(t => (\n <button key={t.key} onClick={() => setTheme(t.key)}\n className={`p-4 rounded-xl border text-center transition-colors ${theme === t.key ? \"border-blue-500 bg-blue-50\" : \"border-gray-200 hover:bg-gray-50\"}`}>\n {t.icon}\n <div className=\"font-medium text-gray-900\">{t.label}</div>\n <div className=\"text-xs text-gray-500\">{t.desc}</div>\n </button>\n ))}\n </div>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4\">品牌颜色</h3>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">主色调</label>\n <div className=\"flex gap-2\">\n {[\"#3B82F6\", \"#10B981\", \"#8B5CF6\", \"#F59E0B\", \"#EF4444\", \"#EC4899\"].map(color => (\n <button key={color} onClick={() => setPrimaryColor(color)}\n className={`w-10 h-10 rounded-lg transition-transform ${primaryColor === color ? \"ring-2 ring-offset-2 ring-gray-400 scale-110\" : \"\"}`}\n style={{ backgroundColor: color }} />\n ))}\n </div>\n </div>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4\">品牌标识</h3>\n <div className=\"grid grid-cols-2 gap-4\">\n {[{ label: \"Logo\", hint: \"建议尺寸 200x50px\" }, { label: \"Favicon\", hint: \"建议尺寸 32x32px\" }].map(item => (\n <div key={item.label}>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">{item.label}</label>\n <div className=\"border-2 border-dashed border-gray-200 rounded-xl p-6 text-center hover:bg-gray-50 cursor-pointer\" onClick={() => toast(\"文件选择功能开发中\", \"info\")}>\n <ImageIcon className=\"w-8 h-8 mx-auto mb-2 text-gray-400\" />\n <div className=\"text-sm text-gray-600\">点击上传 {item.label}</div>\n <div className=\"text-xs text-gray-400 mt-1\">{item.hint}</div>\n </div>\n </div>\n ))}\n </div>\n </div>\n <div className=\"flex justify-end\">\n <button onClick={() => toast(\"界面设置已保存\")} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存界面设置</button>\n </div>\n </div>\n )}\n\n {/* ───── 数据管理 ───── */}\n {activeMenu === \"数据管理\" && (\n <div className=\"max-w-3xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">数据管理</h2>\n <p className=\"text-sm text-gray-500\">数据导出、备份和清理</p>\n </div>\n\n {/* 数据导出 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2\"><Download className=\"w-4 h-4\" /> 数据导出</h3>\n <div className=\"space-y-3\">\n {[\n { name: \"客户数据\", desc: \"导出所有客户信息(姓名、联系方式、标签)\", format: \"CSV, Excel\" },\n { name: \"会话记录\", desc: \"导出历史会话数据(含会话摘要)\", format: \"JSON, CSV\" },\n { name: \"消息记录\", desc: \"导出所有消息内容(含附件链接)\", format: \"JSON\" },\n { name: \"报告数据\", desc: \"导出统计报告(含图表数据)\", format: \"Excel, PDF\" },\n ].map(item => (\n <div key={item.name} className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className=\"font-medium text-gray-900\">{item.name}</div>\n <div className=\"text-xs text-gray-500\">{item.desc}</div>\n </div>\n <div className=\"flex items-center gap-3\">\n <span className=\"text-xs text-gray-400\">{item.format}</span>\n <button onClick={() => handleExport(item.name, item.format)} className=\"px-3 py-1.5 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 flex items-center gap-1\">\n <Download className=\"w-3.5 h-3.5\" /> 导出\n </button>\n </div>\n </div>\n ))}\n </div>\n </div>\n\n {/* 数据备份 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2\"><Database className=\"w-4 h-4\" /> 数据备份</h3>\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className=\"font-medium text-gray-900\">自动备份</div>\n <div className=\"text-xs text-gray-500\">每天凌晨 2:00 自动备份</div>\n </div>\n <label className=\"relative inline-flex items-center cursor-pointer\">\n <input type=\"checkbox\" defaultChecked className=\"sr-only peer\" />\n <div className=\"w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600\"></div>\n </label>\n </div>\n <div className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className=\"font-medium text-gray-900\">备份保留期</div>\n <div className=\"text-xs text-gray-500\">超过保留期的备份将自动删除</div>\n </div>\n <select value={exportRetentionDays} onChange={e => setExportRetentionDays(e.target.value)} className=\"px-3 py-1.5 border border-gray-200 rounded text-sm\">\n <option>7 天</option>\n <option>30 天</option>\n <option>90 天</option>\n <option>永久</option>\n </select>\n </div>\n <div className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className=\"font-medium text-gray-900\">上次备份时间</div>\n <div className=\"text-xs text-gray-500\">2026-04-01 02:00:00 · 共 128 MB</div>\n </div>\n <button onClick={() => toast(\"正在创建备份...\")} className=\"px-3 py-1.5 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 flex items-center gap-1\">\n <RefreshCw className=\"w-3.5 h-3.5\" /> 立即备份\n </button>\n </div>\n </div>\n </div>\n\n {/* 数据清理 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2 text-red-600\"><Trash className=\"w-4 h-4\" /> 数据清理</h3>\n <div className=\"space-y-3\">\n {[\n { name: \"清理已删除客户\", desc: \"永久删除已标记删除的客户数据,不可恢复\", danger: true },\n { name: \"清理过期会话\", desc: \"删除超过 1 年的已结束会话记录\", danger: true },\n { name: \"清理系统日志\", desc: \"删除 90 天前的系统运行日志\", danger: false },\n ].map(item => (\n <div key={item.name} className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className={`font-medium ${item.danger ? \"text-red-600\" : \"text-gray-900\"}`}>{item.name}</div>\n <div className=\"text-xs text-gray-500\">{item.desc}</div>\n </div>\n <button onClick={() => handleClean(item.name, item.desc)}\n className={`px-3 py-1.5 rounded text-sm flex items-center gap-1 ${item.danger ? \"bg-red-50 text-red-600 hover:bg-red-100\" : \"bg-white border border-gray-200 hover:bg-gray-50\"}`}>\n {item.danger && <AlertTriangle className=\"w-3.5 h-3.5\" />} 执行\n </button>\n </div>\n ))}\n </div>\n </div>\n </div>\n )}\n\n {/* ───── API 密钥 ───── */}\n {activeMenu === \"API密钥\" && (\n <div className=\"max-w-3xl space-y-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">API密钥</h2>\n <p className=\"text-sm text-gray-500\">管理API访问密钥和权限</p>\n </div>\n <button onClick={generateNewKey} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2\">\n <Plus className=\"w-4 h-4\" /> 新建密钥\n </button>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <table className=\"w-full\">\n <thead className=\"bg-gray-50\">\n <tr>{[\"名称\", \"密钥\", \"创建时间\", \"最后使用\", \"状态\", \"操作\"].map(h => <th key={h} className=\"px-4 py-3 text-left text-xs font-medium text-gray-500\">{h}</th>)}</tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {keys.map(k => (\n <tr key={k.id}>\n <td className=\"px-4 py-3 text-sm font-medium text-gray-900\">{k.name}</td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <code className=\"text-xs bg-gray-100 px-2 py-1 rounded\">{showKeyId === k.id ? k.key : `${k.key.substring(0, 8)}...`}</code>\n <button onClick={() => setShowKeyId(showKeyId === k.id ? null : k.id)} className=\"text-gray-400 hover:text-gray-600\">\n {showKeyId === k.id ? <EyeOff className=\"w-4 h-4\" /> : <Eye className=\"w-4 h-4\" />}\n </button>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm text-gray-500\">{k.createdAt}</td>\n <td className=\"px-4 py-3 text-sm text-gray-500\">{k.lastUsed}</td>\n <td className=\"px-4 py-3\">\n <span className={`text-xs px-2 py-1 rounded-full ${k.status === \"active\" ? \"bg-green-100 text-green-700\" : \"bg-gray-100 text-gray-600\"}`}>\n {k.status === \"active\" ? \"活跃\" : \"已禁用\"}\n </span>\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <button onClick={() => copyKey(k.key)} className=\"text-gray-400 hover:text-gray-600\" title=\"复制\"><Copy className=\"w-4 h-4\" /></button>\n <button onClick={() => deleteKey(k.id)} className=\"text-gray-400 hover:text-red-600\" title=\"删除\"><Trash2 className=\"w-4 h-4\" /></button>\n </div>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <div className=\"bg-blue-50 rounded-xl border border-blue-100 p-5\">\n <h3 className=\"font-medium text-blue-900 mb-2\">API 文档</h3>\n <p className=\"text-sm text-blue-700 mb-3\">查看完整的 API 文档和示例代码</p>\n <button className=\"text-sm text-blue-600 hover:underline\">查看文档 →</button>\n </div>\n </div>\n )}\n\n </div>{/* end Settings Content */}\n </div>{/* end flex h-screen */}\n\n {/* ═══════════════ 全局 Toast ═══════════════ */}\n {showToast && <Toast msg={toastMsg} type={toastType} onClose={() => setShowToast(false)} />}\n\n {/* ═══════════════ 邀请成员弹窗 ═══════════════ */}\n {showInviteModal && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-2xl w-[440px] p-6 shadow-2xl\">\n <div className=\"flex items-center justify-between mb-5\">\n <h3 className=\"text-lg font-semibold text-gray-900\">邀请成员</h3>\n <button onClick={() => setShowInviteModal(false)} className=\"text-gray-400 hover:text-gray-600\"><X className=\"w-5 h-5\" /></button>\n </div>\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">邮箱地址</label>\n <input type=\"email\" value={inviteEmail} onChange={e => setInviteEmail(e.target.value)} placeholder=\"colleague@company.com\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">角色</label>\n <select value={inviteRole} onChange={e => setInviteRole(e.target.value)} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>管理员</option><option>主管</option><option>客服</option><option>只读</option>\n </select>\n </div>\n <p className=\"text-xs text-gray-400\">邀请邮件将发送至上述地址,对方接受后成为团队成员。</p>\n </div>\n <div className=\"flex justify-end gap-2 mt-5\">\n <button onClick={() => setShowInviteModal(false)} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={() => { if (!inviteEmail.trim()) return; setShowInviteModal(false); setInviteEmail(\"\"); toast(`邀请已发送至 ${inviteEmail}`); }}\n disabled={!inviteEmail.trim()}\n className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50\">发送邀请</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 编辑成员弹窗 ═══════════════ */}\n {showEditMemberModal && editingMember && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl w-[440px] p-6 shadow-2xl\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-lg font-semibold\">编辑成员</h3>\n <button onClick={() => { setShowEditMemberModal(false); setEditingMember(null); }} className=\"text-gray-400 hover:text-gray-600\"><X className=\"w-5 h-5\" /></button>\n </div>\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">姓名</label>\n <input value={editingMember.name} onChange={e => setEditingMember({ ...editingMember, name: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">邮箱</label>\n <input type=\"email\" value={editingMember.email} onChange={e => setEditingMember({ ...editingMember, email: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">角色</label>\n <select value={editingMember.role} onChange={e => setEditingMember({ ...editingMember, role: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>管理员</option><option>主管</option><option>客服</option><option>只读</option>\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">状态</label>\n <select value={editingMember.status} onChange={e => setEditingMember({ ...editingMember, status: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>在线</option><option>忙碌</option><option>离线</option>\n </select>\n </div>\n </div>\n </div>\n <div className=\"flex justify-end gap-2 mt-6\">\n <button onClick={() => { setShowEditMemberModal(false); setEditingMember(null); }} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={() => {\n if (!editingMember.name.trim()) return;\n setMemberList(prev => prev.map(m => m.email === editingMember.email ? editingMember : m));\n setShowEditMemberModal(false); setEditingMember(null); toast(\"成员信息已更新\");\n }} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存修改</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 删除成员确认 ═══════════════ */}\n {showDeleteMemberConfirm && deletingMember && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl w-[400px] p-6 shadow-2xl\">\n <h3 className=\"text-lg font-semibold mb-2\">移除成员</h3>\n <p className=\"text-sm text-gray-600 mb-6\">确定要移除成员 <span className=\"font-medium text-gray-900\">\"{deletingMember.name}\"</span> 吗?移除后该成员将无法访问系统。</p>\n <div className=\"flex justify-end gap-2\">\n <button onClick={() => { setShowDeleteMemberConfirm(false); setDeletingMember(null); }} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={() => {\n setMemberList(prev => prev.filter(m => m.email !== deletingMember.email));\n setShowDeleteMemberConfirm(false); setDeletingMember(null); toast(\"成员已移除\");\n }} className=\"px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700\">确认移除</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 渠道配置弹窗 ═══════════════ */}\n {showChannelModal && editingChannel && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-2xl w-full max-w-lg mx-4 shadow-2xl\">\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-gray-200\">\n <div className=\"flex items-center gap-3\">\n <span className=\"text-2xl\">{editingChannel.icon}</span>\n <div>\n <div className=\"font-semibold text-gray-900\">{editingChannel.name} 配置</div>\n <div className=\"text-xs text-gray-400\">{editingChannel.type}</div>\n </div>\n </div>\n <button onClick={() => { setShowChannelModal(false); setEditingChannel(null); }} className=\"p-1 hover:bg-gray-100 rounded-lg\"><X className=\"w-5 h-5 text-gray-400\" /></button>\n </div>\n <div className=\"p-6 space-y-4\">\n {/* App ID */}\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">\n {editingChannel.type === \"邮件\" ? \"发件邮箱\" : \"App ID / Bot Token\"}\n </label>\n <input value={editingChannel.appId} onChange={e => setEditingChannel({ ...editingChannel, appId: e.target.value })}\n placeholder={editingChannel.type === \"邮件\" ? \"support@yourcompany.com\" : \"输入 App ID 或 Bot Token\"}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n {/* App Secret */}\n {editingChannel.type !== \"邮件\" && (\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">App Secret</label>\n <div className=\"relative\">\n <input type={channelSecretVisible[editingChannel.id] ? \"text\" : \"password\"}\n value={editingChannel.appSecret}\n onChange={e => setEditingChannel({ ...editingChannel, appSecret: e.target.value })}\n placeholder=\"输入 App Secret\"\n className=\"w-full px-3 py-2 pr-10 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n <button type=\"button\" onClick={() => setChannelSecretVisible(prev => ({ ...prev, [editingChannel.id]: !prev[editingChannel.id] }))}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600\">\n {channelSecretVisible[editingChannel.id] ? <EyeOff className=\"w-4 h-4\" /> : <Eye className=\"w-4 h-4\" />}\n </button>\n </div>\n </div>\n )}\n {/* 绑定店铺 */}\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5 flex items-center gap-1\">\n <Store className=\"w-3.5 h-3.5\" /> 绑定店铺\n </label>\n <select value={editingChannel.bindStore} onChange={e => setEditingChannel({ ...editingChannel, bindStore: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option value=\"\">-- 不绑定店铺 --</option>\n <option value=\"主店铺\">主店铺</option>\n <option value=\"跨境店\">跨境店</option>\n <option value=\"批发渠道\">批发渠道</option>\n <option value=\"代理商\">代理商渠道</option>\n </select>\n <p className=\"text-xs text-gray-400 mt-1\">绑定后,该渠道的会话将关联到对应店铺的客户数据</p>\n </div>\n {/* Webhook URL */}\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">Webhook 回调地址</label>\n <div className=\"flex gap-2\">\n <input value={editingChannel.webhook} onChange={e => setEditingChannel({ ...editingChannel, webhook: e.target.value })}\n placeholder=\"https://api.example.com/webhook/...\"\n className=\"flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n <button onClick={() => testWebhook(editingChannel.id)} className=\"px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 text-sm flex items-center gap-1 whitespace-nowrap\">\n <TestTube className=\"w-3.5 h-3.5\" /> 测试\n </button>\n </div>\n {webhookTestResult[editingChannel.id] === \"ok\" && <p className=\"text-xs text-green-600 mt-1\">✓ Webhook 连通性正常</p>}\n {webhookTestResult[editingChannel.id] === \"fail\" && <p className=\"text-xs text-red-500 mt-1\">✗ Webhook 测试失败,请检查地址</p>}\n </div>\n </div>\n <div className=\"flex items-center justify-between px-6 py-4 border-t border-gray-200\">\n <span className=\"text-xs text-gray-400\">配置保存后自动更新连接状态</span>\n <div className=\"flex gap-3\">\n <button onClick={() => { setShowChannelModal(false); setEditingChannel(null); }} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm text-gray-600 hover:bg-gray-50\">取消</button>\n <button onClick={saveChannel} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存配置</button>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ AI 模型配置弹窗 ═══════════════ */}\n {showAIConfigModal && editingAIModel && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-2xl w-full max-w-md mx-4 shadow-2xl\">\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-gray-200\">\n <div>\n <div className=\"font-semibold text-gray-900\">配置 {editingAIModel.name}</div>\n <div className=\"text-xs text-gray-400 mt-0.5\">{editingAIModel.provider} · {editingAIModel.type}</div>\n </div>\n <button onClick={() => { setShowAIConfigModal(false); setEditingAIModel(null); }} className=\"p-1 hover:bg-gray-100 rounded-lg\"><X className=\"w-5 h-5 text-gray-400\" /></button>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">API Key</label>\n <div className=\"relative\">\n <input type={showAIKeyValue[editingAIModel.name] ? \"text\" : \"password\"}\n value={editingAIModel.apiKey}\n onChange={e => setEditingAIModel({ ...editingAIModel, apiKey: e.target.value })}\n placeholder=\"请输入 API Key(如 sk-xxxxxxxxxxxxxxxx)\"\n className=\"w-full px-3 py-2 pr-10 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n <button type=\"button\" onClick={() => setShowAIKeyValue(prev => ({ ...prev, [editingAIModel.name]: !prev[editingAIModel.name] }))}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600\">\n {showAIKeyValue[editingAIModel.name] ? <EyeOff className=\"w-4 h-4\" /> : <Eye className=\"w-4 h-4\" />}\n </button>\n </div>\n <p className=\"text-xs text-gray-400 mt-1\">API Key 将加密保存,不会在页面中明文展示</p>\n </div>\n <div className=\"bg-blue-50 rounded-lg p-3 text-xs text-blue-700\">\n 💡 获取 API Key:前往 {editingAIModel.provider} 官网 → 控制台 → API 密钥管理\n </div>\n </div>\n <div className=\"flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200\">\n <button onClick={() => { setShowAIConfigModal(false); setEditingAIModel(null); }} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm text-gray-600 hover:bg-gray-50\">取消</button>\n <button onClick={handleSaveAIModel} disabled={!editingAIModel.apiKey.trim()} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\">保存配置</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 添加模型弹窗 ═══════════════ */}\n {showAddModelModal && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-2xl w-full max-w-md mx-4 shadow-2xl\">\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-gray-200\">\n <span className=\"font-semibold text-gray-900\">添加 AI 模型</span>\n <button onClick={() => setShowAddModelModal(false)} className=\"p-1 hover:bg-gray-100 rounded-lg\"><X className=\"w-5 h-5 text-gray-400\" /></button>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">模型名称</label>\n <input type=\"text\" value={newModel.name} onChange={e => setNewModel({ ...newModel, name: e.target.value })} placeholder=\"例如:GPT-4o、Kimi-k2...\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">提供商</label>\n <select value={newModel.provider} onChange={e => setNewModel({ ...newModel, provider: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>DeepSeek</option><option>阿里云</option><option>智谱AI</option><option>月之暗面</option><option>百度文心</option><option>OpenAI</option><option>Anthropic</option><option>其他</option>\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">模型类型</label>\n <select value={newModel.type} onChange={e => setNewModel({ ...newModel, type: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>对话模型</option><option>推理模型</option><option>快速模型</option><option>多模态模型</option>\n </select>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">API Key</label>\n <input type=\"password\" value={newModel.apiKey} onChange={e => setNewModel({ ...newModel, apiKey: e.target.value })} placeholder=\"请输入 API Key\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n </div>\n <div className=\"flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200\">\n <button onClick={() => setShowAddModelModal(false)} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm text-gray-600 hover:bg-gray-50\">取消</button>\n <button onClick={handleSaveNewModel} disabled={!newModel.name.trim() || !newModel.apiKey.trim()} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\">添加</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 新建 API 密钥弹窗 ═══════════════ */}\n {showNewKeyModal && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl p-6 w-full max-w-md shadow-2xl\">\n <div className=\"flex items-center gap-3 mb-4\">\n <div className=\"w-10 h-10 bg-green-100 rounded-full flex items-center justify-center\">\n <CheckCircle2 className=\"w-5 h-5 text-green-600\" />\n </div>\n <div>\n <h3 className=\"font-semibold text-gray-900\">密钥已生成</h3>\n <p className=\"text-sm text-gray-500\">请立即复制保存,此密钥仅显示一次</p>\n </div>\n </div>\n <div className=\"bg-gray-50 rounded-lg p-3 mb-4 flex items-center gap-2\">\n <code className=\"text-sm break-all flex-1\">{generatedKey}</code>\n <button onClick={() => copyKey(generatedKey)} className=\"text-gray-400 hover:text-gray-600 flex-shrink-0\"><Copy className=\"w-4 h-4\" /></button>\n </div>\n <div className=\"mb-4\">\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">密钥名称</label>\n <input value={newKeyName} onChange={e => setNewKeyName(e.target.value)} placeholder=\"例如:生产环境\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-blue-400\" />\n </div>\n <div className=\"flex gap-3\">\n <button onClick={() => setShowNewKeyModal(false)} className=\"flex-1 px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={saveNewKey} disabled={!newKeyName.trim()} className=\"flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50\">保存密钥</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 导出确认弹窗 ═══════════════ */}\n {showExportConfirm && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl p-6 w-[420px] shadow-2xl\">\n <div className=\"flex items-center gap-3 mb-4\">\n <div className=\"w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center\">\n <Download className=\"w-5 h-5 text-blue-600\" />\n </div>\n <div>\n <h3 className=\"font-semibold text-gray-900\">确认导出</h3>\n <p className=\"text-sm text-gray-500\">即将导出 <span className=\"font-medium\">{showExportConfirm.name}</span></p>\n </div>\n </div>\n <div className=\"bg-gray-50 rounded-lg p-3 mb-4 text-sm text-gray-600 space-y-1\">\n <div>格式:<span className=\"font-medium\">{showExportConfirm.format}</span></div>\n <div>导出结果将通过邮件发送,请注意查收</div>\n </div>\n <div className=\"flex gap-3\">\n <button onClick={() => setShowExportConfirm(null)} className=\"flex-1 px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={confirmExport} className=\"flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">确认导出</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 清理确认弹窗 ═══════════════ */}\n {showCleanConfirm && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl p-6 w-[420px] shadow-2xl\">\n <div className=\"flex items-center gap-3 mb-4\">\n <div className=\"w-10 h-10 bg-red-100 rounded-full flex items-center justify-center\">\n <AlertTriangle className=\"w-5 h-5 text-red-600\" />\n </div>\n <div>\n <h3 className=\"font-semibold text-gray-900\">{showCleanConfirm.name}</h3>\n <p className=\"text-sm text-gray-500\">此操作不可恢复,请谨慎操作</p>\n </div>\n </div>\n <p className=\"text-sm text-gray-600 mb-5 bg-red-50 rounded-lg p-3\">{showCleanConfirm.desc}</p>\n <div className=\"flex gap-3\">\n <button onClick={() => setShowCleanConfirm(null)} className=\"flex-1 px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={confirmClean} className=\"flex-1 px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700\">确认清理</button>\n </div>\n </div>\n </div>\n )}\n\n </PageLayout>\n );\n}\n"}}}} |