'use client' import React, { useEffect, useState, useCallback } from 'react' import { API_BASE } from '@/lib/apiBase' // ---- Types ---- interface Tenant { id: string; companyName: string; contactName: string | null; contactEmail: string | null plan: string; status: string; validFrom: string; validUntil: string platformType: string | null; platformUrl: string | null features: string | null; usageQuotas: string | null createdAt: string; updatedAt: string; _daysUntilExpiry?: number; _userCount?: number } interface User { id: string; email: string | null; username: string | null; name: string | null; role: string; isActive: boolean tenantId: string | null; tenant: { companyName: string } | null; createdAt: string } interface SystemConfig { id: string; key: string; value: string; group: string; description: string | null createdAt: string; updatedAt: string } interface Stats { overview: { tenantCount: number; userCount: number; productCount: number; leadCount: number; imageCount: number; inquiryCount: number } perTenant: { id: string; companyName: string; plan: string; status: string; validUntil: string userCount: number quotas: { monthlyLeads: number; monthlyImages: number; monthlyProducts: number; monthlyComparisons: number; maxUsers: number } usage: { leads: number; images: number; products: number; comparisons: number; users: number } }[] } interface Features { inquiry: boolean; imageGen: boolean; product: boolean; comparison: boolean } interface UsageQuotas { monthlyLeads: number; monthlyImages: number; monthlyProducts: number; monthlyComparisons: number; maxUsers: number } const DEFAULT_FEATURES: Features = { inquiry: true, imageGen: true, product: true, comparison: true } const DEFAULT_QUOTAS: UsageQuotas = { monthlyLeads: 100, monthlyImages: 50, monthlyProducts: 100, monthlyComparisons: 50, maxUsers: 5 } type Tab = 'tenants' | 'users' | 'stats' | 'configs' | 'plans' const PLAN_LABELS: Record = { basic: '基础版', pro: '专业版', enterprise: '旗舰版' } const PLAN_COLORS: Record = { basic: 'bg-gray-100 text-gray-700', pro: 'bg-blue-50 text-blue-700', enterprise: 'bg-purple-50 text-purple-700' } const STATUS_LABELS: Record = { active: '正常', inactive: '已停用', expired: '已过期' } const STATUS_COLORS: Record = { active: 'bg-green-50 text-green-700', inactive: 'bg-red-50 text-red-700', expired: 'bg-yellow-50 text-yellow-700' } const PLATFORM_LABELS: Record = { jubian: '巨变平台', custom: '自建平台' } const FEATURE_LABELS: Record = { inquiry: '询价发布', imageGen: '商品生图', product: '商品上架', comparison: '智能比价' } const CONFIG_GROUPS: Record = { ai: 'AI 模型', platform: '平台对接', system: '系统参数' } const API = `${API_BASE}/api/admin` const authHeaders = (): Record => { const auth = localStorage.getItem('auth') return { 'Content-Type': 'application/json', Authorization: `Bearer ${auth ? JSON.parse(auth).token : ''}` } } function toastManager() { const [toast, setToast] = useState('') const show = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 2500) } return { toast, show } } export default function AdminPage({ section }: { section: Tab }) { const { toast, show: showToast } = toastManager() return (
{toast &&
{toast}
}

运营后台

平台管理控制台

