主要变更: - 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>
276 lines
11 KiB
TypeScript
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>
|
|
);
|
|
} |