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
This commit is contained in:
158
desktop/src/components/SaaS/SaaSSettings.tsx
Normal file
158
desktop/src/components/SaaS/SaaSSettings.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { useState } from 'react';
|
||||
import { useSaaSStore } from '../../store/saasStore';
|
||||
import { SaaSLogin } from './SaaSLogin';
|
||||
import { SaaSStatus } from './SaaSStatus';
|
||||
import { Cloud, Info } from 'lucide-react';
|
||||
|
||||
export function SaaSSettings() {
|
||||
const isLoggedIn = useSaaSStore((s) => s.isLoggedIn);
|
||||
const account = useSaaSStore((s) => s.account);
|
||||
const saasUrl = useSaaSStore((s) => s.saasUrl);
|
||||
const connectionMode = useSaaSStore((s) => s.connectionMode);
|
||||
const login = useSaaSStore((s) => s.login);
|
||||
const register = useSaaSStore((s) => s.register);
|
||||
const logout = useSaaSStore((s) => s.logout);
|
||||
|
||||
const [showLogin, setShowLogin] = useState(!isLoggedIn);
|
||||
const [loginError, setLoginError] = useState<string | null>(null);
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
|
||||
const handleLogin = async (url: string, username: string, password: string) => {
|
||||
setIsLoggingIn(true);
|
||||
setLoginError(null);
|
||||
try {
|
||||
await login(url, username, password);
|
||||
setShowLogin(false);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '登录失败';
|
||||
setLoginError(message);
|
||||
} finally {
|
||||
setIsLoggingIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async (
|
||||
url: string,
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
) => {
|
||||
setIsLoggingIn(true);
|
||||
setLoginError(null);
|
||||
try {
|
||||
await register(url, username, email, password, displayName);
|
||||
// register auto-logs in, no need for separate login call
|
||||
setShowLogin(false);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '注册失败';
|
||||
setLoginError(message);
|
||||
} finally {
|
||||
setIsLoggingIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
setShowLogin(true);
|
||||
setLoginError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-9 h-9 rounded-lg bg-emerald-100 flex items-center justify-center">
|
||||
<Cloud className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">SaaS 账号</h1>
|
||||
<p className="text-sm text-gray-500">管理 ZCLAW 云端平台连接</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection mode info */}
|
||||
<div className="flex items-start gap-2 text-sm text-gray-500 bg-blue-50 rounded-lg border border-blue-100 p-3 mb-5">
|
||||
<Info className="w-4 h-4 mt-0.5 text-blue-500 flex-shrink-0" />
|
||||
<span>
|
||||
当前模式: <strong className="text-gray-700">{connectionMode === 'saas' ? 'SaaS 云端' : connectionMode === 'gateway' ? 'Gateway' : '本地 Tauri'}</strong>。
|
||||
{connectionMode !== 'saas' && '连接 SaaS 平台可解锁云端同步、团队协作等高级功能。'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Login form or status display */}
|
||||
{!showLogin ? (
|
||||
<SaaSStatus
|
||||
isLoggedIn={isLoggedIn}
|
||||
account={account}
|
||||
saasUrl={saasUrl}
|
||||
onLogout={handleLogout}
|
||||
onLogin={() => setShowLogin(true)}
|
||||
/>
|
||||
) : (
|
||||
<SaaSLogin
|
||||
onLogin={handleLogin}
|
||||
onRegister={handleRegister}
|
||||
initialUrl={saasUrl}
|
||||
isLoggingIn={isLoggingIn}
|
||||
error={loginError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Features list when logged in */}
|
||||
{isLoggedIn && !showLogin && (
|
||||
<div className="mt-6">
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-3">
|
||||
云端功能
|
||||
</h2>
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
|
||||
<div className="space-y-3">
|
||||
<CloudFeatureRow
|
||||
name="云端同步"
|
||||
description="对话记录和配置自动同步到云端"
|
||||
status="active"
|
||||
/>
|
||||
<CloudFeatureRow
|
||||
name="团队协作"
|
||||
description="与团队成员共享 Agent 和技能"
|
||||
status={account?.role === 'admin' || account?.role === 'pro' ? 'active' : 'inactive'}
|
||||
/>
|
||||
<CloudFeatureRow
|
||||
name="高级分析"
|
||||
description="使用统计和用量分析仪表板"
|
||||
status={account?.role === 'admin' || account?.role === 'pro' ? 'active' : 'inactive'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CloudFeatureRow({
|
||||
name,
|
||||
description,
|
||||
status,
|
||||
}: {
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'active' | 'inactive';
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{name}</div>
|
||||
<div className="text-xs text-gray-500">{description}</div>
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
status === 'active'
|
||||
? 'bg-emerald-100 text-emerald-700'
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{status === 'active' ? '可用' : '需要订阅'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user