{section === 'tenants' && } {section === 'users' && } {section === 'stats' && } {section === 'configs' && } {section === 'plans' && }
) } // ====================== Tenant Tab ====================== function TenantTab({ showToast }: { showToast: (m: string) => void }) { const [tenants, setTenants] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [showForm, setShowForm] = useState(false) const [editingId, setEditingId] = useState(null) const [form, setForm] = useState({ companyName: '', contactName: '', contactEmail: '', plan: 'basic', status: 'active', validFrom: '', validUntil: '', platformType: 'jubian', platformUrl: '', platformApiKey: '' }) const [features, setFeatures] = useState(DEFAULT_FEATURES) const [quotas, setQuotas] = useState(DEFAULT_QUOTAS) const [adminUsername, setAdminUsername] = useState('') const [adminPassword, setAdminPassword] = useState('') const [adminName, setAdminName] = useState('') const [saving, setSaving] = useState(false) const [planConfig, setPlanConfig] = useState | null>(null) const [userManager, setUserManager] = useState<{ tenantId: string; companyName: string } | null>(null) const loadTenants = useCallback(async () => { try { const res = await fetch(`${API}/tenants`, { headers: authHeaders() }) if (res.ok) setTenants(await res.json()) } catch { showToast('加载失败') } finally { setLoading(false) } }, [showToast]) useEffect(() => { loadTenants() }, [loadTenants]) useEffect(() => { fetch(`${API}/plan-config`, { headers: authHeaders() }) .then((r) => r.ok && r.json()) .then((cfg) => setPlanConfig(cfg)) .catch(() => {}) }, []) const filtered = tenants.filter((t) => [t.companyName, t.contactName, t.contactEmail].some((v) => v?.toLowerCase().includes(search.toLowerCase())) ) const openCreate = () => { setForm({ companyName: '', contactName: '', contactEmail: '', plan: 'basic', status: 'active', validFrom: '', validUntil: '', platformType: 'jubian', platformUrl: '', platformApiKey: '' }) const q = planConfig?.basic?.quotas || DEFAULT_QUOTAS setFeatures(planConfig?.basic?.features || DEFAULT_FEATURES); setQuotas(q); setEditingId(null); setShowForm(true) setAdminUsername(''); setAdminPassword(''); setAdminName('') } const openEdit = (t: Tenant) => { setForm({ companyName: t.companyName, contactName: t.contactName || '', contactEmail: t.contactEmail || '', plan: t.plan, status: t.status, validFrom: t.validFrom?.slice(0, 10) || '', validUntil: t.validUntil?.slice(0, 10) || '', platformType: t.platformType || 'jubian', platformUrl: t.platformUrl || '', platformApiKey: '', }) try { setFeatures(t.features ? JSON.parse(t.features) : DEFAULT_FEATURES) } catch { setFeatures(DEFAULT_FEATURES) } try { setQuotas(t.usageQuotas ? JSON.parse(t.usageQuotas) : DEFAULT_QUOTAS) } catch { setQuotas(DEFAULT_QUOTAS) } setEditingId(t.id); setShowForm(true) } const handleSave = async () => { if (!form.companyName || !form.validUntil) { showToast('请填写公司名和有效期'); return } setSaving(true) try { const body = { companyName: form.companyName, contactName: form.contactName || undefined, contactEmail: form.contactEmail || undefined, plan: form.plan, status: form.status, validFrom: form.validFrom || undefined, validUntil: form.validUntil, features: JSON.stringify(features), usageQuotas: JSON.stringify(quotas), } if (!editingId) { Object.assign(body, { platformApiKey: form.platformApiKey || '', adminUsername: adminUsername || undefined, adminPassword: adminPassword || undefined, adminName: adminName || undefined }) } else { Object.assign(body, { platformType: form.platformType, platformUrl: form.platformUrl, platformApiKey: form.platformApiKey }) } const res = await fetch(editingId ? `${API}/tenants/${editingId}` : `${API}/tenants`, { method: editingId ? 'PATCH' : 'POST', headers: authHeaders(), body: JSON.stringify(body), }) if (!res.ok) { showToast('保存失败'); return } showToast(editingId ? '更新成功' : '创建成功'); setShowForm(false); loadTenants() } catch { showToast('保存失败') } finally { setSaving(false) } } const handleDelete = async (id: string) => { if (!confirm('确定删除该租户?')) return const res = await fetch(`${API}/tenants/${id}`, { method: 'DELETE', headers: authHeaders() }) if (res.ok) { showToast('删除成功'); loadTenants() } else showToast('删除失败') } const expiryBadge = (days: number | undefined) => { if (days === undefined) return null if (days < 0) return 已过期 {Math.abs(days)}天 if (days <= 7) return 即将到期 {days}天 if (days <= 30) return {days}天后到期 return {days}天后到期 } const fmt = (d: string) => d ? new Date(d).toLocaleDateString('zh-CN') : '-' return (
setSearch(e.target.value)} className="w-full max-w-md px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
{loading ? ( ) : filtered.length === 0 ? ( ) : filtered.map((t) => { const feat = (() => { try { return JSON.parse(t.features || '{}') as Features } catch { return {} as Features } })() return ( ) })}
公司名 订阅版本 状态 有效期 / 到期提醒 联系人 功能 对接平台 操作
加载中...
暂无租户数据
{t.companyName} {PLAN_LABELS[t.plan] || t.plan} {STATUS_LABELS[t.status] || t.status}
{fmt(t.validFrom)} ~ {fmt(t.validUntil)}
{expiryBadge(t._daysUntilExpiry)}
{t.contactName || '-'}
{t.contactEmail &&
{t.contactEmail}
}
{(Object.entries(FEATURE_LABELS) as [keyof Features, string][]).map(([k, label]) => ( {label} ))}
{PLATFORM_LABELS[t.platformType || ''] || t.platformType || '-'}
{showForm && (
setShowForm(false)}>
e.stopPropagation()}>

