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:
iven
2026-03-27 14:21:23 +08:00
parent a66b675675
commit 15450ca895
9 changed files with 1407 additions and 1 deletions

View 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>
);
}