Files
aiops/frontend/src/components/cloud/CloudAccountDetailSheet.tsx
T
LoganandClaude 76ec9387f0 feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更:
- RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、
  backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API
- Windows SMB 文件传输:backend/pkg/winsmb
- 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情
- Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs
- 服务器导入助手:server_import_helpers、ImportCredentialSettings
- 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块)
- 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-*
- .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-04 01:50:36 +08:00

276 lines
11 KiB
TypeScript

// 云账号详情侧栏 — 概览 / 区域 / 账单 // Logan.Li
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
CheckCircle2,
CloudDownload,
Loader2,
MapPin,
RefreshCw,
Wallet,
XCircle,
} from 'lucide-react';
import { toast } from 'sonner';
import { cloudApi, billingApi } from '@/api';
import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon';
import type { JsonData } from '@/types/common';
interface CloudAccountDetailSheetProps {
account: JsonData | null;
open: boolean;
onOpenChange: (open: boolean) => void;
providerLabels: Record<string, string>;
onSyncRegions?: () => void;
onSyncBilling?: () => void;
onImportHosts?: () => void;
}
export function CloudAccountDetailSheet({
account,
open,
onOpenChange,
providerLabels,
onSyncRegions,
onSyncBilling,
onImportHosts,
}: CloudAccountDetailSheetProps) {
const { t } = useTranslation();
const [tab, setTab] = useState('overview');
const [regions, setRegions] = useState<JsonData[]>([]);
const [regionsLoading, setRegionsLoading] = useState(false);
const [syncingRegions, setSyncingRegions] = useState(false);
const [billings, setBillings] = useState<JsonData[]>([]);
const [billingsLoading, setBillingsLoading] = useState(false);
const [syncingBilling, setSyncingBilling] = useState(false);
const accountId = account?.id as number | string | undefined;
const loadRegions = useCallback(async () => {
if (!accountId) return;
setRegionsLoading(true);
try {
const res = await cloudApi.getAccountRegions(accountId);
setRegions(res?.list || []);
} catch {
setRegions([]);
} finally {
setRegionsLoading(false);
}
}, [accountId]);
const loadBillings = useCallback(async () => {
if (!accountId) return;
setBillingsLoading(true);
try {
const monthStart = new Date().toISOString().slice(0, 7) + '-01';
const res = await billingApi.list({
cloud_account_id: Number(accountId),
start_date: monthStart,
page_size: 20,
});
setBillings(res?.list || []);
} catch {
setBillings([]);
} finally {
setBillingsLoading(false);
}
}, [accountId]);
useEffect(() => {
if (!open || !accountId) return;
setTab('overview');
loadRegions();
loadBillings();
}, [open, accountId, loadRegions, loadBillings]);
const handleSyncRegions = async () => {
if (!accountId) return;
setSyncingRegions(true);
try {
await cloudApi.syncRegions(accountId);
await loadRegions();
onSyncRegions?.();
toast.success(t('cloud.syncRegionsSuccess'));
} catch (e) {
toast.error(t('cloud.syncRegionsFailed'), { description: (e as JsonData).message });
} finally {
setSyncingRegions(false);
}
};
const handleSyncBilling = async () => {
if (!accountId) return;
setSyncingBilling(true);
try {
const res = await cloudApi.syncBilling(accountId);
await loadBillings();
onSyncBilling?.();
const errs = res?.errors || [];
if (errs.length > 0) {
toast.warning(t('cloud.syncBillingPartial'), { description: errs.join('\n') });
} else {
toast.success(t('cloud.syncBillingSuccess'), {
description: t('cloud.syncBillingResult', {
date: res.billing_date,
created: res.created,
updated: res.updated,
}),
});
}
} catch (e) {
toast.error(t('cloud.syncBillingFailed'), { description: (e as JsonData).message });
} finally {
setSyncingBilling(false);
}
};
if (!account) return null;
const provider = String(account.provider || '');
const serverCount = account.server_count ?? account.serverCount ?? 0;
const regionCount = account.region_count ?? account.regionCount ?? regions.length;
const monthlyCost = account.monthly_cost ?? account.monthlyCost ?? 0;
const lastSync = account.last_sync_at || account.lastSyncAt || account.last_sync;
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-full sm:max-w-xl overflow-y-auto">
<SheetHeader className="border-b border-[var(--dg-border)] pb-4">
<div className="flex items-start gap-3 pr-8">
<div
className="w-10 h-10 rounded-lg bg-[var(--dg-bg-tertiary)] flex items-center justify-center shrink-0"
style={{ borderBottom: `2px solid ${getProviderColor(provider)}` }}
>
<CloudBrandIcon provider={provider} className="h-6 w-6" />
</div>
<div className="min-w-0">
<SheetTitle className="truncate">{account.name}</SheetTitle>
<SheetDescription>
{providerLabels[provider] || provider}
{account.status === 'active' ? (
<Badge className="dg-status-running border-0 ml-2">
<CheckCircle2 className="h-3 w-3 mr-1" />
{t('cloud.normal')}
</Badge>
) : account.status === 'inactive' ? (
<Badge className="dg-status-stopped border-0 ml-2">
<XCircle className="h-3 w-3 mr-1" />
{t('cloud.stopped')}
</Badge>
) : null}
</SheetDescription>
</div>
</div>
</SheetHeader>
<Tabs value={tab} onValueChange={setTab} className="px-1 pt-2">
<TabsList className="w-full grid grid-cols-3">
<TabsTrigger value="overview">{t('cloud.tabOverview')}</TabsTrigger>
<TabsTrigger value="regions">{t('cloud.tabRegions')}</TabsTrigger>
<TabsTrigger value="billing">{t('cloud.tabBilling')}</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4 mt-4">
<div className="grid grid-cols-3 gap-3">
<div className="rounded-lg p-3 bg-[var(--dg-bg-tertiary)]">
<p className="text-xs dg-text-secondary">{t('cloud.monthlyFee')}</p>
<p className="text-lg font-medium text-[var(--dg-green)]">¥{Number(monthlyCost).toLocaleString()}</p>
</div>
<div className="rounded-lg p-3 bg-[var(--dg-bg-tertiary)]">
<p className="text-xs dg-text-secondary">{t('cloud.serverCount')}</p>
<p className="text-lg font-medium">{serverCount}</p>
</div>
<div className="rounded-lg p-3 bg-[var(--dg-bg-tertiary)]">
<p className="text-xs dg-text-secondary">{t('cloud.regionCount')}</p>
<p className="text-lg font-medium">{regionCount}</p>
</div>
</div>
{lastSync && (
<p className="text-xs dg-text-muted">
{t('cloud.syncTime')}: {new Date(lastSync).toLocaleString()}
</p>
)}
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" className="dg-btn-secondary" disabled={syncingRegions} onClick={handleSyncRegions}>
{syncingRegions ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <MapPin className="h-4 w-4 mr-1" />}
{t('cloud.syncRegions')}
</Button>
<Button size="sm" variant="outline" className="dg-btn-secondary" disabled={syncingBilling} onClick={handleSyncBilling}>
{syncingBilling ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Wallet className="h-4 w-4 mr-1" />}
{t('cloud.syncBilling')}
</Button>
{onImportHosts && (
<Button size="sm" className="dg-btn-primary" onClick={onImportHosts}>
<CloudDownload className="h-4 w-4 mr-1" />
{t('cloud.importHosts')}
</Button>
)}
</div>
</TabsContent>
<TabsContent value="regions" className="mt-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm dg-text-secondary">{t('cloud.regionListHint')}</span>
<Button size="sm" variant="outline" className="dg-btn-secondary h-8" disabled={syncingRegions} onClick={handleSyncRegions}>
{syncingRegions ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
</Button>
</div>
{regionsLoading ? (
<div className="flex justify-center py-8"><Loader2 className="h-6 w-6 animate-spin dg-text-muted" /></div>
) : regions.length === 0 ? (
<p className="text-sm dg-text-muted text-center py-8">{t('cloud.noRegions')}</p>
) : (
<div className="space-y-2 max-h-[50vh] overflow-y-auto">
{regions.map((r) => (
<div key={r.id || r.code} className="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--dg-bg-tertiary)] text-sm">
<span className="font-medium">{r.name || r.code}</span>
<span className="dg-text-muted text-xs">{r.code}</span>
</div>
))}
</div>
)}
</TabsContent>
<TabsContent value="billing" className="mt-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm dg-text-secondary">{t('cloud.billingMonthHint')}</span>
<Button size="sm" variant="outline" className="dg-btn-secondary h-8" disabled={syncingBilling} onClick={handleSyncBilling}>
{syncingBilling ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
</Button>
</div>
{billingsLoading ? (
<div className="flex justify-center py-8"><Loader2 className="h-6 w-6 animate-spin dg-text-muted" /></div>
) : billings.length === 0 ? (
<p className="text-sm dg-text-muted text-center py-8">{t('cloud.noBillingRecords')}</p>
) : (
<div className="space-y-2 max-h-[50vh] overflow-y-auto">
{billings.map((b) => (
<div key={b.id} className="flex items-center justify-between px-3 py-2 rounded-md bg-[var(--dg-bg-tertiary)] text-sm">
<div>
<p className="font-medium">{b.service || '-'}</p>
<p className="text-xs dg-text-muted">{b.billing_date} · {b.region || '-'}</p>
</div>
<span className="text-[var(--dg-green)] font-medium">
¥{Number(b.cost || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
))}
</div>
)}
</TabsContent>
</Tabs>
</SheetContent>
</Sheet>
);
}