{editingId ? '编辑租户' : '新建租户'}

{/* 基本信息 */}
setForm({ ...form, companyName: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setForm({ ...form, contactName: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setForm({ ...form, contactEmail: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
{/* 订阅版本 & 状态 */}
setForm({ ...form, validFrom: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setForm({ ...form, validUntil: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
{/* 平台对接配置 */}

平台对接配置

setForm({ ...form, platformUrl: e.target.value })} placeholder="https://api.example.com" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setForm({ ...form, platformApiKey: e.target.value })} placeholder="用于平台对接的 API 密钥" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
{!editingId && (

管理员账号(创建时可选)

setAdminUsername(e.target.value)} placeholder="admin" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setAdminPassword(e.target.value)} placeholder="至少6位" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setAdminName(e.target.value)} placeholder="可选,默认显示'公司名管理员'" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)} {/* 功能权限 */}

功能权限

{(Object.entries(FEATURE_LABELS) as [keyof Features, string][]).map(([k, label]) => ( ))}
{/* 用量配额 */}

用量配额

setQuotas({ ...quotas, monthlyLeads: +e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setQuotas({ ...quotas, monthlyImages: +e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setQuotas({ ...quotas, monthlyProducts: +e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setQuotas({ ...quotas, monthlyComparisons: +e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setQuotas({ ...quotas, maxUsers: +e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)} {/* User Manager Modal */} {userManager && setUserManager(null)} showToast={showToast} />}
) } // ====================== Tenant User Manager ====================== function TenantUserManager({ tenantId, companyName, onClose, showToast }: { tenantId: string; companyName: string; onClose: () => void; showToast: (m: string) => void }) { const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) const [showForm, setShowForm] = useState(false) const [form, setForm] = useState({ username: '', password: '', name: '' }) const [saving, setSaving] = useState(false) const [resetPwd, setResetPwd] = useState<{ id: string; name: string } | null>(null) const [newPwd, setNewPwd] = useState('') const loadUsers = useCallback(async () => { try { const res = await fetch(`${API}/users`, { headers: authHeaders() }) if (res.ok) { const all: User[] = await res.json() setUsers(all.filter((u) => u.tenantId === tenantId)) } } catch { showToast('加载失败') } finally { setLoading(false) } }, [tenantId, showToast]) useEffect(() => { loadUsers() }, [loadUsers]) const handleCreate = async () => { if (!form.username || !form.password) { showToast('请填写用户名和密码'); return } if (form.password.length < 6) { showToast('密码至少6位'); return } setSaving(true) try { const res = await fetch(`${API}/users`, { method: 'POST', headers: authHeaders(), body: JSON.stringify({ ...form, tenantId }), }) if (res.ok) { showToast('创建成功'); setShowForm(false); setForm({ username: '', password: '', name: '' }); loadUsers() } else { const err = await res.json(); showToast(err.message || '创建失败') } } catch { showToast('创建失败') } finally { setSaving(false) } } const handleResetPwd = async () => { if (!resetPwd || !newPwd || newPwd.length < 6) { showToast('密码至少6位'); return } setSaving(true) try { const res = await fetch(`${API}/users/${resetPwd.id}/reset-password`, { method: 'POST', headers: authHeaders(), body: JSON.stringify({ password: newPwd }), }) if (res.ok) { showToast('密码重置成功'); setResetPwd(null); setNewPwd(''); loadUsers() } else showToast('重置失败') } catch { showToast('重置失败') } finally { setSaving(false) } } const handleToggleActive = async (id: string) => { const res = await fetch(`${API}/users/${id}/toggle-active`, { method: 'POST', headers: authHeaders() }) if (res.ok) loadUsers(); else showToast('操作失败') } return (
e.stopPropagation()}>

用户管理

{companyName}

共 {users.length} 个用户
{loading ? (
加载中...
) : users.length === 0 ? (
暂无用户
) : ( {users.map((u) => ( ))}
姓名 用户名 角色 状态 创建时间 操作
{u.name || '-'} {u.username || u.email} {u.role === 'admin' ? '管理员' : '普通用户'} {u.isActive ? '正常' : '已禁用'} {u.createdAt ? new Date(u.createdAt).toLocaleDateString('zh-CN') : '-'}
)}
{/* Create user form */} {showForm && (
setShowForm(false)}>
e.stopPropagation()}>

新增用户 — {companyName}

setForm({ ...form, username: e.target.value })} placeholder="请输入用户名" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setForm({ ...form, password: e.target.value })} placeholder="至少6位" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setForm({ ...form, name: e.target.value })} placeholder="可选" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)} {/* Reset password */} {resetPwd && (
setResetPwd(null)}>
e.stopPropagation()}>

重置密码

用户:{resetPwd.name}

setNewPwd(e.target.value)} placeholder="至少6位" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)}
) } // ====================== User Tab ====================== function UserTab({ showToast }: { showToast: (m: string) => void }) { const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [resetPwd, setResetPwd] = useState<{ id: string; name: string } | null>(null) const [newPwd, setNewPwd] = useState('') const [saving, setSaving] = useState(false) const [showUserForm, setShowUserForm] = useState(false) const [userForm, setUserForm] = useState({ username: '', password: '', name: '', tenantId: '' }) const [tenants, setTenants] = useState([]) const [userSaving, setUserSaving] = useState(false) const loadUsers = useCallback(async () => { try { const res = await fetch(`${API}/users`, { headers: authHeaders() }) if (res.ok) setUsers(await res.json()) } catch { showToast('加载失败') } finally { setLoading(false) } }, [showToast]) useEffect(() => { loadUsers() }, [loadUsers]) useEffect(() => { fetch(`${API}/tenants`, { headers: authHeaders() }).then(r => r.ok && r.json()).then(setTenants) }, []) const handleCreateUser = async () => { if (!userForm.username || !userForm.password) { showToast('请填写用户名和密码'); return } if (userForm.password.length < 6) { showToast('密码至少6位'); return } setUserSaving(true) try { const res = await fetch(`${API}/users`, { method: 'POST', headers: authHeaders(), body: JSON.stringify(userForm), }) if (res.ok) { showToast('创建成功'); setShowUserForm(false); setUserForm({ username: '', password: '', name: '', tenantId: '' }); loadUsers() } else { const err = await res.json(); showToast(err.message || '创建失败') } } catch { showToast('创建失败') } finally { setUserSaving(false) } } const filtered = users.filter((u) => [u.username, u.email, u.name, u.tenant?.companyName].some((v) => v?.toLowerCase().includes(search.toLowerCase())) ) const handleResetPwd = async () => { if (!resetPwd || !newPwd || newPwd.length < 6) { showToast('密码至少6位'); return } setSaving(true) try { const res = await fetch(`${API}/users/${resetPwd.id}/reset-password`, { method: 'POST', headers: authHeaders(), body: JSON.stringify({ password: newPwd }), }) if (res.ok) { showToast('密码重置成功'); setResetPwd(null); setNewPwd('') } else showToast('重置失败') } catch { showToast('重置失败') } finally { setSaving(false) } } const handleToggleActive = async (id: string) => { const res = await fetch(`${API}/users/${id}/toggle-active`, { method: 'POST', headers: authHeaders() }) if (res.ok) loadUsers(); else showToast('操作失败') } return (
setSearch(e.target.value)} className="w-full max-w-md px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
{loading ? ( ) : filtered.length === 0 ? ( ) : filtered.map((u) => ( ))}
姓名 邮箱 角色 所属租户 状态 操作
加载中...
暂无用户数据
{u.name || '-'} {u.username || u.email} {u.role === 'admin' ? '管理员' : '普通用户'} {u.tenant?.companyName || '-'} {u.isActive ? '正常' : '已禁用'}
{showUserForm && (
setShowUserForm(false)}>
e.stopPropagation()}>

