Files
zclaw_openfang/desktop/src/components/Settings/SettingsLayout.tsx
iven 15450ca895 feat(saas): Phase 3 桌面端 SaaS 集成 — 客户端、Store、UI、LLM 适配器
- saas-client.ts: SaaS HTTP 客户端 (登录/注册/Token/模型列表/Chat Relay/配置同步)
- saasStore.ts: Zustand 状态管理 (登录态、连接模式、可用模型、localStorage 持久化)
- connectionStore.ts: 集成 SaaS 模式分支 (connect() 优先检查 SaaS 连接模式)
- llm-service.ts: SaasLLMAdapter 实现 (通过 SaaS Relay 代理 LLM 调用)
- SaaSLogin.tsx: 登录/注册表单 (服务器地址、用户名、密码、邮箱)
- SaaSStatus.tsx: 连接状态展示 (账号信息、健康检查、可用模型列表)
- SaaSSettings.tsx: SaaS 设置页面入口 (登录态切换、功能列表)
- SettingsLayout.tsx: 添加 SaaS 平台菜单项
- store/index.ts: 导出 useSaaSStore
2026-03-27 14:21:23 +08:00

223 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { useSecurityStore } from '../../store/securityStore';
import {
Settings as SettingsIcon,
BarChart3,
Puzzle,
MessageSquare,
FolderOpen,
Shield,
Info,
ArrowLeft,
Coins,
Cpu,
Zap,
HelpCircle,
ClipboardList,
Clock,
Heart,
Key,
Database,
Cloud,
} from 'lucide-react';
import { silentErrorHandler } from '../../lib/error-utils';
import { General } from './General';
import { UsageStats } from './UsageStats';
import { ModelsAPI } from './ModelsAPI';
import { MCPServices } from './MCPServices';
import { Skills } from './Skills';
import { IMChannels } from './IMChannels';
import { Workspace } from './Workspace';
import { Privacy } from './Privacy';
import { About } from './About';
import { Credits } from './Credits';
import { AuditLogsPanel } from '../AuditLogsPanel';
import { SecurityStatus } from '../SecurityStatus';
import { SecurityLayersPanel } from '../SecurityLayersPanel';
import { TaskList } from '../TaskList';
import { HeartbeatConfig } from '../HeartbeatConfig';
import { SecureStorage } from './SecureStorage';
import { VikingPanel } from '../VikingPanel';
import { SaaSSettings } from '../SaaS/SaaSSettings';
interface SettingsLayoutProps {
onBack: () => void;
}
type SettingsPage =
| 'general'
| 'usage'
| 'credits'
| 'models'
| 'mcp'
| 'skills'
| 'im'
| 'workspace'
| 'privacy'
| 'security'
| 'storage'
| 'saas'
| 'viking'
| 'audit'
| 'tasks'
| 'heartbeat'
| 'feedback'
| 'about';
const menuItems: { id: SettingsPage; label: string; icon: React.ReactNode }[] = [
{ id: 'general', label: '通用', icon: <SettingsIcon className="w-4 h-4" /> },
{ id: 'usage', label: '用量统计', icon: <BarChart3 className="w-4 h-4" /> },
{ id: 'credits', label: '积分详情', icon: <Coins className="w-4 h-4" /> },
{ id: 'models', label: '模型与 API', icon: <Cpu className="w-4 h-4" /> },
{ id: 'mcp', label: 'MCP 服务', icon: <Puzzle className="w-4 h-4" /> },
{ id: 'skills', label: '技能', icon: <Zap className="w-4 h-4" /> },
{ id: 'im', label: 'IM 频道', icon: <MessageSquare className="w-4 h-4" /> },
{ id: 'workspace', label: '工作区', icon: <FolderOpen className="w-4 h-4" /> },
{ id: 'privacy', label: '数据与隐私', icon: <Shield className="w-4 h-4" /> },
{ id: 'storage', label: '安全存储', icon: <Key className="w-4 h-4" /> },
{ id: 'saas', label: 'SaaS 平台', icon: <Cloud className="w-4 h-4" /> },
{ id: 'viking', label: '语义记忆', icon: <Database className="w-4 h-4" /> },
{ id: 'security', label: '安全状态', icon: <Shield className="w-4 h-4" /> },
{ id: 'audit', label: '审计日志', icon: <ClipboardList className="w-4 h-4" /> },
{ id: 'tasks', label: '定时任务', icon: <Clock className="w-4 h-4" /> },
{ id: 'heartbeat', label: '心跳配置', icon: <Heart className="w-4 h-4" /> },
{ id: 'feedback', label: '提交反馈', icon: <HelpCircle className="w-4 h-4" /> },
{ id: 'about', label: '关于', icon: <Info className="w-4 h-4" /> },
];
export function SettingsLayout({ onBack }: SettingsLayoutProps) {
const [activePage, setActivePage] = useState<SettingsPage>('general');
const securityStatus = useSecurityStore((s) => s.securityStatus);
const renderPage = () => {
switch (activePage) {
case 'general': return <General />;
case 'usage': return <UsageStats />;
case 'credits': return <Credits />;
case 'models': return <ModelsAPI />;
case 'mcp': return <MCPServices />;
case 'skills': return <Skills />;
case 'im': return <IMChannels />;
case 'workspace': return <Workspace />;
case 'privacy': return <Privacy />;
case 'storage': return <SecureStorage />;
case 'saas': return <SaaSSettings />;
case 'security': return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-bold text-gray-900 mb-4"></h1>
<SecurityStatus />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4"></h2>
<SecurityLayersPanel
status={securityStatus || {
layers: [],
enabledCount: 0,
totalCount: 16,
securityLevel: 'low',
}}
/>
</div>
</div>
);
case 'audit': return <AuditLogsPanel />;
case 'tasks': return (
<div className="max-w-3xl">
<h1 className="text-xl font-bold text-gray-900 mb-6"></h1>
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
<TaskList />
</div>
</div>
);
case 'heartbeat': return (
<div className="max-w-3xl h-full">
<HeartbeatConfig />
</div>
);
case 'viking': return <VikingPanel />;
case 'feedback': return <Feedback />;
case 'about': return <About />;
default: return <General />;
}
};
return (
<div className="h-screen flex bg-f9fafb overflow-hidden text-gray-800 text-sm">
{/* Left navigation */}
<aside className="w-64 bg-gray-50 border-r border-gray-200 flex flex-col flex-shrink-0">
{/* 返回按钮 */}
<div className="p-4 border-b border-gray-200">
<button
onClick={onBack}
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
<span></span>
</button>
</div>
{/* 导航菜单 */}
<nav className="flex-1 overflow-y-auto custom-scrollbar py-2 px-3 space-y-1">
{menuItems.map((item) => (
<button
key={item.id}
onClick={() => setActivePage(item.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all ${
activePage === item.id
? 'bg-gray-200 text-gray-900 font-medium'
: 'text-gray-500 hover:bg-black/5 hover:text-gray-700'
}`}
>
{item.icon}
<span>{item.label}</span>
</button>
))}
</nav>
</aside>
{/* Main content */}
<main className="flex-1 overflow-y-auto custom-scrollbar bg-white p-8">
{renderPage()}
</main>
</div>
);
}
// Simple feedback page (inline)
function Feedback() {
const [text, setText] = useState('');
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(text.trim());
setCopied(true);
};
return (
<div className="max-w-3xl">
<h1 className="text-xl font-bold text-gray-900 mb-6"></h1>
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
<p className="text-sm text-gray-500 mb-4">线</p>
<textarea
value={text}
onChange={(e) => {
setText(e.target.value);
if (copied) {
setCopied(false);
}
}}
placeholder="请尽量详细描述复现步骤、期望结果和实际结果"
className="w-full h-40 border border-gray-300 rounded-lg p-3 text-sm resize-none focus:outline-none focus:border-orange-400"
/>
<button
onClick={() => { handleCopy().catch(silentErrorHandler('SettingsLayout')); }}
disabled={!text.trim()}
className="mt-4 px-6 py-2 bg-orange-500 text-white text-sm rounded-lg hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{copied ? '已复制' : '复制反馈内容'}
</button>
</div>
</div>
);
}