fix: 修复整合后的前端类型契约

- 补齐账单币种与公共组件类型导出
- 修复 LDAP 和主机监控拆分组件类型
- 删除无后端接口且未引用的旧凭证弹窗
This commit is contained in:
Logan.Li committed 2026-08-05 20:18:48 +08:00
1 parent a0edab6569
commit fe07598039
7 files changed
+27 -159

No files matched your search

+1 -1
View File
@@ -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 }) => { 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); 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<ListResponse>('/cloud-billings', params); return http.get<ListResponse>('/cloud-billings', params);
}, },
create: async (data: Record<string, unknown>) => { create: async (data: Record<string, unknown>) => {
@@ -42,6 +42,21 @@ export function formatCurrency(n: number) {
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; 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 }: { export function StatCard({ icon, label, value, trend, trendLabel, alert }: {
icon: ReactNode; icon: ReactNode;
label: string; label: string;
@@ -106,4 +121,4 @@ export function TrendChart({ data }: { data: { date: string; cost: number }[] })
</div> </div>
</div> </div>
); );
} }
@@ -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<number | string>('');
const [credentials, setCredentials] = useState<Credential[]>([]);
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[480px] bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<CredIcon className="h-5 w-5 text-[var(--dg-blue)]" />
{editId ? t('common.edit') : t('common.add')} {isRepo ? t('deploy.credForm.gitRepo') : t('deploy.credForm.imageRegistry')}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-1"><Label>{t('deploy.form.nameStar')}</Label>
<Input value={name} onChange={e => 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)]" /></div>
<div className="space-y-1"><Label>{isRepo ? t('deploy.form.repoUrlStar') : 'Registry URL *'}</Label>
<Input value={url} onChange={e => 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)]" /></div>
{!isRepo && <div className="space-y-1"><Label>{t('deploy.form.defaultNamespace')}</Label>
<Input value={namespace} onChange={e => setNamespace(e.target.value)} placeholder={t('deploy.credForm.namespacePlaceholder')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>}
{/* 关联凭证 */}
<div className="space-y-1">
<Label>{t('deploy.cred.linkedCred')} <span className="text-xs text-[var(--dg-text-secondary)] opacity-70">{t('deploy.credForm.credHint')}</span></Label>
<Select value={String(credentialId)} onValueChange={v => setCredentialId(v === '__none' ? '' : v)}>
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
<SelectValue placeholder={isRepo ? t('deploy.cred.selectApiCred') : t('deploy.cred.selectDockerPwd')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">{t('deploy.cred.notLinkedCred')}</SelectItem>
{filteredCredentials.map(c => (
<SelectItem key={c.id} value={String(c.id)}>
<span className="flex items-center gap-1.5">
<Key className="h-3 w-3 opacity-60" />
{c.name} {c.summary && <span className="text-[10px] opacity-50">({c.summary})</span>}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
{filteredCredentials.length === 0 && (
<p className="text-xs dg-text-yellow flex items-center gap-1 mt-1">
<Plus className="h-3 w-3" />
{t('deploy.credForm.noCredHint', { type: isRepo ? '' : t('deploy.credForm.dockerPwd') })}
</p>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('common.cancel')}</Button>
<Button onClick={handleSave} disabled={saving}>{saving ? t('deploy.pipeline.saving') : t('common.save')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -51,7 +51,7 @@ export interface LdapFormData {
search_filter: string; search_filter: string;
} }
interface SystemSettingsTabProps { export interface SystemSettingsTabProps {
// 审计策略 // Logan.Li // 审计策略 // Logan.Li
auditRetentionDays: string; auditRetentionDays: string;
setAuditRetentionDays: (v: string) => void; setAuditRetentionDays: (v: string) => void;
@@ -17,7 +17,6 @@ type LdapSettingsTabProps = Pick<
| "ldapSaving" | "ldapSaving"
| "ldapTesting" | "ldapTesting"
| "ldapTestResult" | "ldapTestResult"
| "ldapTestSuccess"
| "handleLdapSave" | "handleLdapSave"
| "handleLdapTest" | "handleLdapTest"
> & > &
@@ -76,7 +75,6 @@ export function LdapSettingsTab(props: LdapSettingsTabProps) {
ldapSaving, ldapSaving,
ldapTesting, ldapTesting,
ldapTestResult, ldapTestResult,
ldapTestSuccess,
handleLdapSave, handleLdapSave,
handleLdapTest, handleLdapTest,
} = props; } = props;
@@ -206,7 +204,7 @@ export function LdapSettingsTab(props: LdapSettingsTabProps) {
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-[var(--dg-border)]"> <div className="flex items-center gap-3 mt-4 pt-4 border-t border-[var(--dg-border)]">
{ldapTestResult && ( {ldapTestResult && (
<span <span
className={`text-xs ${ldapTestSuccess ? "dg-text-green" : "dg-text-red"}`} className={`text-xs ${ldapTestResult.includes("成功") ? "dg-text-green" : "dg-text-red"}`}
> >
{ldapTestResult} {ldapTestResult}
</span> </span>
@@ -7,7 +7,7 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import i18n from '@/i18n'; import i18n from '@/i18n';
interface ConfirmOptions { export interface ConfirmOptions {
title?: string; title?: string;
description?: string; description?: string;
confirmText?: string; confirmText?: string;
@@ -42,7 +42,12 @@ export const TIME_RANGES = TIME_RANGE_PRESETS.map((p) => ({
export type AggType = 'avg' | 'max' | 'min'; export type AggType = 'avg' | 'max' | 'min';
export interface RefLine {
value: number;
label?: string;
color?: string;
}
export type HostRecord = Record<string, unknown>; export type HostRecord = Record<string, unknown>;
export interface BucketPoint { export interface BucketPoint {
@@ -84,4 +89,4 @@ export const DEFAULT_PROBE_FORM = (hostname = ''): ProbeForm => ({
timeout: 10, timeout: 10,
interval: 60, interval: 60,
description: '', description: '',
}); });