feat: 补全多云资产与账单同步

- 接入 AWS、华为云和 Azure 资产及账单 API

- 按原始币种隔离统计并修正账号服务器数

- 修复侧栏菜单展开后的滚动行为
This commit is contained in:
Logan.Li committed 2026-07-29 06:15:00 +08:00
1 parent 65e2f09b97
commit 132fe9ba97
25 files changed
+1094 -141

No files matched your search

@@ -71,7 +71,7 @@ export function AIPredictiveAnalytics() {
dashboardStatsApi.getMetrics(72),
serverApi.getServerStats(),
billingApi.getStats().catch(() => null),
billingApi.list({ start_date: startDate, page_size: 500 }).catch(() => ({ list: [] })),
billingApi.list({ start_date: startDate, currency: 'CNY', page_size: 500 }).catch(() => ({ list: [] })),
billingApi.listAlertRules().catch(() => ({ list: [] })),
billingApi.getCostAnalysis().catch(() => null),
]);
@@ -25,6 +25,7 @@ import { cloudApi, billingApi } from '@/api';
import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon';
import type { JsonData } from '@/types/common';
import { formatCurrencyByCode } from './CloudBillingHelpers';
interface CloudAccountDetailSheetProps {
account: JsonData | null;
@@ -141,6 +142,7 @@ export function CloudAccountDetailSheet({
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 billingCurrency = account.billing_currency || account.billingCurrency || 'CNY';
const lastSync = account.last_sync_at || account.lastSyncAt || account.last_sync;
return (
@@ -185,7 +187,7 @@ export function CloudAccountDetailSheet({
<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>
<p className="text-lg font-medium text-[var(--dg-green)]">{formatCurrencyByCode(Number(monthlyCost), billingCurrency)}</p>
</div>
<div className="rounded-lg p-3 bg-[var(--dg-bg-tertiary)]">
<p className="text-xs dg-text-secondary">{t('cloud.serverCount')}</p>
@@ -262,7 +264,7 @@ export function CloudAccountDetailSheet({
<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 })}
{formatCurrencyByCode(Number(b.cost || 0), b.currency)}
</span>
</div>
))}
@@ -273,4 +275,4 @@ export function CloudAccountDetailSheet({
</SheetContent>
</Sheet>
);
}
}
@@ -39,7 +39,22 @@ export function ProviderDot({ provider, className = 'w-2 h-2' }: { provider: str
}
export function formatCurrency(n: number) {
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
return formatCurrencyByCode(n, 'CNY');
}
// 按账单原始币种格式化,避免 USD 等费用被错误显示成人民币。 // Logan.Li
export function formatCurrencyByCode(n: number, currency = 'CNY') {
try {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: currency || 'CNY',
currencyDisplay: 'narrowSymbol',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(Number(n) || 0);
} catch {
return `${currency || 'CNY'} ${(Number(n) || 0).toFixed(2)}`;
}
}
export function StatCard({ icon, label, value, trend, trendLabel, alert }: {
@@ -106,4 +121,4 @@ export function TrendChart({ data }: { data: { date: string; cost: number }[] })
</div>
</div>
);
}
}
@@ -16,6 +16,7 @@ import {
import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon';
import type { JsonData } from '@/types/common';
import type { ActionLoading } from './types';
import { formatCurrencyByCode } from '../CloudBillingHelpers';
export interface AccountCardProps {
account: JsonData;
@@ -46,6 +47,7 @@ export function AccountCard({
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;
const billingCurrency = account.billing_currency || account.billingCurrency || 'CNY';
return (
<Card key={account.id} className="dg-card dg-card-hover">
@@ -120,7 +122,7 @@ export function AccountCard({
<div>
<p className="text-xs dg-text-secondary">{t('cloud.monthlyFee')}</p>
<p className="text-lg font-medium text-[var(--dg-green)]">
¥{Number(monthlyCost).toLocaleString()}
{formatCurrencyByCode(Number(monthlyCost), billingCurrency)}
</p>
</div>
<div>
@@ -34,6 +34,14 @@ export function AccountFormDialog({
onSubmit,
}: AccountFormDialogProps) {
const { t } = useTranslation();
const accessKeyPlaceholder = formData.provider === 'azure'
? t('cloud.azureAccessKeyPlaceholder')
: formData.provider === 'aws'
? t('cloud.awsAccessKeyPlaceholder')
: t('cloud.defaultAccessKeyPlaceholder');
const secretKeyPlaceholder = formData.provider === 'azure'
? t('cloud.azureSecretKeyPlaceholder')
: t('cloud.defaultSecretKeyPlaceholder');
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -87,7 +95,7 @@ export function AccountFormDialog({
)}
</Label>
<Input
placeholder="LTAI..."
placeholder={accessKeyPlaceholder}
value={formData.access_key}
onChange={(e) => onFormDataChange({ ...formData, access_key: e.target.value })}
className="dg-input font-mono text-sm"
@@ -103,7 +111,7 @@ export function AccountFormDialog({
</Label>
<Input
type="password"
placeholder="••••••••"
placeholder={secretKeyPlaceholder}
value={formData.secret_key}
onChange={(e) => onFormDataChange({ ...formData, secret_key: e.target.value })}
className="dg-input font-mono text-sm"
@@ -13,7 +13,7 @@ import { Plus, Trash2, Pencil, RefreshCw, Wallet } from 'lucide-react';
import { ContentPanel } from '@/components/layout/ContentPanel';
import { PageLoading } from '@/components/layout/PageLoading';
import { PageEmpty } from '@/components/layout/PageEmpty';
import { formatCurrency, providerLabels, ProviderDot } from '../CloudBillingHelpers';
import { formatCurrencyByCode, providerLabels, ProviderDot } from '../CloudBillingHelpers';
import type { JsonData } from '@/types/common';
/** 成本记录筛选工具栏 — 云厂商下拉 + 日期范围 + 查询按钮 // Logan.Li */
@@ -123,7 +123,7 @@ export function RecordsTab({
<td className="px-4 py-2.5 dg-text-secondary">{r.service || '-'}</td>
<td className="px-4 py-2.5 dg-text-secondary">{r.region || '-'}</td>
<td className="px-4 py-2.5 dg-text-secondary">{r.account_name || '-'}</td>
<td className="px-4 py-2.5 text-right text-[var(--text-primary)] font-medium">{formatCurrency(r.cost)}</td>
<td className="px-4 py-2.5 text-right text-[var(--text-primary)] font-medium">{formatCurrencyByCode(r.cost, r.currency)}</td>
<td className="px-4 py-2.5 dg-text-muted max-w-xs truncate">{r.description || '-'}</td>
<td className="px-4 py-2.5">
<div className="flex items-center justify-center gap-1">
+1 -1
View File
@@ -388,7 +388,7 @@ export function Sidebar() {
</div>
{/* Menu Items */}
<ScrollArea className="flex-1 py-2 px-1.5">
<ScrollArea className="flex-1 min-h-0 py-2 px-1.5 overscroll-contain">
<nav className="space-y-0.5">
{visibleMenuItems.map((item) => (
<MenuItemComponent key={item.id} item={item} />