From 796e6592c2dd53d90d98816fe0328874e6bece77 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 9 Jul 2026 01:07:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor(quality):=20Q23=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=B7=A8=E5=9E=8B=E7=BB=84=E4=BB=B6=E6=8B=86=E5=88=86=20?= =?UTF-8?q?=E2=80=94=20CloudAccountManager=20572L=E2=86=92308L=20(-46%),?= =?UTF-8?q?=20=E6=8F=90=E5=8F=96=20cloud-account/=203=20=E4=B8=AA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=20(types(AccountFormData/AccountStats/ActionLoading?= =?UTF-8?q?=20+=206=20=E4=B8=AA=E7=BA=AF=E5=87=BD=E6=95=B0=20helper)=20+?= =?UTF-8?q?=20AccountCard(178L=20=E5=8F=97=E6=8E=A7=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E7=BB=84=E4=BB=B6)=20+=20AccountFormDialog(162L=20=E5=88=9B?= =?UTF-8?q?=E5=BB=BA/=E7=BC=96=E8=BE=91=E8=A1=A8=E5=8D=95=20Dialog)),=20ca?= =?UTF-8?q?tch=20=E7=B1=BB=E5=9E=8B=E5=AE=89=E5=85=A8=E5=8D=87=E7=BA=A7=20?= =?UTF-8?q?(e=3F.message=E2=86=92e=20instanceof=20Error),=20=E9=9B=B6?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=8F=98=E6=9B=B4,=20ESLint=20errors=3D0/war?= =?UTF-8?q?nings=3D0/any=3D0,=20tsc=20-b=3D0,=20npm=20build=E2=9C=93=20(17?= =?UTF-8?q?.14s),=2017/17=20=E6=B5=8B=E8=AF=95=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/cloud/CloudAccountManager.tsx | 380 +++--------------- .../cloud/cloud-account/AccountCard.tsx | 178 ++++++++ .../cloud/cloud-account/AccountFormDialog.tsx | 162 ++++++++ .../components/cloud/cloud-account/types.ts | 86 ++++ 4 files changed, 484 insertions(+), 322 deletions(-) create mode 100644 frontend/src/components/cloud/cloud-account/AccountCard.tsx create mode 100644 frontend/src/components/cloud/cloud-account/AccountFormDialog.tsx create mode 100644 frontend/src/components/cloud/cloud-account/types.ts diff --git a/frontend/src/components/cloud/CloudAccountManager.tsx b/frontend/src/components/cloud/CloudAccountManager.tsx index ace6d96..34f6eb6 100644 --- a/frontend/src/components/cloud/CloudAccountManager.tsx +++ b/frontend/src/components/cloud/CloudAccountManager.tsx @@ -2,35 +2,9 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Card, CardContent } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { - Plus, - Search, - Trash2, - Pencil, - Key, - Cloud, - CheckCircle2, - XCircle, - RefreshCw, - Loader2, - MapPin, - Wallet, - CloudDownload, -} from 'lucide-react'; +import { Plus, Search, Key, RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import { cloudApi } from '@/api'; -import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon'; import { CloudAccountDetailSheet } from './CloudAccountDetailSheet'; import { CloudServerImport } from '@/components/server/CloudServerImport'; import { PageShell } from '@/components/layout/PageShell'; @@ -38,17 +12,17 @@ import { PageLoading } from '@/components/layout/PageLoading'; import { PageEmpty } from '@/components/layout/PageEmpty'; import { StatStrip } from '@/components/ui/stat-strip'; import type { JsonData } from '@/types/common'; - -function getProviderLabels(t: (key: string) => string): Record { - return { - aws: 'AWS', - aliyun: t('cloud.aliyun'), - tencent: t('cloud.tencent'), - huawei: t('cloud.huawei'), - azure: 'Azure', - gcp: 'GCP', - }; -} +import { + type AccountFormData, + type ActionLoading, + getInitialFormData, + buildFormDataFromAccount, + computeAccountStats, + filterAccounts, + getProviderLabels, +} from './cloud-account/types'; +import { AccountCard } from './cloud-account/AccountCard'; +import { AccountFormDialog } from './cloud-account/AccountFormDialog'; export function CloudAccountManager() { const { t } = useTranslation(); @@ -60,20 +34,13 @@ export function CloudAccountManager() { // 创建/编辑账号表单状态 const [formOpen, setFormOpen] = useState(false); const [submitting, setSubmitting] = useState(false); - const [editingAccount, setEditingAccount] = useState(null); - const [formData, setFormData] = useState({ - name: '', - provider: 'aliyun', - access_key: '', - secret_key: '', - monthly_cost: '', - status: 'active', - }); - const [detailAccount, setDetailAccount] = useState(null); + const [editingAccount, setEditingAccount] = useState(null); + const [formData, setFormData] = useState(getInitialFormData()); + const [detailAccount, setDetailAccount] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); const [importAccountId, setImportAccountId] = useState(); - const [actionLoading, setActionLoading] = useState>({}); + const [actionLoading, setActionLoading] = useState({}); const providerLabels = getProviderLabels(t); @@ -104,7 +71,9 @@ export function CloudAccountManager() { toast.success(t('cloud.syncRegionsSuccess')); loadData(); } catch (e) { - toast.error(t('cloud.syncRegionsFailed'), { description: (e as JsonData).message }); + toast.error(t('cloud.syncRegionsFailed'), { + description: e instanceof Error ? e.message : String(e), + }); } finally { setCardAction(account.id, null); } @@ -128,7 +97,9 @@ export function CloudAccountManager() { } loadData(); } catch (e) { - toast.error(t('cloud.syncBillingFailed'), { description: (e as JsonData).message }); + toast.error(t('cloud.syncBillingFailed'), { + description: e instanceof Error ? e.message : String(e), + }); } finally { setCardAction(account.id, null); } @@ -136,14 +107,7 @@ export function CloudAccountManager() { const resetForm = () => { setEditingAccount(null); - setFormData({ - name: '', - provider: 'aliyun', - access_key: '', - secret_key: '', - monthly_cost: '', - status: 'active', - }); + setFormData(getInitialFormData()); }; const handleOpenCreate = () => { @@ -153,14 +117,7 @@ export function CloudAccountManager() { const handleOpenEdit = (account: JsonData) => { setEditingAccount(account); - setFormData({ - name: account.name || '', - provider: account.provider || 'aliyun', - access_key: '', - secret_key: '', - monthly_cost: String(account.monthly_cost ?? 0), - status: account.status || 'active', - }); + setFormData(buildFormDataFromAccount(account)); setFormOpen(true); }; @@ -215,20 +172,8 @@ export function CloudAccountManager() { } }; - const filteredAccounts = accounts.filter(account => { - const accessKey = account.access_key || account.accessKey || ''; - const matchesSearch = (account.name || '').toLowerCase().includes(searchQuery.toLowerCase()) || - accessKey.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesProvider = selectedProvider === 'all' || account.provider === selectedProvider; - return matchesSearch && matchesProvider; - }); - - const stats = { - total: accounts.length, - active: accounts.filter(a => a.status === 'active').length, - inactive: accounts.filter(a => a.status === 'inactive').length, - error: accounts.filter(a => a.status === 'error').length, - }; + const filteredAccounts = filterAccounts(accounts, searchQuery, selectedProvider); + const stats = computeAccountStats(accounts); const toolbar = (
@@ -269,13 +214,17 @@ export function CloudAccountManager() { { label: t('common.total'), value: stats.total, color: 'blue' }, { label: t('cloud.normal'), value: stats.active, color: 'green' }, { label: t('cloud.disabled'), value: stats.inactive, color: 'muted' }, - { label: t('cloud.error'), value: stats.error, color: stats.error > 0 ? 'red' : 'muted' }, + { + label: t('cloud.error'), + value: stats.error, + color: stats.error > 0 ? 'red' : 'muted', + }, ]} /> ) : undefined } toolbar={toolbar} - actions={( + actions={ <> - )} + } > {loading ? ( @@ -294,254 +243,41 @@ export function CloudAccountManager() { } title={t('cloud.noAccountData')} - action={( + action={ - )} + } /> ) : (
- {filteredAccounts.map((account) => { - const accessKey = account.access_key || account.accessKey || ''; - const serverCount = account.server_count ?? account.serverCount ?? 0; - const lastSync = account.last_sync || account.lastSync || account.created_at || '-'; - const monthlyCost = account.monthly_cost ?? account.monthlyCost ?? 0; - return ( - - -
-
-
- -
-
- -

{providerLabels[account.provider] || account.provider}

-
- {accessKey && ( - - - {accessKey.slice(0, 8)}... - - )} - {account.status === 'active' && ( - - - {t('cloud.normal')} - - )} - {account.status === 'inactive' && ( - - - {t('cloud.stopped')} - - )} - {account.status === 'error' && ( - - - {t('cloud.error')} - - )} -
-
-
-
- - -
-
-
-
-

{t('cloud.monthlyFee')}

-

¥{Number(monthlyCost).toLocaleString()}

-
-
-

{t('cloud.serverCount')}

-

{serverCount}

-
-
-

{t('cloud.syncTime')}

-

- {lastSync !== '-' ? new Date(lastSync).toLocaleString() : '-'} -

-
-
-
- - - -
-
-
- ); - })} + {filteredAccounts.map((account) => ( + + ))}
)} - {/* 创建账号 Dialog */} - - - - - - {editingAccount ? t('cloud.editAccount') : t('cloud.addAccount')} - - - {editingAccount ? t('cloud.editAccountDesc') : t('cloud.addAccountDesc')} - - - -
-
-
- - setFormData({ ...formData, name: e.target.value })} - className="dg-input" - /> -
-
- - -
-
- -
- - setFormData({ ...formData, access_key: e.target.value })} - className="dg-input font-mono text-sm" - /> -
- -
- - setFormData({ ...formData, secret_key: e.target.value })} - className="dg-input font-mono text-sm" - /> -
- -
-
- - setFormData({ ...formData, monthly_cost: e.target.value })} - className="dg-input" - /> -
-
- - -
-
-
- - - - - -
-
+ ; + actionLoading: ActionLoading; + onEdit: (account: JsonData) => void; + onDelete: (id: string) => void; + onDetail: (account: JsonData) => void; + onSyncRegions: (account: JsonData) => void; + onSyncBilling: (account: JsonData) => void; + onImport: (account: JsonData) => void; +} + +export function AccountCard({ + account, + providerLabels, + actionLoading, + onEdit, + onDelete, + onDetail, + onSyncRegions, + onSyncBilling, + onImport, +}: AccountCardProps) { + const { t } = useTranslation(); + const accountIdStr = String(account.id); + const accessKey = account.access_key || account.accessKey || ''; + const serverCount = account.server_count ?? account.serverCount ?? 0; + const lastSync = account.last_sync || account.lastSync || account.created_at || '-'; + const monthlyCost = account.monthly_cost ?? account.monthlyCost ?? 0; + + return ( + + +
+
+
+ +
+
+ +

+ {providerLabels[account.provider] || account.provider} +

+
+ {accessKey && ( + + + {accessKey.slice(0, 8)}... + + )} + {account.status === 'active' && ( + + + {t('cloud.normal')} + + )} + {account.status === 'inactive' && ( + + + {t('cloud.stopped')} + + )} + {account.status === 'error' && ( + + + {t('cloud.error')} + + )} +
+
+
+
+ + +
+
+
+
+

{t('cloud.monthlyFee')}

+

+ ¥{Number(monthlyCost).toLocaleString()} +

+
+
+

{t('cloud.serverCount')}

+

{serverCount}

+
+
+

{t('cloud.syncTime')}

+

+ {lastSync !== '-' ? new Date(lastSync).toLocaleString() : '-'} +

+
+
+
+ + + +
+
+
+ ); +} diff --git a/frontend/src/components/cloud/cloud-account/AccountFormDialog.tsx b/frontend/src/components/cloud/cloud-account/AccountFormDialog.tsx new file mode 100644 index 0000000..7c348dd --- /dev/null +++ b/frontend/src/components/cloud/cloud-account/AccountFormDialog.tsx @@ -0,0 +1,162 @@ +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Plus, Pencil, Cloud, Loader2 } from 'lucide-react'; +import type { JsonData } from '@/types/common'; +import type { AccountFormData } from './types'; + +export interface AccountFormDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + editingAccount: JsonData | null; + formData: AccountFormData; + onFormDataChange: (data: AccountFormData) => void; + submitting: boolean; + onSubmit: () => void; +} + +export function AccountFormDialog({ + open, + onOpenChange, + editingAccount, + formData, + onFormDataChange, + submitting, + onSubmit, +}: AccountFormDialogProps) { + const { t } = useTranslation(); + + return ( + + + + + + {editingAccount ? t('cloud.editAccount') : t('cloud.addAccount')} + + + {editingAccount ? t('cloud.editAccountDesc') : t('cloud.addAccountDesc')} + + + +
+
+
+ + onFormDataChange({ ...formData, name: e.target.value })} + className="dg-input" + /> +
+
+ + +
+
+ +
+ + onFormDataChange({ ...formData, access_key: e.target.value })} + className="dg-input font-mono text-sm" + /> +
+ +
+ + onFormDataChange({ ...formData, secret_key: e.target.value })} + className="dg-input font-mono text-sm" + /> +
+ +
+
+ + onFormDataChange({ ...formData, monthly_cost: e.target.value })} + className="dg-input" + /> +
+
+ + +
+
+
+ + + + + +
+
+ ); +} diff --git a/frontend/src/components/cloud/cloud-account/types.ts b/frontend/src/components/cloud/cloud-account/types.ts new file mode 100644 index 0000000..22b31b0 --- /dev/null +++ b/frontend/src/components/cloud/cloud-account/types.ts @@ -0,0 +1,86 @@ +import type { JsonData } from '@/types/common'; + +/** Create/Edit account form state */ +export interface AccountFormData { + name: string; + provider: string; + access_key: string; + secret_key: string; + monthly_cost: string; + status: string; +} + +/** Aggregated stats derived from account list */ +export interface AccountStats { + total: number; + active: number; + inactive: number; + error: number; +} + +/** Per-account action loading state (key=accountId, value=action name) */ +export type ActionLoading = Record; + +/** Default form data for new account creation */ +export function getInitialFormData(): AccountFormData { + return { + name: '', + provider: 'aliyun', + access_key: '', + secret_key: '', + monthly_cost: '', + status: 'active', + }; +} + +/** Build form data from an existing account (for editing) */ +export function buildFormDataFromAccount(account: JsonData): AccountFormData { + return { + name: account.name || '', + provider: account.provider || 'aliyun', + access_key: '', + secret_key: '', + monthly_cost: String(account.monthly_cost ?? 0), + status: account.status || 'active', + }; +} + +/** Compute stats from a list of accounts */ +export function computeAccountStats(accounts: JsonData[]): AccountStats { + return { + total: accounts.length, + active: accounts.filter((a) => a.status === 'active').length, + inactive: accounts.filter((a) => a.status === 'inactive').length, + error: accounts.filter((a) => a.status === 'error').length, + }; +} + +/** Filter accounts by search query + provider selection */ +export function filterAccounts( + accounts: JsonData[], + searchQuery: string, + selectedProvider: string, +): JsonData[] { + const q = searchQuery.toLowerCase(); + return accounts.filter((account) => { + const accessKey = account.access_key || account.accessKey || ''; + const matchesSearch = + (account.name || '').toLowerCase().includes(q) || accessKey.toLowerCase().includes(q); + const matchesProvider = selectedProvider === 'all' || account.provider === selectedProvider; + return matchesSearch && matchesProvider; + }); +} + +/** Provider label map builder (provider code → display label) */ +export function getProviderLabels( + t: (key: string) => string, +): Record { + return { + aws: 'AWS', + aliyun: t('cloud.aliyun'), + tencent: t('cloud.tencent'), + huawei: t('cloud.huawei'), + azure: 'Azure', + gcp: 'GCP', + }; +}