新增用户

setUserForm({ ...userForm, username: e.target.value })} placeholder="请输入用户名" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setUserForm({ ...userForm, password: e.target.value })} placeholder="至少6位" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setUserForm({ ...userForm, name: e.target.value })} placeholder="可选" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)} {resetPwd && (
setResetPwd(null)}>
e.stopPropagation()}>

重置密码

用户:{resetPwd.name}

setNewPwd(e.target.value)} placeholder="至少6位" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)}
) } // ====================== Stats Tab ====================== function StatsTab() { const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('all') const [selectedTenant, setSelectedTenant] = useState(null) useEffect(() => { (async () => { try { const res = await fetch(`${API}/stats`, { headers: authHeaders() }) if (res.ok) setStats(await res.json()) } catch {} finally { setLoading(false) } })() }, []) if (loading) return
加载中...
if (!stats) return
暂无数据
const cards = [ { label: '租户总数', value: stats.overview.tenantCount, color: 'bg-blue-50 text-blue-700' }, { label: '用户总数', value: stats.overview.userCount, color: 'bg-purple-50 text-purple-700' }, { label: '商品数', value: stats.overview.productCount, color: 'bg-green-50 text-green-700' }, { label: '线索数', value: stats.overview.leadCount, color: 'bg-orange-50 text-orange-700' }, { label: '生成图片', value: stats.overview.imageCount, color: 'bg-pink-50 text-pink-700' }, { label: '询价数', value: stats.overview.inquiryCount, color: 'bg-teal-50 text-teal-700' }, ] const METRICS: { key: keyof Stats['perTenant'][0]['usage']; label: string; quotaKey: keyof Stats['perTenant'][0]['quotas']; desc: string }[] = [ { key: 'users', label: '用户数', quotaKey: 'maxUsers', desc: '已用 / 上限' }, { key: 'products', label: '商品上架', quotaKey: 'monthlyProducts', desc: '已上架 / 月配额' }, { key: 'leads', label: '月线索', quotaKey: 'monthlyLeads', desc: '本月 / 月配额' }, { key: 'images', label: '月生图', quotaKey: 'monthlyImages', desc: '本月 / 月配额' }, { key: 'comparisons', label: '月比价', quotaKey: 'monthlyComparisons', desc: '本月 / 月配额' }, ] function UsageBar({ usage, quota }: { usage: number; quota: number }) { if (quota === 0) return 未开通 const pct = Math.min(usage / quota, 1) const color = pct >= 1 ? 'bg-red-500' : pct >= 0.8 ? 'bg-yellow-500' : 'bg-green-500' return (
{usage} / {quota}
) } const filtered = stats.perTenant.filter((t) => { if (statusFilter !== 'all' && t.status !== statusFilter) return false if (search && !t.companyName.toLowerCase().includes(search.toLowerCase())) return false return true }) // Detail view if (selectedTenant) { const t = selectedTenant return (
{t.companyName} {PLAN_LABELS[t.plan] || t.plan} {STATUS_LABELS[t.status] || t.status} 有效期至 {t.validUntil ? new Date(t.validUntil).toLocaleDateString('zh-CN') : '-'}
{METRICS.map((m) => (
{m.label} {m.desc}
))}
) } // List view return (
{cards.map((c) => (

{c.label}

{c.value}

))}
setSearch(e.target.value)} className="max-w-xs px-3 py-1.5 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" /> 共 {filtered.length} 个租户
{filtered.length === 0 ? ( ) : filtered.map((t) => ( setSelectedTenant(t)}> ))}
企业名称 版本 状态 有效期 用户 商品上架 月线索 月生图 月比价
无匹配租户
{t.companyName} {PLAN_LABELS[t.plan] || t.plan} {STATUS_LABELS[t.status] || t.status} {t.validUntil ? new Date(t.validUntil).toLocaleDateString('zh-CN') : '-'}
) } // ====================== Config Tab ====================== interface Integration { id: string; name: string; type: string; endpoint: string; appId: string | null; apiKey: string; customerCode: string status: string; tools: string | null; lastConnected: string | null; error: string | null createdAt: string; updatedAt: string } const INTEGRATION_API = `${API_BASE}/api/integrations` function ConfigTab({ showToast }: { showToast: (m: string) => void }) { const [configs, setConfigs] = useState([]) const [loading, setLoading] = useState(true) const [editing, setEditing] = useState(null) const [editValue, setEditValue] = useState('') const [newConfig, setNewConfig] = useState({ key: '', value: '', group: 'system', description: '' }) const [showNew, setShowNew] = useState(false) const [saving, setSaving] = useState(false) const [integrations, setIntegrations] = useState([]) const [integrationLoading, setIntegrationLoading] = useState(true) const [showIntegrationForm, setShowIntegrationForm] = useState(false) const [editingIntegration, setEditingIntegration] = useState(null) const [integrationForm, setIntegrationForm] = useState({ name: '', endpoint: 'http://100.111.84.16:3002/mcp', appId: '', apiKey: '', customerCode: '' }) const [testingId, setTestingId] = useState(null) const [connectingId, setConnectingId] = useState(null) const DEVBOX_ADMIN_APPLY_URL = 'http://100.111.84.16:3000/openapi/app' const loadConfigs = useCallback(async () => { try { const res = await fetch(`${API}/configs`, { headers: authHeaders() }) if (res.ok) setConfigs(await res.json()) } catch { showToast('加载失败') } finally { setLoading(false) } }, [showToast]) const loadIntegrations = useCallback(async () => { try { const res = await fetch(INTEGRATION_API, { headers: authHeaders() }) if (res.ok) setIntegrations(await res.json()) } catch { showToast('加载对接配置失败') } finally { setIntegrationLoading(false) } }, [showToast]) useEffect(() => { loadConfigs(); loadIntegrations() }, [loadConfigs, loadIntegrations]) const grouped = configs.reduce((acc, c) => { const subGroup = c.group === 'platform' ? `platform-${c.key.startsWith('jubian') ? 'jubian' : 'custom'}` : c.group if (!acc[subGroup]) acc[subGroup] = [] acc[subGroup].push(c) return acc }, {} as Record) const GROUP_LABELS: Record = { ai: 'AI 模型', 'platform-jubian': '巨变平台配置', 'platform-custom': '自建平台配置', system: '系统参数', } const handleSaveEdit = async () => { if (!editing) return setSaving(true) try { const res = await fetch(`${API}/configs`, { method: 'POST', headers: authHeaders(), body: JSON.stringify({ key: editing.key, value: editValue, group: editing.group, description: editing.description }), }) if (res.ok) { showToast('更新成功'); setEditing(null); loadConfigs() } else showToast('更新失败') } catch { showToast('更新失败') } finally { setSaving(false) } } const handleCreate = async () => { if (!newConfig.key || !newConfig.value) { showToast('请填写键和值'); return } setSaving(true) try { const res = await fetch(`${API}/configs`, { method: 'POST', headers: authHeaders(), body: JSON.stringify(newConfig), }) if (res.ok) { showToast('创建成功'); setShowNew(false); setNewConfig({ key: '', value: '', group: 'system', description: '' }); loadConfigs() } else showToast('创建失败') } catch { showToast('创建失败') } finally { setSaving(false) } } const handleDelete = async (id: string) => { if (!confirm('确定删除此配置?')) return const res = await fetch(`${API}/configs/${id}`, { method: 'DELETE', headers: authHeaders() }) if (res.ok) { showToast('删除成功'); loadConfigs() } else showToast('删除失败') } const handleSaveIntegration = async () => { if (!integrationForm.name || !integrationForm.endpoint) { showToast('请填写名称和 MCP 地址'); return } if (!integrationForm.appId || !integrationForm.apiKey) { showToast('请填写 appId 和 appSecret(先在 devbox 运营后台申请)'); return } setSaving(true) try { const body = { ...integrationForm, type: 'mcp' } const res = await fetch(editingIntegration ? `${INTEGRATION_API}/${editingIntegration.id}` : INTEGRATION_API, { method: editingIntegration ? 'PUT' : 'POST', headers: authHeaders(), body: JSON.stringify(body), }) if (res.ok) { showToast(editingIntegration ? '更新成功' : '创建成功'); setShowIntegrationForm(false); setEditingIntegration(null); loadIntegrations() } else showToast('保存失败') } catch { showToast('保存失败') } finally { setSaving(false) } } const handleDeleteIntegration = async (id: string) => { if (!confirm('确定删除此对接配置?')) return const res = await fetch(`${INTEGRATION_API}/${id}`, { method: 'DELETE', headers: authHeaders() }) if (res.ok) { showToast('删除成功'); loadIntegrations() } else showToast('删除失败') } const handleTestIntegration = async (id: string) => { setTestingId(id) try { const res = await fetch(`${INTEGRATION_API}/${id}/connect`, { method: 'POST', headers: authHeaders() }) const data = await res.json() if (res.ok) { showToast(`测试成功!发现 ${data.tools?.length || 0} 个工具`); loadIntegrations() } else showToast(data.error || data.message || '测试失败') } catch { showToast('测试失败') } finally { setTestingId(null) } } const handleConnectIntegration = async (id: string) => { setConnectingId(id) try { const res = await fetch(`${INTEGRATION_API}/${id}/connect`, { method: 'POST', headers: authHeaders() }) if (res.ok) { showToast('连接成功'); loadIntegrations() } else showToast('连接失败') } catch { showToast('连接失败') } finally { setConnectingId(null) } } const handleDisconnectIntegration = async (id: string) => { try { const res = await fetch(`${INTEGRATION_API}/${id}/disconnect`, { method: 'POST', headers: authHeaders() }) if (res.ok) { showToast('已断开'); loadIntegrations() } else showToast('断开失败') } catch { showToast('断开失败') } } if (loading) return
加载中...
return (

平台全局参数配置,修改后立即生效

MCP 对接配置

{integrationLoading ? (
加载中...
) : integrations.length === 0 ? (
暂无对接配置,点击"新增对接"添加
) : ( {integrations.map((item) => { let toolsList: any[] = [] try { if (item.tools) toolsList = JSON.parse(item.tools) } catch { /* ignore */ } return ( {toolsList.length > 0 && ( )} ) })}
名称 MCP 地址 客户编码 状态 工具数 最后连接 操作
{item.name} {item.endpoint} {item.customerCode || '-'} {item.status === 'active' ? '已连接' : item.status === 'error' ? '错误' : '未连接'} {toolsList.length} {item.lastConnected ? new Date(item.lastConnected).toLocaleString('zh-CN') : '-'}
{item.status === 'active' && ( )} {item.status !== 'active' && ( )}
已加载 Tools: {toolsList.map((t: any) => ( {t.name} ))}
{item.error && (
最近错误: {item.error}
)}
)}
{Object.entries(grouped).map(([group, items]) => (

{GROUP_LABELS[group] || CONFIG_GROUPS[group] || group}

{items.map((c) => ( ))}
配置键 值 说明 操作
{c.key} {c.value ? ( {c.value} ) : 未设置} {c.description || '-'}
))} {/* Edit modal */} {editing && (
setEditing(null)}>
e.stopPropagation()}>

编辑配置

{editing.key}

setEditValue(e.target.value)} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" /> {editing.description &&

