diff --git a/frontend/src/api/cloud/billing.ts b/frontend/src/api/cloud/billing.ts index a948e5d..e4c24ad 100644 --- a/frontend/src/api/cloud/billing.ts +++ b/frontend/src/api/cloud/billing.ts @@ -16,7 +16,7 @@ export const billingApi = { getCostAnalysis: async (params?: { start_month?: string; end_month?: string; dimension?: string; currency?: string; provider?: string; cloud_account_id?: number }) => { return http.get('/cloud-billings/cost-analysis', params); }, - 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('/cloud-billings', params); }, create: async (data: Record) => { diff --git a/frontend/src/components/cloud/CloudBillingHelpers.tsx b/frontend/src/components/cloud/CloudBillingHelpers.tsx index 1db593a..97b200e 100644 --- a/frontend/src/components/cloud/CloudBillingHelpers.tsx +++ b/frontend/src/components/cloud/CloudBillingHelpers.tsx @@ -42,6 +42,21 @@ export function formatCurrency(n: number) { return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } +// 按账单原币种展示金额,未知币种回退为币种代码前缀。 // Logan.Li +export function formatCurrencyByCode(n: number, currency = 'CNY') { + const normalizedCurrency = currency.trim().toUpperCase() || 'CNY'; + try { + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: normalizedCurrency, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(n); + } catch { + return `${normalizedCurrency} ${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + } +} + export function StatCard({ icon, label, value, trend, trendLabel, alert }: { icon: ReactNode; label: string; @@ -106,4 +121,4 @@ export function TrendChart({ data }: { data: { date: string; cost: number }[] }) ); -} \ No newline at end of file +} diff --git a/frontend/src/components/deploy/CredentialFormDialog.tsx b/frontend/src/components/deploy/CredentialFormDialog.tsx deleted file mode 100644 index ffbc9d4..0000000 --- a/frontend/src/components/deploy/CredentialFormDialog.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Plus, Key, Globe, Container } from 'lucide-react'; -import { toast } from 'sonner'; -import { credentialApi, deployApi } from '@/api'; -import type { JsonData } from '@/types/common'; - -interface Repo { - id: number; name: string; url: string; credential_id: number | null; -} -interface Registry { - id: number; name: string; registry_url: string; namespace: string; credential_id: number | null; -} -interface Credential { - id: number; name: string; secret_type: string; summary: string; -} - -interface Props { - open: boolean; - onOpenChange: (v: boolean) => void; - type: 'repo' | 'registry'; - editId: number | null; - repos: Repo[]; - registries: Registry[]; - onSave: () => void; -} - -export default function CredentialFormDialog({ open, onOpenChange, type, editId, repos, registries, onSave }: Props) { - const { t } = useTranslation(); - const isRepo = type === 'repo'; - const [name, setName] = useState(''); - const [desc, setDesc] = useState(''); - const [url, setUrl] = useState(''); - const [namespace, setNamespace] = useState(''); - const [credentialId, setCredentialId] = useState(''); - const [credentials, setCredentials] = useState([]); - const [saving, setSaving] = useState(false); - - // Docker 仓库 → docker 类型;Git 仓库 → api 或 docker 类型 - const filteredCredentials = credentials.filter(c => - isRepo ? true : c.secret_type === 'docker' - ); - - const loadCredentials = async () => { - try { - const res: JsonData = await credentialApi.list({ page_size: 100 }); - setCredentials(res?.list || []); - } catch { /* silent */ } - }; - - useEffect(() => { - loadCredentials(); - if (editId) { - if (isRepo) { - const r = repos.find(x => x.id === editId); - if (r) { setName(r.name); setDesc(''); setUrl(r.url); setCredentialId(r.credential_id || ''); } - } else { - const r = registries.find(x => x.id === editId); - if (r) { setName(r.name); setDesc(''); setUrl(r.registry_url); setNamespace(r.namespace || ''); setCredentialId(r.credential_id || ''); } - } - } else { - setName(''); setDesc(''); setUrl(''); setNamespace(''); setCredentialId(''); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editId, open, isRepo]); - - const handleSave = async () => { - if (!name.trim() || !url.trim()) { toast.error(t('deploy.form.nameAddrRequired')); return; } - setSaving(true); - try { - const cid = credentialId ? Number(credentialId) : null; - const payload = isRepo - ? { name: name.trim(), description: desc.trim(), url: url.trim(), credential_id: cid } - : { name: name.trim(), description: desc.trim(), registry_url: url.trim(), namespace: namespace.trim(), credential_id: cid }; - if (editId) { - if (isRepo) await deployApi.updateRepo(editId, payload); - else await deployApi.updateRegistry(editId, payload); - toast.success(t('deploy.toast.updated')); - } else { - if (isRepo) await deployApi.createRepo(payload); - else await deployApi.createRegistry(payload); - toast.success(t('deploy.toast.created')); - } - onOpenChange(false); - onSave(); - } catch { toast.error(t('deploy.toast.saveFailed')); } - finally { setSaving(false); } - }; - - const CredIcon = isRepo ? Globe : Container; - - return ( - - - - - - {editId ? t('common.edit') : t('common.add')} {isRepo ? t('deploy.credForm.gitRepo') : t('deploy.credForm.imageRegistry')} - - -
-
- setName(e.target.value)} placeholder={isRepo ? t('deploy.credForm.repoNamePlaceholder') : t('deploy.credForm.registryNamePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
-
- setUrl(e.target.value)} placeholder={isRepo ? 'http://10.10.10.72:9000/group/repo.git' : 'http://10.10.10.72'} className="font-mono text-sm bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
- {!isRepo &&
- setNamespace(e.target.value)} placeholder={t('deploy.credForm.namespacePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
} - - {/* 关联凭证 */} -
- - - {filteredCredentials.length === 0 && ( -

- - {t('deploy.credForm.noCredHint', { type: isRepo ? '' : t('deploy.credForm.dockerPwd') })} -

- )} -
-
- - - - -
-
- ); -} diff --git a/frontend/src/components/system/SystemSettingsTab.tsx b/frontend/src/components/system/SystemSettingsTab.tsx index c95760c..9d6d562 100644 --- a/frontend/src/components/system/SystemSettingsTab.tsx +++ b/frontend/src/components/system/SystemSettingsTab.tsx @@ -51,7 +51,7 @@ export interface LdapFormData { search_filter: string; } -interface SystemSettingsTabProps { +export interface SystemSettingsTabProps { // 审计策略 // Logan.Li auditRetentionDays: string; setAuditRetentionDays: (v: string) => void; diff --git a/frontend/src/components/system/system-settings/LdapSettingsTab.tsx b/frontend/src/components/system/system-settings/LdapSettingsTab.tsx index 00b245e..de4b2dc 100644 --- a/frontend/src/components/system/system-settings/LdapSettingsTab.tsx +++ b/frontend/src/components/system/system-settings/LdapSettingsTab.tsx @@ -17,7 +17,6 @@ type LdapSettingsTabProps = Pick< | "ldapSaving" | "ldapTesting" | "ldapTestResult" - | "ldapTestSuccess" | "handleLdapSave" | "handleLdapTest" > & @@ -76,7 +75,6 @@ export function LdapSettingsTab(props: LdapSettingsTabProps) { ldapSaving, ldapTesting, ldapTestResult, - ldapTestSuccess, handleLdapSave, handleLdapTest, } = props; @@ -206,7 +204,7 @@ export function LdapSettingsTab(props: LdapSettingsTabProps) {
{ldapTestResult && ( {ldapTestResult} diff --git a/frontend/src/components/ui/confirm-dialog.tsx b/frontend/src/components/ui/confirm-dialog.tsx index f4cbf20..e2c715f 100644 --- a/frontend/src/components/ui/confirm-dialog.tsx +++ b/frontend/src/components/ui/confirm-dialog.tsx @@ -7,7 +7,7 @@ import { import { Button } from '@/components/ui/button'; import i18n from '@/i18n'; -interface ConfirmOptions { +export interface ConfirmOptions { title?: string; description?: string; confirmText?: string; diff --git a/frontend/src/pages/monitor/host-monitor/types.ts b/frontend/src/pages/monitor/host-monitor/types.ts index e1a4b8b..3dd1c4a 100644 --- a/frontend/src/pages/monitor/host-monitor/types.ts +++ b/frontend/src/pages/monitor/host-monitor/types.ts @@ -42,7 +42,12 @@ export const TIME_RANGES = TIME_RANGE_PRESETS.map((p) => ({ export type AggType = 'avg' | 'max' | 'min'; - +export interface RefLine { + value: number; + label?: string; + color?: string; +} + export type HostRecord = Record; export interface BucketPoint { @@ -84,4 +89,4 @@ export const DEFAULT_PROBE_FORM = (hostname = ''): ProbeForm => ({ timeout: 10, interval: 60, description: '', -}); \ No newline at end of file +});