feat: 补全多云资产与账单同步
- 接入 AWS、华为云和 Azure 资产及账单 API - 按原始币种隔离统计并修正账号服务器数 - 修复侧栏菜单展开后的滚动行为
This commit is contained in:
1 parent
65e2f09b97
commit
132fe9ba97
25 files changed
+1094
-141
No files matched your search
@@ -3,13 +3,13 @@
|
||||
import { http, type ListResponse } from '@/utils/request';
|
||||
|
||||
export const billingApi = {
|
||||
getStats: async () => {
|
||||
return http.get('/cloud-billings/stats');
|
||||
getStats: async (currency = 'CNY') => {
|
||||
return http.get('/cloud-billings/stats', { currency });
|
||||
},
|
||||
getCostAnalysis: async () => {
|
||||
return http.get('/cloud-billings/cost-analysis');
|
||||
},
|
||||
list: async (params?: { provider?: string; cloud_account_id?: number; start_date?: string; end_date?: string; page?: number; page_size?: number }) => {
|
||||
list: async (params?: { provider?: string; cloud_account_id?: number; currency?: string; start_date?: string; end_date?: string; page?: number; page_size?: number }) => {
|
||||
return http.get<ListResponse>('/cloud-billings', params);
|
||||
},
|
||||
create: async (data: Record<string, unknown>) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -6945,6 +6945,11 @@
|
||||
"accountId": "Account ID",
|
||||
"accountName": "Account Name",
|
||||
"accountNamePlaceholder": "e.g. Aliyun Production",
|
||||
"azureAccessKeyPlaceholder": "tenantId:subscriptionId:clientId",
|
||||
"awsAccessKeyPlaceholder": "AWS Access Key ID",
|
||||
"defaultAccessKeyPlaceholder": "AccessKey ID",
|
||||
"azureSecretKeyPlaceholder": "Azure client secret",
|
||||
"defaultSecretKeyPlaceholder": "SecretKey",
|
||||
"accountSecretKey": "SecretKey",
|
||||
"accountStatus": "Status",
|
||||
"accountType": "Vendor",
|
||||
|
||||
@@ -6945,6 +6945,11 @@
|
||||
"accountId": "账号 ID",
|
||||
"accountName": "账号名称",
|
||||
"accountNamePlaceholder": "阿里云生产账号",
|
||||
"azureAccessKeyPlaceholder": "租户ID:订阅ID:客户端ID",
|
||||
"awsAccessKeyPlaceholder": "AWS Access Key ID",
|
||||
"defaultAccessKeyPlaceholder": "AccessKey ID",
|
||||
"azureSecretKeyPlaceholder": "Azure 客户端密钥",
|
||||
"defaultSecretKeyPlaceholder": "SecretKey",
|
||||
"accountSecretKey": "SecretKey",
|
||||
"accountStatus": "状态",
|
||||
"accountType": "云厂商",
|
||||
|
||||
Reference in new issue
Block a user