{editing.description}

}
)} {showNew && (
setShowNew(false)}>
e.stopPropagation()}>

新增配置

setNewConfig({ ...newConfig, key: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" placeholder="例如: llm_model" />
setNewConfig({ ...newConfig, value: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setNewConfig({ ...newConfig, description: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)} {showIntegrationForm && (
{ setShowIntegrationForm(false); setEditingIntegration(null) }}>
e.stopPropagation()}>

{editingIntegration ? '编辑对接' : '新增对接'}

APIKey 需先在 devbox B2B 运营后台 → 集成管理 → 应用列表 申请获取

setIntegrationForm({ ...integrationForm, name: e.target.value })} placeholder="例如: B2B 平台对接" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setIntegrationForm({ ...integrationForm, endpoint: e.target.value })} placeholder="http://100.111.84.16:3002/mcp" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setIntegrationForm({ ...integrationForm, appId: e.target.value })} placeholder="例如: app_2a66d12104e64b02a195" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm font-mono outline-none focus:border-secondary transition-colors" />

申请后从运营后台"应用列表"详情里复制

setIntegrationForm({ ...integrationForm, apiKey: e.target.value })} placeholder="申请时一次性返回, 仅显示一次, 请妥善保存" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />

AES-256-GCM 加密存储于本地, 不会明文返回列表

