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>
This commit is contained in:
1 parent
777d9e4e71
commit
76ec9387f0
202 files changed
+16555
-2543
No files matched your search
@@ -0,0 +1,276 @@
|
||||
// 云账号详情侧栏 — 概览 / 区域 / 账单 // 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>
|
||||
);
|
||||
}
|
||||
@@ -24,9 +24,15 @@ import {
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
Loader2,
|
||||
MapPin,
|
||||
Wallet,
|
||||
CloudDownload,
|
||||
} 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';
|
||||
import { PageLoading } from '@/components/layout/PageLoading';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
@@ -63,9 +69,71 @@ export function CloudAccountManager() {
|
||||
monthly_cost: '',
|
||||
status: 'active',
|
||||
});
|
||||
const [detailAccount, setDetailAccount] = useState<JsonData>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [importAccountId, setImportAccountId] = useState<number | string | undefined>();
|
||||
const [actionLoading, setActionLoading] = useState<Record<string, string>>({});
|
||||
|
||||
const providerLabels = getProviderLabels(t);
|
||||
|
||||
const setCardAction = (accountId: string | number, action: string | null) => {
|
||||
setActionLoading((prev) => {
|
||||
const next = { ...prev };
|
||||
const key = String(accountId);
|
||||
if (action) next[key] = action;
|
||||
else delete next[key];
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const openDetail = (account: JsonData) => {
|
||||
setDetailAccount(account);
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
const openImport = (account: JsonData) => {
|
||||
setImportAccountId(account.id);
|
||||
setImportOpen(true);
|
||||
};
|
||||
|
||||
const handleQuickSyncRegions = async (account: JsonData) => {
|
||||
setCardAction(account.id, 'regions');
|
||||
try {
|
||||
await cloudApi.syncRegions(account.id);
|
||||
toast.success(t('cloud.syncRegionsSuccess'));
|
||||
loadData();
|
||||
} catch (e) {
|
||||
toast.error(t('cloud.syncRegionsFailed'), { description: (e as JsonData).message });
|
||||
} finally {
|
||||
setCardAction(account.id, null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickSyncBilling = async (account: JsonData) => {
|
||||
setCardAction(account.id, 'billing');
|
||||
try {
|
||||
const res = await cloudApi.syncBilling(account.id);
|
||||
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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
loadData();
|
||||
} catch (e) {
|
||||
toast.error(t('cloud.syncBillingFailed'), { description: (e as JsonData).message });
|
||||
} finally {
|
||||
setCardAction(account.id, null);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setEditingAccount(null);
|
||||
setFormData({
|
||||
@@ -178,7 +246,7 @@ export function CloudAccountManager() {
|
||||
onChange={(e) => setSelectedProvider(e.target.value)}
|
||||
className="px-3 py-2 h-9 rounded-md text-sm dg-input"
|
||||
>
|
||||
<option value="all">{t('cloud.allPlatforms')}</option>
|
||||
<option value="all">{t('cloud.allProviders')}</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="aliyun">{t('cloud.aliyun')}</option>
|
||||
<option value="tencent">{t('cloud.tencent')}</option>
|
||||
@@ -244,12 +312,18 @@ export function CloudAccountManager() {
|
||||
<Card key={account.id} className="dg-card dg-card-hover">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-[var(--dg-bg-tertiary)] flex items-center justify-center" style={{ borderBottom: `2px solid ${getProviderColor(account.provider)}` }}>
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<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(account.provider)}` }}>
|
||||
<CloudBrandIcon provider={account.provider} className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-[var(--dg-text-primary)]">{account.name}</h3>
|
||||
<div className="min-w-0">
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-[var(--dg-text-primary)] hover:text-[var(--dg-blue)] text-left truncate block max-w-full"
|
||||
onClick={() => openDetail(account)}
|
||||
>
|
||||
{account.name}
|
||||
</button>
|
||||
<p className="text-sm dg-text-secondary mt-0.5">{providerLabels[account.provider] || account.provider}</p>
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
{accessKey && (
|
||||
@@ -314,6 +388,40 @@ export function CloudAccountManager() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mt-3 pt-3 border-t border-[var(--dg-border)]">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs dg-btn-secondary"
|
||||
disabled={actionLoading[String(account.id)] === 'regions'}
|
||||
onClick={() => handleQuickSyncRegions(account)}
|
||||
>
|
||||
{actionLoading[String(account.id)] === 'regions'
|
||||
? <Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
: <MapPin className="h-3 w-3 mr-1" />}
|
||||
{t('cloud.syncRegions')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs dg-btn-secondary"
|
||||
disabled={actionLoading[String(account.id)] === 'billing'}
|
||||
onClick={() => handleQuickSyncBilling(account)}
|
||||
>
|
||||
{actionLoading[String(account.id)] === 'billing'
|
||||
? <Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
: <Wallet className="h-3 w-3 mr-1" />}
|
||||
{t('cloud.syncBilling')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs dg-btn-primary"
|
||||
onClick={() => openImport(account)}
|
||||
>
|
||||
<CloudDownload className="h-3 w-3 mr-1" />
|
||||
{t('cloud.importHosts')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -349,7 +457,7 @@ export function CloudAccountManager() {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label>
|
||||
{t('cloud.platformLabel')} <span className="text-[var(--dg-red)]">*</span>
|
||||
{t('cloud.providerLabel')} <span className="text-[var(--dg-red)]">*</span>
|
||||
</Label>
|
||||
<select
|
||||
value={formData.provider}
|
||||
@@ -434,6 +542,31 @@ export function CloudAccountManager() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<CloudAccountDetailSheet
|
||||
account={detailAccount}
|
||||
open={detailOpen}
|
||||
onOpenChange={setDetailOpen}
|
||||
providerLabels={providerLabels}
|
||||
onSyncRegions={loadData}
|
||||
onSyncBilling={loadData}
|
||||
onImportHosts={() => {
|
||||
if (detailAccount) {
|
||||
setDetailOpen(false);
|
||||
openImport(detailAccount);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<CloudServerImport
|
||||
open={importOpen}
|
||||
initialAccountId={importAccountId}
|
||||
onClose={() => {
|
||||
setImportOpen(false);
|
||||
setImportAccountId(undefined);
|
||||
loadData();
|
||||
}}
|
||||
/>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
RefreshCw,
|
||||
Wallet,
|
||||
Bell,
|
||||
CloudDownload,
|
||||
Loader2,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
@@ -62,6 +65,7 @@ export function CloudBilling() {
|
||||
const [editingRecord, setEditingRecord] = useState<JsonData>(null);
|
||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||
const [editingRule, setEditingRule] = useState<JsonData>(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
const loadStats = useCallback(async () => {
|
||||
setStatsLoading(true);
|
||||
@@ -121,6 +125,37 @@ export function CloudBilling() {
|
||||
if (activeTab === 'alerts') loadRules();
|
||||
};
|
||||
|
||||
// 从云账号 AK 同步昨日账单(当前支持阿里云) // Logan.Li
|
||||
const handleSyncYesterday = async () => {
|
||||
setSyncing(true);
|
||||
try {
|
||||
const res = await billingApi.sync();
|
||||
const errs = res?.errors || [];
|
||||
if (errs.length > 0) {
|
||||
toast.warning(t('cloud.syncBillingPartial'), {
|
||||
description: `${t('cloud.syncBillingResult', {
|
||||
date: res.billing_date,
|
||||
created: res.created,
|
||||
updated: res.updated,
|
||||
})}\n${errs.join('\n')}`,
|
||||
});
|
||||
} else {
|
||||
toast.success(t('cloud.syncBillingSuccess'), {
|
||||
description: t('cloud.syncBillingResult', {
|
||||
date: res.billing_date,
|
||||
created: res.created,
|
||||
updated: res.updated,
|
||||
}),
|
||||
});
|
||||
}
|
||||
refreshAll();
|
||||
} catch (e) {
|
||||
toast.error(t('cloud.syncBillingFailed'), { description: (e as JsonData).message });
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openAddRecord = () => {
|
||||
setEditingRecord(null);
|
||||
setRecordDialogOpen(true);
|
||||
@@ -234,6 +269,17 @@ export function CloudBilling() {
|
||||
}
|
||||
actions={(
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="dg-btn-secondary"
|
||||
onClick={handleSyncYesterday}
|
||||
disabled={syncing}
|
||||
title={t('cloud.syncBillingHint')}
|
||||
>
|
||||
{syncing ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <CloudDownload className="h-4 w-4 mr-1" />}
|
||||
{t('cloud.syncYesterday')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={refreshAll}>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />{t('common.refresh')}
|
||||
</Button>
|
||||
@@ -256,6 +302,16 @@ export function CloudBilling() {
|
||||
<PageLoading />
|
||||
) : stats ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/40 p-3">
|
||||
<div className="flex items-start gap-2 text-sm text-[var(--dg-text-secondary)]">
|
||||
<Info className="h-4 w-4 dg-text-blue shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<p>{t('cloud.syncBillingInfo')}</p>
|
||||
<p className="text-xs dg-text-muted">{t('cloud.syncBillingTiming')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stats.alerts?.length > 0 && (
|
||||
<div className="dg-alert-banner">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
|
||||
Reference in new issue
Block a user