fix(management): 联动对象存储连接状态

This commit is contained in:
yuzhe
2026-07-24 16:24:42 +08:00
parent 2dadeed759
commit fd93c69245
2 changed files with 11 additions and 16 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { CheckCircle2, Cloud, Database, Loader2, LockKeyhole, RadioTower, ShieldCheck } from 'lucide-react'; import { CheckCircle2, Cloud, Database, Loader2, LockKeyhole, RadioTower, ShieldCheck } from 'lucide-react';
import type { StorageConfig } from '@shared/types'; import type { StorageConfig } from '@shared/types';
import { api, ApiError } from '@/api/client'; import { api, ApiError } from '@/api/client';
@@ -8,10 +8,8 @@ const initialForm = {
path_prefix: 'delivery-desk', secret_id: '', secret_key: '', path_prefix: 'delivery-desk', secret_id: '', secret_key: '',
}; };
export default function StorageSettings() { export default function StorageSettings({ items, onItemsChange }: { items: StorageConfig[]; onItemsChange: (items: StorageConfig[]) => void }) {
const [items, setItems] = useState<StorageConfig[]>([]);
const [form, setForm] = useState(initialForm); const [form, setForm] = useState(initialForm);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState<'save' | 'test' | 'activate' | null>(null); const [busy, setBusy] = useState<'save' | 'test' | 'activate' | null>(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [notice, setNotice] = useState(''); const [notice, setNotice] = useState('');
@@ -19,12 +17,9 @@ export default function StorageSettings() {
const draft = useMemo(() => items.find((item) => item.status === 'draft'), [items]); const draft = useMemo(() => items.find((item) => item.status === 'draft'), [items]);
const load = async () => { const load = async () => {
setLoading(true); try { onItemsChange(await api.listStorageConfigs()); }
try { setItems(await api.listStorageConfigs()); }
catch (reason) { setError(messageOf(reason)); } catch (reason) { setError(messageOf(reason)); }
finally { setLoading(false); }
}; };
useEffect(() => { void load(); }, []);
const save = async () => { const save = async () => {
setBusy('save'); setError(''); setNotice(''); setBusy('save'); setError(''); setNotice('');
@@ -55,8 +50,6 @@ export default function StorageSettings() {
finally { setBusy(null); } finally { setBusy(null); }
}; };
if (loading) return <div className="grid gap-3 py-8">{[1, 2].map((item) => <div key={item} className="h-28 animate-pulse rounded-2xl bg-black/5"/>)}</div>;
return <section className="py-8"> return <section className="py-8">
<div className="mb-7"><p className="font-mono text-[10px] uppercase tracking-[.28em] text-[#d15f37]">Infrastructure / Object Storage</p><h2 className="mt-2 font-display text-4xl tracking-[-.04em]"> COS</h2><p className="mt-2 max-w-2xl text-sm leading-6 text-black/45">稿</p></div> <div className="mb-7"><p className="font-mono text-[10px] uppercase tracking-[.28em] text-[#d15f37]">Infrastructure / Object Storage</p><h2 className="mt-2 font-display text-4xl tracking-[-.04em]"> COS</h2><p className="mt-2 max-w-2xl text-sm leading-6 text-black/45">稿</p></div>
{error && <div className="mb-5 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>} {error && <div className="mb-5 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>}

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ArrowRightLeft, ChevronDown, Copy, Fingerprint, History, KeyRound, Plus, RefreshCw, Search, ShieldCheck, Users, X } from 'lucide-react'; import { ArrowRightLeft, ChevronDown, Copy, Fingerprint, History, KeyRound, Plus, RefreshCw, Search, ShieldCheck, Users, X } from 'lucide-react';
import type { AuditLogEntry, ManagedApiKey, ManagedUser, OperationGroup, Project } from '@shared/types'; import type { AuditLogEntry, ManagedApiKey, ManagedUser, OperationGroup, Project, StorageConfig } from '@shared/types';
import { api, ApiError } from '@/api/client'; import { api, ApiError } from '@/api/client';
import { useAuthStore } from '@/store/useAuthStore'; import { useAuthStore } from '@/store/useAuthStore';
import StorageSettings from '@/components/StorageSettings'; import StorageSettings from '@/components/StorageSettings';
@@ -28,6 +28,7 @@ export default function ManagementPage() {
const [keys, setKeys] = useState<ManagedApiKey[]>([]); const [keys, setKeys] = useState<ManagedApiKey[]>([]);
const [logs, setLogs] = useState<AuditLogEntry[]>([]); const [logs, setLogs] = useState<AuditLogEntry[]>([]);
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [storageConfigs, setStorageConfigs] = useState<StorageConfig[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -53,11 +54,12 @@ export default function ManagementPage() {
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
const [nextAccounts, nextKeys, nextLogs, nextProjects, nextGroups] = await Promise.all([ const [nextAccounts, nextKeys, nextLogs, nextProjects, nextGroups, nextStorageConfigs] = await Promise.all([
api.listManagedUsers(), api.listApiKeys(), api.listAuditLogs(), api.listProjects(), api.listManagedUsers(), api.listApiKeys(), api.listAuditLogs(), api.listProjects(),
isPlatform ? api.listGroups() : Promise.resolve([]), isPlatform ? api.listGroups() : Promise.resolve([]),
isPlatform ? api.listStorageConfigs() : Promise.resolve([]),
]); ]);
setAccounts(nextAccounts); setKeys(nextKeys); setLogs(nextLogs); setProjects(nextProjects); setGroups(nextGroups); setAccounts(nextAccounts); setKeys(nextKeys); setLogs(nextLogs); setProjects(nextProjects); setGroups(nextGroups); setStorageConfigs(nextStorageConfigs);
} catch (reason) { setError(messageOf(reason)); } } catch (reason) { setError(messageOf(reason)); }
finally { setLoading(false); } finally { setLoading(false); }
}, [isPlatform, user]); }, [isPlatform, user]);
@@ -67,9 +69,9 @@ export default function ManagementPage() {
...(isPlatform ? [{ id: 'groups' as const, label: '运营组', count: groups.length }] : []), ...(isPlatform ? [{ id: 'groups' as const, label: '运营组', count: groups.length }] : []),
{ id: 'accounts' as const, label: '账号', count: accounts.length }, { id: 'accounts' as const, label: '账号', count: accounts.length },
{ id: 'keys' as const, label: 'API Key', count: keys.filter((item) => item.status === 'active').length }, { id: 'keys' as const, label: 'API Key', count: keys.filter((item) => item.status === 'active').length },
...(isPlatform ? [{ id: 'storage' as const, label: '对象存储', count: 0 }] : []), ...(isPlatform ? [{ id: 'storage' as const, label: '对象存储', count: storageConfigs.some((item) => item.status === 'active') ? '已连接' : '未配置' }] : []),
{ id: 'audit' as const, label: '审计日志', count: logs.length }, { id: 'audit' as const, label: '审计日志', count: logs.length },
], [accounts.length, groups.length, isPlatform, keys, logs.length]); ], [accounts.length, groups.length, isPlatform, keys, logs.length, storageConfigs]);
if (user?.role === 'operator') { if (user?.role === 'operator') {
return <main className="mx-auto max-w-3xl px-5 py-24 text-center"><ShieldCheck className="mx-auto text-black/25"/><h1 className="mt-5 font-display text-4xl"></h1><p className="mt-3 text-sm text-black/45"></p></main>; return <main className="mx-auto max-w-3xl px-5 py-24 text-center"><ShieldCheck className="mx-auto text-black/25"/><h1 className="mt-5 font-display text-4xl"></h1><p className="mt-3 text-sm text-black/45"></p></main>;
@@ -111,7 +113,7 @@ export default function ManagementPage() {
{tab === 'groups' && <Groups groups={groups} accounts={accounts} onCreate={() => setPanel('group')} onRename={(item)=>{setRenameGroup(item);setRenameGroupValue(item.name);setPanel('rename_group')}} onShowAccounts={(item) => { setAccountGroupFilter(String(item.id)); setTab('accounts'); }} onReplace={(item) => { setReplaceGroup(item); setReplacementUserId(''); setPreviousAdminAction('demote'); setPanel('replace_admin'); }} onToggle={(item) => { if(item.status==='active'&&!window.confirm(`停用“${item.name}”将影响 ${item.active_user_count} 个启用账号、${item.project_count} 个项目和 ${item.customer_link_count} 个客户访问链接。确认继续吗?`))return;void run(() => api.setGroupStatus(item.id, item.status === 'active' ? 'disabled' : 'active').then(() => undefined)); }}/>} {tab === 'groups' && <Groups groups={groups} accounts={accounts} onCreate={() => setPanel('group')} onRename={(item)=>{setRenameGroup(item);setRenameGroupValue(item.name);setPanel('rename_group')}} onShowAccounts={(item) => { setAccountGroupFilter(String(item.id)); setTab('accounts'); }} onReplace={(item) => { setReplaceGroup(item); setReplacementUserId(''); setPreviousAdminAction('demote'); setPanel('replace_admin'); }} onToggle={(item) => { if(item.status==='active'&&!window.confirm(`停用“${item.name}”将影响 ${item.active_user_count} 个启用账号、${item.project_count} 个项目和 ${item.customer_link_count} 个客户访问链接。确认继续吗?`))return;void run(() => api.setGroupStatus(item.id, item.status === 'active' ? 'disabled' : 'active').then(() => undefined)); }}/>}
{tab === 'accounts' && <Accounts accounts={accounts} groups={groups} groupFilter={accountGroupFilter} onGroupFilter={setAccountGroupFilter} currentId={user?.id} currentRole={user?.role} onCreate={() => setPanel('user')} onAudit={(item) => { setAuditUserId(item.id); setTab('audit'); }} onRename={(item) => { setRenameTarget(item); setRenameValue(item.display_name); setPanel('rename'); }} onToggle={(item) => void run(() => api.setUserStatus(item.id, item.status === 'active' ? 'disabled' : 'active').then(() => undefined))} onReset={(item) => { setResetTarget(item); setResetPassword(''); setPanel('reset'); }}/>} {tab === 'accounts' && <Accounts accounts={accounts} groups={groups} groupFilter={accountGroupFilter} onGroupFilter={setAccountGroupFilter} currentId={user?.id} currentRole={user?.role} onCreate={() => setPanel('user')} onAudit={(item) => { setAuditUserId(item.id); setTab('audit'); }} onRename={(item) => { setRenameTarget(item); setRenameValue(item.display_name); setPanel('rename'); }} onToggle={(item) => void run(() => api.setUserStatus(item.id, item.status === 'active' ? 'disabled' : 'active').then(() => undefined))} onReset={(item) => { setResetTarget(item); setResetPassword(''); setPanel('reset'); }}/>}
{tab === 'keys' && <ApiKeys items={keys} onCreate={() => { setRevealedKey(''); setCopyState('idle'); setPanel('key'); }} onRevoke={(item) => void run(() => api.revokeApiKey(item.id))}/>} {tab === 'keys' && <ApiKeys items={keys} onCreate={() => { setRevealedKey(''); setCopyState('idle'); setPanel('key'); }} onRevoke={(item) => void run(() => api.revokeApiKey(item.id))}/>}
{tab === 'storage' && isPlatform && <StorageSettings/>} {tab === 'storage' && isPlatform && <StorageSettings items={storageConfigs} onItemsChange={setStorageConfigs}/>}
{tab === 'audit' && <AuditLogs items={logs} filteredUser={auditUserId ? accounts.find((item)=>item.id===auditUserId) : undefined} onClear={() => setAuditUserId(null)}/>} {tab === 'audit' && <AuditLogs items={logs} filteredUser={auditUserId ? accounts.find((item)=>item.id===auditUserId) : undefined} onClear={() => setAuditUserId(null)}/>}
</>} </>}