setIntegrationForm({ ...integrationForm, customerCode: e.target.value })} placeholder="例如: test-customer-001 (MCP 会从 app 自动识别, 这里仅做备注)" className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)}
) } // ====================== Plan Config Tab ====================== interface PlanEntry { label: string price: number description: string features: Features quotas: UsageQuotas platformCount: number supportLevel: string } const PLAN_KEYS = ['basic', 'pro', 'enterprise'] function PlanConfigTab({ showToast }: { showToast: (m: string) => void }) { const [config, setConfig] = useState | null>(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [editPlan, setEditPlan] = useState(null) const [editData, setEditData] = useState(null) const load = useCallback(async () => { try { const res = await fetch(`${API}/plan-config`, { headers: authHeaders() }) if (res.ok) setConfig(await res.json()) } catch { showToast('加载失败') } finally { setLoading(false) } }, [showToast]) useEffect(() => { load() }, [load]) const openEdit = (key: string) => { if (!config) return setEditPlan(key) setEditData(JSON.parse(JSON.stringify(config[key]))) } const handleSave = async () => { if (!config || !editPlan || !editData) return const updated = { ...config, [editPlan]: editData } setSaving(true) try { const res = await fetch(`${API}/plan-config`, { method: 'PUT', headers: authHeaders(), body: JSON.stringify(updated), }) if (res.ok) { showToast('保存成功'); setConfig(updated); setEditPlan(null) } else showToast('保存失败') } catch { showToast('保存失败') } finally { setSaving(false) } } if (loading) return
加载中...
if (!config) return
暂无数据
return (

配置订阅方案的定价、功能权限和用量配额。修改后创建租户时将自动应用新的默认值。

{PLAN_KEYS.map((key) => { const p = config[key] if (!p) return null return (
{key === 'pro' &&
推荐
}

{p.label}

{PLAN_LABELS[key]}

{p.description}

¥{p.price.toLocaleString()} /年
折合 ¥{Math.round(p.price / 12)}/月
功能权限
{(Object.entries(FEATURE_LABELS) as [keyof Features, string][]).map(([fk, fl]) => ( {fl} ))}
用量配额
月线索: {p.quotas.monthlyLeads >= 99999 ? '不限' : p.quotas.monthlyLeads} 条
月生图: {p.quotas.monthlyImages >= 99999 ? '不限' : p.quotas.monthlyImages} 张
月上架: {p.quotas.monthlyProducts >= 99999 ? '不限' : p.quotas.monthlyProducts} 条
月比价: {p.quotas.monthlyComparisons >= 99999 ? '不限' : p.quotas.monthlyComparisons} 次
用户数: {p.quotas.maxUsers >= 99999 ? '不限' : p.quotas.maxUsers} 人
平台对接: {p.platformCount >= 999 ? '不限' : p.platformCount} 个平台
技术支持: {p.supportLevel}
) })}
{/* Edit modal */} {editPlan && editData && (
setEditPlan(null)}>
e.stopPropagation()}>

编辑方案 — {editData.label}

setEditData({ ...editData, label: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setEditData({ ...editData, price: +e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setEditData({ ...editData, description: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setEditData({ ...editData, platformCount: +e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setEditData({ ...editData, supportLevel: e.target.value })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
{/* Features */}

功能权限

{(Object.entries(FEATURE_LABELS) as [keyof Features, string][]).map(([k, label]) => ( ))}
{/* Quotas */}

用量配额

setEditData({ ...editData, quotas: { ...editData.quotas, monthlyLeads: +e.target.value } })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setEditData({ ...editData, quotas: { ...editData.quotas, monthlyImages: +e.target.value } })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setEditData({ ...editData, quotas: { ...editData.quotas, monthlyProducts: +e.target.value } })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setEditData({ ...editData, quotas: { ...editData.quotas, monthlyComparisons: +e.target.value } })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
setEditData({ ...editData, quotas: { ...editData.quotas, maxUsers: +e.target.value } })} className="w-full px-4 py-2 border border-border-subtle rounded-lg text-sm outline-none focus:border-secondary transition-colors" />
)}
) }