From 480e677042e1a8dd642a0d51711f8c10016f3950 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Fri, 26 Jun 2026 00:22:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20P1-P2=20=E7=BB=AD=20?= =?UTF-8?q?=E2=80=94=20API=20=E5=AD=90=E6=A8=A1=E5=9D=97=E5=8C=96=E3=80=81?= =?UTF-8?q?=E5=A4=A7=E7=BB=84=E4=BB=B6=E6=8B=86=E5=88=86=E3=80=81Query=20?= =?UTF-8?q?=E9=93=BA=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api/server/ 与 api/cloud/ 子目录拆分,保持 @/api 导入不变 - hooks/api 新增 useCredentials、usePromDatasources,增强 useServers - 拆分 CredentialManager、DeploymentWorkload、PrometheusDatasourceManager - ServerManager、CredentialManager、PromDs 接入 TanStack Query --- frontend/src/api/cloud.ts | 107 +-- frontend/src/api/cloud/account.ts | 42 ++ frontend/src/api/cloud/billing.ts | 39 + frontend/src/api/cloud/index.ts | 5 + frontend/src/api/cloud/platform.ts | 18 + frontend/src/api/server.ts | 105 +-- frontend/src/api/server/index.ts | 5 + frontend/src/api/server/region.ts | 21 + frontend/src/api/server/resource-model.ts | 18 + frontend/src/api/server/server.ts | 58 ++ .../components/deploy/CredentialManager.tsx | 675 +---------------- .../credential-manager/CredDetailDialog.tsx | 180 +++++ .../credential-manager/CredFormDialog.tsx | 395 ++++++++++ .../deploy/credential-manager/constants.ts | 26 + .../deploy/credential-manager/types.ts | 18 + .../kubernetes/DeploymentWorkload.tsx | 390 +--------- .../deployment-workload/ActionModals.tsx | 121 ++++ .../CICDWebhookContent.tsx | 50 ++ .../deployment-workload/EditModeContainer.tsx | 117 +++ .../deployment-workload/ModalShell.tsx | 24 + .../deployment-workload/PodLogsModal.tsx | 100 +++ .../kubernetes/deployment-workload/types.ts | 25 + .../PrometheusDatasourceManager.tsx | 676 +----------------- .../prometheus-ds/PromDsFormDialog.tsx | 527 ++++++++++++++ .../datasource/prometheus-ds/helpers.ts | 115 +++ .../monitor/datasource/prometheus-ds/types.ts | 58 ++ .../src/components/server/ServerManager.tsx | 114 ++- frontend/src/hooks/api/index.ts | 4 +- frontend/src/hooks/api/queryKeys.ts | 3 + frontend/src/hooks/api/useCredentials.ts | 77 ++ frontend/src/hooks/api/usePromDatasources.ts | 72 ++ frontend/src/hooks/api/useServers.ts | 35 +- 32 files changed, 2280 insertions(+), 1940 deletions(-) create mode 100644 frontend/src/api/cloud/account.ts create mode 100644 frontend/src/api/cloud/billing.ts create mode 100644 frontend/src/api/cloud/index.ts create mode 100644 frontend/src/api/cloud/platform.ts create mode 100644 frontend/src/api/server/index.ts create mode 100644 frontend/src/api/server/region.ts create mode 100644 frontend/src/api/server/resource-model.ts create mode 100644 frontend/src/api/server/server.ts create mode 100644 frontend/src/components/deploy/credential-manager/CredDetailDialog.tsx create mode 100644 frontend/src/components/deploy/credential-manager/CredFormDialog.tsx create mode 100644 frontend/src/components/deploy/credential-manager/constants.ts create mode 100644 frontend/src/components/deploy/credential-manager/types.ts create mode 100644 frontend/src/components/kubernetes/deployment-workload/ActionModals.tsx create mode 100644 frontend/src/components/kubernetes/deployment-workload/CICDWebhookContent.tsx create mode 100644 frontend/src/components/kubernetes/deployment-workload/EditModeContainer.tsx create mode 100644 frontend/src/components/kubernetes/deployment-workload/ModalShell.tsx create mode 100644 frontend/src/components/kubernetes/deployment-workload/PodLogsModal.tsx create mode 100644 frontend/src/components/kubernetes/deployment-workload/types.ts create mode 100644 frontend/src/components/monitor/datasource/prometheus-ds/PromDsFormDialog.tsx create mode 100644 frontend/src/components/monitor/datasource/prometheus-ds/helpers.ts create mode 100644 frontend/src/components/monitor/datasource/prometheus-ds/types.ts create mode 100644 frontend/src/hooks/api/useCredentials.ts create mode 100644 frontend/src/hooks/api/usePromDatasources.ts diff --git a/frontend/src/api/cloud.ts b/frontend/src/api/cloud.ts index b18bcd3..561da9c 100644 --- a/frontend/src/api/cloud.ts +++ b/frontend/src/api/cloud.ts @@ -1,106 +1,3 @@ -// 云账号/平台/账单 -// 从 services/api.ts 拆分 — Q3 api.ts 模块化 +// 云资源 API — 子模块 re-export(保持 @/api 导入路径不变) // Logan.Li -import { http, type ListResponse } from '@/utils/request'; - -export const cloudApi = { - getAccounts: async () => { - return http.get('/cloud-accounts'); - }, - createAccount: async ( data: any) => { - return http.post('/cloud-accounts', data); - }, - updateAccount: async (id: number | string, data: any) => { - return http.put(`/cloud-accounts/${id}`, data); - }, - deleteAccount: async (id: number | string) => { - return http.delete(`/cloud-accounts/${id}`); - }, - // ==================== 云同步 API ==================== - // 同步云账号的区域列表到本地 - syncRegions: async (id: number | string) => { - return http.post(`/cloud-accounts/${id}/sync-regions`); - }, - // 获取云账号的区域列表 - getAccountRegions: async (id: number | string) => { - return http.get(`/cloud-accounts/${id}/regions`); - }, - // 发现指定区域的云主机实例 - discoverInstances: async (id: number | string, regions: string[]) => { - return http.post<{ list: any[]; total: number; errors: string[] }>(`/cloud-accounts/${id}/discover-instances`, { regions }); - }, - // 导入选中的云主机实例为本地服务器 - importInstances: async (id: number | string, instances: any[], opts?: { default_port?: number; credential_id?: number }) => { - return http.post<{ imported: number; skipped: number }>(`/cloud-accounts/${id}/import-instances`, { instances, ...opts }); - }, - getDomains: async (params?: { page?: number; page_size?: number; keyword?: string }) => { - return http.get('/domains', params); - }, - createDomain: async ( data: any) => { - return http.post('/domains', data); - }, - updateDomain: async (id: number | string, data: any) => { - return http.put(`/domains/${id}`, data); - }, - deleteDomain: async (id: number | string) => { - return http.delete(`/domains/${id}`); - }, -}; - -// ==================== 云平台 API ==================== - -export const platformApi = { - list: async () => { - return http.get('/platforms'); - }, - create: async ( data: any) => { - return http.post('/platforms', data); - }, - update: async (id: number | string, data: any) => { - return http.put(`/platforms/${id}`, data); - }, - delete: async (id: number | string) => { - return http.delete(`/platforms/${id}`); - }, -}; - -// ==================== 云账单 API ==================== - -export const billingApi = { - getStats: async () => { - return http.get('/cloud-billings/stats'); - }, - list: async (params?: { provider?: string; start_date?: string; end_date?: string; page?: number; page_size?: number }) => { - return http.get('/cloud-billings', params); - }, - create: async ( data: any) => { - return http.post('/cloud-billings', data); - }, - batchCreate: async ( records: any[]) => { - return http.post('/cloud-billings/batch', { records }); - }, - remove: async (id: number | string) => { - return http.delete(`/cloud-billings/${id}`); - }, - getProviders: async () => { - return http.get('/cloud-billings/providers'); - }, - // 预警规则 - listAlertRules: async () => { - return http.get('/billing-alerts'); - }, - createAlertRule: async ( data: any) => { - return http.post('/billing-alerts', data); - }, - updateAlertRule: async (id: number | string, data: any) => { - return http.put(`/billing-alerts/${id}`, data); - }, - deleteAlertRule: async (id: number | string) => { - return http.delete(`/billing-alerts/${id}`); - }, - toggleAlertRule: async (id: number | string) => { - return http.post(`/billing-alerts/${id}/toggle`); - }, -}; - -// ==================== Agent 监控 API ==================== +export * from './cloud/index'; \ No newline at end of file diff --git a/frontend/src/api/cloud/account.ts b/frontend/src/api/cloud/account.ts new file mode 100644 index 0000000..f962544 --- /dev/null +++ b/frontend/src/api/cloud/account.ts @@ -0,0 +1,42 @@ +// 云账号 / 域名 API // Logan.Li + +import { http, type ListResponse } from '@/utils/request'; + +export const cloudApi = { + getAccounts: async () => { + return http.get('/cloud-accounts'); + }, + createAccount: async (data: Record) => { + return http.post('/cloud-accounts', data); + }, + updateAccount: async (id: number | string, data: Record) => { + return http.put(`/cloud-accounts/${id}`, data); + }, + deleteAccount: async (id: number | string) => { + return http.delete(`/cloud-accounts/${id}`); + }, + syncRegions: async (id: number | string) => { + return http.post(`/cloud-accounts/${id}/sync-regions`); + }, + getAccountRegions: async (id: number | string) => { + return http.get(`/cloud-accounts/${id}/regions`); + }, + discoverInstances: async (id: number | string, regions: string[]) => { + return http.post<{ list: Record[]; total: number; errors: string[] }>(`/cloud-accounts/${id}/discover-instances`, { regions }); + }, + importInstances: async (id: number | string, instances: Record[], opts?: { default_port?: number; credential_id?: number }) => { + return http.post<{ imported: number; skipped: number }>(`/cloud-accounts/${id}/import-instances`, { instances, ...opts }); + }, + getDomains: async (params?: { page?: number; page_size?: number; keyword?: string }) => { + return http.get('/domains', params); + }, + createDomain: async (data: Record) => { + return http.post('/domains', data); + }, + updateDomain: async (id: number | string, data: Record) => { + return http.put(`/domains/${id}`, data); + }, + deleteDomain: async (id: number | string) => { + return http.delete(`/domains/${id}`); + }, +}; \ No newline at end of file diff --git a/frontend/src/api/cloud/billing.ts b/frontend/src/api/cloud/billing.ts new file mode 100644 index 0000000..70eb9f4 --- /dev/null +++ b/frontend/src/api/cloud/billing.ts @@ -0,0 +1,39 @@ +// 云账单 / 预警 API // Logan.Li + +import { http, type ListResponse } from '@/utils/request'; + +export const billingApi = { + getStats: async () => { + return http.get('/cloud-billings/stats'); + }, + list: async (params?: { provider?: string; start_date?: string; end_date?: string; page?: number; page_size?: number }) => { + return http.get('/cloud-billings', params); + }, + create: async (data: Record) => { + return http.post('/cloud-billings', data); + }, + batchCreate: async (records: Record[]) => { + return http.post('/cloud-billings/batch', { records }); + }, + remove: async (id: number | string) => { + return http.delete(`/cloud-billings/${id}`); + }, + getProviders: async () => { + return http.get('/cloud-billings/providers'); + }, + listAlertRules: async () => { + return http.get('/billing-alerts'); + }, + createAlertRule: async (data: Record) => { + return http.post('/billing-alerts', data); + }, + updateAlertRule: async (id: number | string, data: Record) => { + return http.put(`/billing-alerts/${id}`, data); + }, + deleteAlertRule: async (id: number | string) => { + return http.delete(`/billing-alerts/${id}`); + }, + toggleAlertRule: async (id: number | string) => { + return http.post(`/billing-alerts/${id}/toggle`); + }, +}; \ No newline at end of file diff --git a/frontend/src/api/cloud/index.ts b/frontend/src/api/cloud/index.ts new file mode 100644 index 0000000..cc7ba00 --- /dev/null +++ b/frontend/src/api/cloud/index.ts @@ -0,0 +1,5 @@ +// 云资源 API 聚合出口 // Logan.Li + +export { cloudApi } from './account'; +export { platformApi } from './platform'; +export { billingApi } from './billing'; \ No newline at end of file diff --git a/frontend/src/api/cloud/platform.ts b/frontend/src/api/cloud/platform.ts new file mode 100644 index 0000000..6e1486e --- /dev/null +++ b/frontend/src/api/cloud/platform.ts @@ -0,0 +1,18 @@ +// 云平台 API // Logan.Li + +import { http, type ListResponse } from '@/utils/request'; + +export const platformApi = { + list: async () => { + return http.get('/platforms'); + }, + create: async (data: Record) => { + return http.post('/platforms', data); + }, + update: async (id: number | string, data: Record) => { + return http.put(`/platforms/${id}`, data); + }, + delete: async (id: number | string) => { + return http.delete(`/platforms/${id}`); + }, +}; \ No newline at end of file diff --git a/frontend/src/api/server.ts b/frontend/src/api/server.ts index 5e1c125..5d586d6 100644 --- a/frontend/src/api/server.ts +++ b/frontend/src/api/server.ts @@ -1,104 +1,3 @@ -// 服务器/资产/区域/资源型号 -// 从 services/api.ts 拆分 — Q3 api.ts 模块化 +// 服务器域 API — 子模块 re-export(保持 @/api 导入路径不变) // Logan.Li -import { http, type ListResponse } from '@/utils/request'; - -export const serverApi = { - getServers: async (params?: { page?: number; page_size?: number; keyword?: string; status?: string }) => { - return http.get('/servers', params); - }, - getServerDetail: async (id: number | string) => { - return http.get(`/servers/${id}`); - }, - getServerStats: async () => { - return http.get('/servers/stats'); - }, - createServer: async ( data: any) => { - return http.post('/servers', data); - }, - updateServer: async (id: number | string, data: any) => { - return http.put(`/servers/${id}`, data); - }, - deleteServer: async (id: number | string) => { - return http.delete(`/servers/${id}`); - }, - batchDelete: async (ids: number[]) => { - return http.post('/servers/batch-delete', { ids }); - }, - batchCreate: async (data: { servers: any[]; region_id?: string; auth_type?: string; credential_id?: number; default_port?: number }) => { - return http.post('/servers/batch', data); - }, - getMetrics: async (_id: number | string) => { - // 后端暂无单机 metrics,用 dashboard metrics - return http.get('/dashboard/metrics'); - }, - // 远程安装 Agent(通过 SSH 执行安装脚本),可选传入凭据 - installAgent: async (id: number | string, token: string, creds?: { username?: string; password?: string; ssh_key?: string; auth_type?: string }) => { - const body: Record = { token }; - if (creds) Object.assign(body, creds); - return http.post<{ installed: boolean; arch: string; output: string }>(`/servers/${id}/install-agent`, body); - }, - // 批量远程安装 Agent - batchInstallAgent: async (ids: number[], token: string) => { - return http.post<{ total: number; success: number; results: any[] }>(`/servers/install-agent?token=${encodeURIComponent(token)}`, { server_ids: ids }); - }, - - // SFTP 文件浏览 - listFiles: async (serverId: number, path: string) => { - return http.get(`/servers/${serverId}/files?path=${encodeURIComponent(path)}`); - }, - downloadFile: async (serverId: number, filePath: string) => { - return http.download(`/servers/${serverId}/files/download?path=${encodeURIComponent(filePath)}`); - }, - uploadFile: async (serverId: number, dirPath: string, file: File) => { - const formData = new FormData(); - formData.append('file', file); - return http.upload(`/servers/${serverId}/files/upload?path=${encodeURIComponent(dirPath)}`, formData); - }, - deleteFile: async (serverId: number, filePath: string) => { - return http.delete(`/servers/${serverId}/files?path=${encodeURIComponent(filePath)}`); - }, - mkdir: async (serverId: number, dirPath: string) => { - return http.post(`/servers/${serverId}/files/mkdir`, { path: dirPath }); - }, -}; - -// ==================== Kubernetes 集群 API ==================== - -export const regionApi = { - list: async () => { - return http.get('/regions'); - }, - create: async ( data: any) => { - return http.post('/regions', data); - }, - update: async (id: number | string, data: any) => { - return http.put(`/regions/${id}`, data); - }, - delete: async (id: number | string) => { - return http.delete(`/regions/${id}`); - }, - // 批量删除 // Logan.Li - batchDelete: async (ids: number[]) => { - return http.delete('/regions/batch-delete', { ids }); - }, -}; - -// ==================== 资源模型 API ==================== - -export const resourceModelApi = { - list: async () => { - return http.get('/resource-models'); - }, - create: async ( data: any) => { - return http.post('/resource-models', data); - }, - update: async (id: number | string, data: any) => { - return http.put(`/resource-models/${id}`, data); - }, - delete: async (id: number | string) => { - return http.delete(`/resource-models/${id}`); - }, -}; - -// ==================== 审计日志 API ==================== +export * from './server/index'; \ No newline at end of file diff --git a/frontend/src/api/server/index.ts b/frontend/src/api/server/index.ts new file mode 100644 index 0000000..228bc6e --- /dev/null +++ b/frontend/src/api/server/index.ts @@ -0,0 +1,5 @@ +// 服务器域 API 聚合出口 // Logan.Li + +export { serverApi } from './server'; +export { regionApi } from './region'; +export { resourceModelApi } from './resource-model'; \ No newline at end of file diff --git a/frontend/src/api/server/region.ts b/frontend/src/api/server/region.ts new file mode 100644 index 0000000..dc4d02b --- /dev/null +++ b/frontend/src/api/server/region.ts @@ -0,0 +1,21 @@ +// 区域 API // Logan.Li + +import { http, type ListResponse } from '@/utils/request'; + +export const regionApi = { + list: async () => { + return http.get('/regions'); + }, + create: async (data: Record) => { + return http.post('/regions', data); + }, + update: async (id: number | string, data: Record) => { + return http.put(`/regions/${id}`, data); + }, + delete: async (id: number | string) => { + return http.delete(`/regions/${id}`); + }, + batchDelete: async (ids: number[]) => { + return http.delete('/regions/batch-delete', { ids }); + }, +}; \ No newline at end of file diff --git a/frontend/src/api/server/resource-model.ts b/frontend/src/api/server/resource-model.ts new file mode 100644 index 0000000..bb85d08 --- /dev/null +++ b/frontend/src/api/server/resource-model.ts @@ -0,0 +1,18 @@ +// 资源型号 API // Logan.Li + +import { http, type ListResponse } from '@/utils/request'; + +export const resourceModelApi = { + list: async () => { + return http.get('/resource-models'); + }, + create: async (data: Record) => { + return http.post('/resource-models', data); + }, + update: async (id: number | string, data: Record) => { + return http.put(`/resource-models/${id}`, data); + }, + delete: async (id: number | string) => { + return http.delete(`/resource-models/${id}`); + }, +}; \ No newline at end of file diff --git a/frontend/src/api/server/server.ts b/frontend/src/api/server/server.ts new file mode 100644 index 0000000..e51e1dc --- /dev/null +++ b/frontend/src/api/server/server.ts @@ -0,0 +1,58 @@ +// 服务器资产 API // Logan.Li + +import { http, type ListResponse } from '@/utils/request'; + +export const serverApi = { + getServers: async (params?: { page?: number; page_size?: number; keyword?: string; status?: string }) => { + return http.get('/servers', params); + }, + getServerDetail: async (id: number | string) => { + return http.get(`/servers/${id}`); + }, + getServerStats: async () => { + return http.get('/servers/stats'); + }, + createServer: async (data: Record) => { + return http.post('/servers', data); + }, + updateServer: async (id: number | string, data: Record) => { + return http.put(`/servers/${id}`, data); + }, + deleteServer: async (id: number | string) => { + return http.delete(`/servers/${id}`); + }, + batchDelete: async (ids: number[]) => { + return http.post('/servers/batch-delete', { ids }); + }, + batchCreate: async (data: { servers: Record[]; region_id?: string; auth_type?: string; credential_id?: number; default_port?: number }) => { + return http.post('/servers/batch', data); + }, + getMetrics: async (_id: number | string) => { + return http.get('/dashboard/metrics'); + }, + installAgent: async (id: number | string, token: string, creds?: { username?: string; password?: string; ssh_key?: string; auth_type?: string }) => { + const body: Record = { token }; + if (creds) Object.assign(body, creds); + return http.post<{ installed: boolean; arch: string; output: string }>(`/servers/${id}/install-agent`, body); + }, + batchInstallAgent: async (ids: number[], token: string) => { + return http.post<{ total: number; success: number; results: Record[] }>(`/servers/install-agent?token=${encodeURIComponent(token)}`, { server_ids: ids }); + }, + listFiles: async (serverId: number, path: string) => { + return http.get(`/servers/${serverId}/files?path=${encodeURIComponent(path)}`); + }, + downloadFile: async (serverId: number, filePath: string) => { + return http.download(`/servers/${serverId}/files/download?path=${encodeURIComponent(filePath)}`); + }, + uploadFile: async (serverId: number, dirPath: string, file: File) => { + const formData = new FormData(); + formData.append('file', file); + return http.upload(`/servers/${serverId}/files/upload?path=${encodeURIComponent(dirPath)}`, formData); + }, + deleteFile: async (serverId: number, filePath: string) => { + return http.delete(`/servers/${serverId}/files?path=${encodeURIComponent(filePath)}`); + }, + mkdir: async (serverId: number, dirPath: string) => { + return http.post(`/servers/${serverId}/files/mkdir`, { path: dirPath }); + }, +}; \ No newline at end of file diff --git a/frontend/src/components/deploy/CredentialManager.tsx b/frontend/src/components/deploy/CredentialManager.tsx index 96ff4a9..36957b0 100644 --- a/frontend/src/components/deploy/CredentialManager.tsx +++ b/frontend/src/components/deploy/CredentialManager.tsx @@ -1,659 +1,40 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState } from 'react'; import { - - Plus, - Pencil, - Trash2, - RefreshCw, - Key, - Shield, - Globe, - Search, - Lock, - Eye, - EyeOff, - Copy, - Server, - Container, - Database, - FileText, - Cloud, - Clock, - AlertTriangle, - CheckCircle2, - XCircle, - Zap, + Plus, Pencil, Trash2, RefreshCw, Key, Shield, Search, + Clock, CheckCircle2, XCircle, Zap, } from 'lucide-react'; - import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; - import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; -import { credentialApi } from '@/api'; - -// ────── Types ────── - -interface Credential { - id: number; - name: string; - variable_name: string; - secret_type: SecretType; - summary: string; - tags: string[]; - description: string; - expires_at: string | null; - is_active: boolean; - env_prefix: string; - created_at: string; - updated_at: string; -} - -type SecretType = 'ssh' | 'docker' | 'tls' | 'aksk' | 'database' | 'api' | 'generic'; - -const TYPE_CONFIG: Record = { - ssh: { label: 'SSH 密钥', icon: Server, color: 'text-amber-400', desc: 'SSH 私钥/公钥/密码' }, - docker: { label: 'Docker 凭证', icon: Container, color: 'text-cyan-400', desc: 'Docker Registry 登录凭据' }, - tls: { label: 'TLS 证书', icon: Shield, color: 'text-green-400', desc: 'TLS/SSL 证书与私钥' }, - aksk: { label: 'AKSK 密钥', icon: Cloud, color: 'text-purple-400', desc: '云服务 AccessKey/SecretKey' }, - database: { label: '数据库凭证', icon: Database, color: 'text-blue-400', desc: '数据库连接信息' }, - api: { label: 'API 密钥', icon: Globe, color: 'text-orange-400', desc: 'API Token/Key + 端点' }, - generic: { label: '通用凭证', icon: FileText, color: 'text-gray-400', desc: '自定义 Key/Value 键值对' }, -}; - -const TYPE_BADGE_CLASS: Record = { - ssh: 'bg-amber-500/20 text-amber-400 border-amber-500/30', - docker: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30', - tls: 'bg-green-500/20 text-green-400 border-green-500/30', - aksk: 'bg-purple-500/20 text-purple-400 border-purple-500/30', - database: 'bg-blue-500/20 text-blue-400 border-blue-500/30', - api: 'bg-orange-500/20 text-orange-400 border-orange-500/30', - generic: 'bg-gray-500/20 text-gray-400 border-gray-500/30', -}; - -// ────── Detail Dialog ────── - -function CredDetailDialog({ open, onClose, credId }: { open: boolean; onClose: () => void; credId: number | null }) { - const [detail, setDetail] = useState(null); - const [loading, setLoading] = useState(false); - const [showMap, setShowMap] = useState>({}); - - useEffect(() => { - if (!credId || !open) return; - setLoading(true); - credentialApi.get(credId).then(( res: any) => { - setDetail(res); - }).catch(() => toast.error('加载详情失败')).finally(() => setLoading(false)); - }, [credId, open]); - - if (!open || !credId) return null; - - const toggleShow = (key: string) => setShowMap(prev => ({ ...prev, [key]: !prev[key] })); - const copyValue = (val: string) => { navigator.clipboard.writeText(val); toast.success('已复制'); }; - - const st = detail?.secret_type as SecretType; - const cfg = TYPE_CONFIG[st]; - const content = detail?.content || {}; - - const Field = ({ label, value, sensitive, fieldKey }: { label: string; value: any; sensitive?: boolean; fieldKey?: string }) => { - if (value === undefined || value === null || value === '') return null; - const isLong = typeof value === 'string' && value.length > 80; - const show = fieldKey ? showMap[fieldKey] : false; - return ( -
-
- {label} -
-
- {isLong ? ( -