feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)

- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
attacker committed 2026-06-26 21:13:15 +08:00
1 parent 5ca0e6d83a
commit c68aef4444
150 files changed
+1038 -690

No files matched your search

@@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
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;
@@ -47,7 +48,7 @@ export default function CredentialFormDialog({ open, onOpenChange, type, editId,
const loadCredentials = async () => {
try {
const res: Record<string, any> = await credentialApi.list({ page_size: 100 });
const res: JsonData = await credentialApi.list({ page_size: 100 });
setCredentials(res?.list || []);
} catch { /* silent */ }
};
@@ -12,6 +12,7 @@ import { useConfirm } from '@/components/ui/confirm-dialog';
import { deployApi } from '@/api';
import CredentialFormDialog from './CredentialFormDialog';
import ToolConfigTab from './ToolConfigTab';
import type { JsonData } from '@/types/common';
interface Repo {
id: number; name: string; description: string; url: string;
@@ -110,16 +111,16 @@ export default function DeployCredentials() {
setLoading(true);
try {
const [rR, rReg, rEnv] = await Promise.all([deployApi.getRepos(), deployApi.getRegistries(), deployApi.getEnvironments()]);
setRepos((rR as Record<string, any>)?.list || []);
setRegistries((rReg as Record<string, any>)?.list || []);
setEnvironments((rEnv as Record<string, any>)?.list || []);
setRepos((rR as JsonData)?.list || []);
setRegistries((rReg as JsonData)?.list || []);
setEnvironments((rEnv as JsonData)?.list || []);
} catch { toast.error('获取数据失败'); }
finally { setLoading(false); }
};
useEffect(() => { fetchAll(); }, []);
const handleDelete = async (type: 'repo' | 'registry' | 'env', item: any) => {
const handleDelete = async (type: 'repo' | 'registry' | 'env', item: JsonData) => {
const ok = await _confirm({ title: '删除', description: `确定删除「${item.name || item.display_name}」?`, confirmText: '删除', variant: 'destructive' });
if (!ok) return;
try {
@@ -9,6 +9,7 @@ import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
import { deployApi, userApi, pipelineApi } from '@/api';
import { useConfirm } from '@/components/ui/confirm-dialog';
import type { JsonData } from '@/types/common';
interface ItemRole {
id: number; name: string; code: string; pattern: string; role: string;
@@ -27,11 +28,11 @@ export default function DeployRBAC() {
const [saving, setSaving] = useState(false);
// Pipeline list for live preview
const [pipelines, setPipelines] = useState<any[]>([]);
const [pipelines, setPipelines] = useState<JsonData[]>([]);
const [previewRoleId, setPreviewRoleId] = useState<number | null>(null);
// User assignment
const [users, setUsers] = useState<any[]>([]);
const [users, setUsers] = useState<JsonData[]>([]);
const [assignments, setAssignments] = useState<AssignedUser[]>([]);
const [selectedUserId, setSelectedUserId] = useState<string>('');
const [assignRoleId, setAssignRoleId] = useState<string>('');
@@ -60,9 +61,9 @@ export default function DeployRBAC() {
pipelineApi.getPipelines({ page_size: 200 }),
userApi.getUsers({ page_size: 500 }),
]);
setRoles(Array.isArray(rolesRes) ? rolesRes : (rolesRes as Record<string, any>)?.list || (rolesRes as Record<string, any>)?.data || []);
setPipelines(Array.isArray(pipesRes) ? pipesRes : (pipesRes as Record<string, any>)?.list || (pipesRes as Record<string, any>)?.data || []);
const uList = Array.isArray(usersRes) ? usersRes : (usersRes as Record<string, any>)?.list || (usersRes as Record<string, any>)?.data || [];
setRoles(Array.isArray(rolesRes) ? rolesRes : (rolesRes as JsonData)?.list || (rolesRes as JsonData)?.data || []);
setPipelines(Array.isArray(pipesRes) ? pipesRes : (pipesRes as JsonData)?.list || (pipesRes as JsonData)?.data || []);
const uList = Array.isArray(usersRes) ? usersRes : (usersRes as JsonData)?.list || (usersRes as JsonData)?.data || [];
setUsers(uList);
} catch { /* */ }
finally { setLoading(false); }
@@ -73,7 +74,7 @@ export default function DeployRBAC() {
const loadAssignments = async () => {
if (!selectedUserId) { setAssignments([]); return; }
try {
const res: Record<string, any> = await deployApi.getUserItemRoles(Number(selectedUserId));
const res: JsonData = await deployApi.getUserItemRoles(Number(selectedUserId));
setAssignments(Array.isArray(res) ? res : res?.data || res?.list || []);
} catch { setAssignments([]); }
};
@@ -16,6 +16,7 @@ import {
import { GitBranch, GitCommit, Clock, ChevronDown, Loader2 } from 'lucide-react';
import type { PipelineStage } from './pipeline-helpers';
import { parseStages, getStatusBadge, getStageStatusIcon } from './pipeline-helpers';
import type { JsonData } from '@/types/common';
// 流水线详情弹窗(阶段/配置/历史三 Tab) // Logan.Li
export function PipelineDetailDialog({
@@ -29,8 +30,8 @@ export function PipelineDetailDialog({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
selectedPipeline: any;
pipelineRuns: any[];
selectedPipeline?: JsonData;
pipelineRuns: JsonData[];
loadingRuns: boolean;
activeStage: string | null;
setActiveStage: (id: string | null) => void;
@@ -131,7 +132,7 @@ export function PipelineDetailDialog({
<div className="text-[var(--dg-text-secondary)] text-center py-8 text-sm">暂无执行记录</div>
) : (
<div className="space-y-2">
{pipelineRuns.map(( run: any) => (
{pipelineRuns.map((run: JsonData) => (
<div
key={run.id}
className="flex items-center gap-3 p-3 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]"
@@ -44,6 +44,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { toast } from 'sonner';
import { pipelineApi, deployApi } from '@/api';
import { useConfirm } from '@/components/ui/confirm-dialog';
import type { JsonData } from '@/types/common';
// ---- types ----
interface Task {
@@ -58,7 +59,7 @@ interface LogEntry {
}
const stageTypeLabels: Record<string, string> = { source: '拉取代码', build: '构建', deploy: '部署', test: '测试', custom: '自定义' };
const taskIcons: Record<string, any> = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
const taskIcons: JsonData = { GitBranch, Wrench, Server, FlaskConical, Zap, Terminal, FileCode, Container };
function uid() { return `tsk-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; }
function fmtDuration(s: number | undefined): string {
@@ -119,7 +120,7 @@ function TaskEditor({ task, onUpdate }: { task: Task | null; onUpdate: (t: Task)
}
// ====== Log Dialog ======
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run: any; logs: LogEntry[] }) {
function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () => void; run?: JsonData; logs: LogEntry[] }) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
@@ -160,16 +161,16 @@ function LogDialog({ open, onClose, run, logs }: { open: boolean; onClose: () =>
export default function PipelineEditor() {
const { code } = useParams<{ code: string }>();
const pipelineCode = code || '';
const [pipeline, setPipeline] = useState<any>(null);
const [pipeline, setPipeline] = useState<JsonData>(null);
const [stages, setStages] = useState<Stage[]>([]);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const [expandedStages, setExpandedStages] = useState<Record<string, boolean>>({});
const [activeTab, setActiveTab] = useState('flow');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [runs, setRuns] = useState<any[]>([]);
const [environments, setEnvironments] = useState<any[]>([]);
const [members, setMembers] = useState<any[]>([]);
const [runs, setRuns] = useState<JsonData[]>([]);
const [environments, setEnvironments] = useState<JsonData[]>([]);
const [members, setMembers] = useState<JsonData[]>([]);
const [loadingMembers, setLoadingMembers] = useState(false);
// trigger config
@@ -186,7 +187,7 @@ export default function PipelineEditor() {
// log dialog
const [logOpen, setLogOpen] = useState(false);
const [logRun, setLogRun] = useState<any>(null);
const [logRun, setLogRun] = useState<JsonData>(null);
const [logs, setLogs] = useState<LogEntry[]>([]);
const _confirm = useConfirm();
@@ -194,7 +195,7 @@ export default function PipelineEditor() {
const loadEnvironments = useCallback(async () => {
try {
const res = await deployApi.getEnvironments();
setEnvironments(Array.isArray(res) ? res : (res as Record<string, any>)?.list || []);
setEnvironments(Array.isArray(res) ? res : (res as JsonData)?.list || []);
} catch { /* silent */ }
}, []);
@@ -202,7 +203,7 @@ export default function PipelineEditor() {
setLoadingMembers(true);
try {
const res = await pipelineApi.getMembers(pipelineCode);
setMembers(Array.isArray(res) ? res : (res as Record<string, any>)?.data || []);
setMembers(Array.isArray(res) ? res : (res as JsonData)?.data || []);
} catch { setMembers([]); }
finally { setLoadingMembers(false); }
}, [pipelineCode]);
@@ -210,7 +211,7 @@ export default function PipelineEditor() {
const loadRuns = useCallback(async () => {
try {
const res = await pipelineApi.getRuns(pipelineCode);
setRuns(Array.isArray(res) ? res : (res as Record<string, any>)?.list || []);
setRuns(Array.isArray(res) ? res : (res as JsonData)?.list || []);
} catch { setRuns([]); }
}, [pipelineCode]);
@@ -218,7 +219,7 @@ export default function PipelineEditor() {
(async () => {
setLoading(true);
try {
const p = await pipelineApi.getPipelineDetail(pipelineCode) as Record<string, any>;
const p = await pipelineApi.getPipelineDetail(pipelineCode) as JsonData;
// http.get 已自动解包 json.data,p 即为 pipeline 对象
if (!p || !p.id) { setPipeline(null); setLoading(false); return; }
setPipeline(p);
@@ -294,11 +295,11 @@ export default function PipelineEditor() {
} catch { toast.error('停止失败'); }
};
const handleViewLog = async ( run: any) => {
const handleViewLog = async (run: JsonData) => {
setLogRun(run);
setLogOpen(true);
try {
const data = await pipelineApi.getRunLog(pipelineCode, run.id) as Record<string, any>;
const data = await pipelineApi.getRunLog(pipelineCode, run.id) as JsonData;
setLogs(data?.logs || []);
} catch { setLogs([]); }
};
@@ -482,7 +483,7 @@ export default function PipelineEditor() {
</tr>
</thead>
<tbody>
{runs.map(( r: any) => (
{runs.map((r: JsonData) => (
<tr key={r.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">#{r.build_number || r.id}</td>
<td className="px-3 py-2">
@@ -552,8 +553,8 @@ export default function PipelineEditor() {
{/* Members list — split direct users vs group assignments */}
{(() => {
const directUsers = members.filter(( m: any) => m.username);
const groupAssignments = members.filter(( m: any) => m.group_id);
const directUsers = members.filter((m: JsonData) => m.username);
const groupAssignments = members.filter((m: JsonData) => m.group_id);
return (
<>
{/* Direct users */}
@@ -568,7 +569,7 @@ export default function PipelineEditor() {
<div className="py-4 text-center text-[var(--dg-text-secondary)] text-xs">暂无直接用户</div>
) : (
<div className="divide-y divide-[var(--dg-border)]">
{directUsers.map(( m: any) => (
{directUsers.map((m: JsonData) => (
<div key={m.id} className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center"><Users className="w-4 h-4 text-blue-400" /></div>
@@ -597,7 +598,7 @@ export default function PipelineEditor() {
</div>
) : (
<div className="divide-y divide-[var(--dg-border)]">
{groupAssignments.map(( m: any) => (
{groupAssignments.map((m: JsonData) => (
<div key={m.id} className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center"><Shield className="w-4 h-4 text-purple-400" /></div>
@@ -12,6 +12,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { toast } from 'sonner';
import { pipelineApi } from '@/api';
import { FileBrowserDialog } from './FileBrowserDialog';
import type { JsonData } from '@/types/common';
// ═══════════ 阶段定义 (与 PipelineManager 共用) ═══════════
const STAGE_DEFS = [
@@ -61,7 +62,7 @@ const STATUS_BADGES: Record<string, string> = {
cancelled: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
};
const STATUS_ICONS: Record<string, any> = {
const STATUS_ICONS: JsonData = {
success: CheckCircle2,
failed: XCircle,
running: Loader2,
@@ -101,8 +102,8 @@ function getRunStageStats(run: PipelineRun | null): Map<string, string> {
// 如果 run.stages 有数据,直接用
const statuses = parseStageStatuses(run.stages);
statuses.forEach(s => {
if (s.name || (s as Record<string, any>).type) {
const key = (s as Record<string, any>).type || (STAGE_DEFS.find(d => d.label === s.name)?.key);
if (s.name || (s as JsonData).type) {
const key = (s as JsonData).type || (STAGE_DEFS.find(d => d.label === s.name)?.key);
if (key) map.set(key, s.status);
}
});
@@ -128,7 +129,7 @@ function getStageLogSummary(logs: LogEntry[], stageKey: string): string {
export default function PipelineRunDetail() {
const { code: pipelineCode } = useParams<{ code: string }>();
const [pipeline, setPipeline] = useState<any>(null);
const [pipeline, setPipeline] = useState<JsonData>(null);
const [runs, setRuns] = useState<PipelineRun[]>([]);
const [selectedRun, setSelectedRun] = useState<PipelineRun | null>(null);
const [logs, setLogs] = useState<LogEntry[]>([]);
@@ -147,7 +148,7 @@ export default function PipelineRunDetail() {
// 加载流水线详情
const fetchPipeline = useCallback(async () => {
try {
const data: Record<string, any> = await pipelineApi.getByCode(pipelineCode);
const data: JsonData = await pipelineApi.getByCode(pipelineCode);
setPipeline(data?.data || data);
} catch { /* silent, pipeline info optional */ }
}, [pipelineCode]);
@@ -156,7 +157,7 @@ export default function PipelineRunDetail() {
const fetchRuns = useCallback(async () => {
setLoading(true);
try {
const data: Record<string, any> = await pipelineApi.getRunsByCode(pipelineCode, pageSize);
const data: JsonData = await pipelineApi.getRunsByCode(pipelineCode, pageSize);
const list = data?.list || [];
setRuns(list);
if (!selectedRun && list.length > 0) {
@@ -180,7 +181,7 @@ export default function PipelineRunDetail() {
// 加载日志
const fetchLogs = useCallback(async (run: PipelineRun) => {
try {
const data: Record<string, any> = await pipelineApi.getRunLogStream(run.id);
const data: JsonData = await pipelineApi.getRunLogStream(run.id);
if (data?.logs) {
const parsed = typeof data.logs === 'string' ? JSON.parse(data.logs) : data.logs;
setLogs(Array.isArray(parsed) ? parsed : []);
@@ -11,6 +11,7 @@ import {
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Play } from 'lucide-react';
import type { JsonData } from '@/types/common';
// 执行流水线弹窗(分支/Commit/环境变量) // Logan.Li
export function PipelineRunDialog({
@@ -21,7 +22,7 @@ export function PipelineRunDialog({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
selectedPipeline: any;
selectedPipeline?: JsonData;
onRun: () => void;
}) {
return (
@@ -12,9 +12,10 @@ import { Button } from '@/components/ui/button';
import { AlertCircle, X, Check, Loader2 } from 'lucide-react';
import { releaseApi } from '@/api';
import { toast } from 'sonner';
import type { JsonData } from '@/types/common';
interface ReleaseApproveDialogProps {
release: any | null;
release: JsonData | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
@@ -15,9 +15,10 @@ import { Rocket, CheckCircle, Loader2, RotateCcw } from 'lucide-react';
import { releaseApi } from '@/api';
import { toast } from 'sonner';
import { PIPELINE_STEPS } from './releaseHelpers';
import type { JsonData } from '@/types/common';
interface ReleaseDetailDialogProps {
release: any | null;
release: JsonData | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
@@ -6,6 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { toast } from 'sonner';
import { deployApi } from '@/api';
import type { JsonData } from '@/types/common';
interface ToolConfig {
id: number; name: string; display_name: string; description: string;
@@ -55,8 +56,8 @@ export default function ToolConfigTab() {
const fetchTools = async () => {
setLoading(true);
try {
const res: Record<string, any> = await deployApi.getTools();
setTools((res as Record<string, any>)?.list || []);
const res: JsonData = await deployApi.getTools();
setTools((res as JsonData)?.list || []);
} catch { toast.error('获取工具配置失败'); }
finally { setLoading(false); }
};
@@ -65,8 +66,8 @@ export default function ToolConfigTab() {
const openVersionDialog = async (tool: ToolConfig) => {
try {
const res: Record<string, any> = await deployApi.getToolVersions(tool.id);
const versions = (res as Record<string, any>)?.versions || [];
const res: JsonData = await deployApi.getToolVersions(tool.id);
const versions = (res as JsonData)?.versions || [];
if (versions.length === 0) {
// No versions — install directly
handleInstall(tool);
@@ -99,7 +100,7 @@ export default function ToolConfigTab() {
const handleCheckStatus = async (tool: ToolConfig) => {
try {
const res: Record<string, any> = await deployApi.checkTool(tool.id);
const res: JsonData = await deployApi.checkTool(tool.id);
toast.success(res?.available ? `${tool.display_name} 可用` : `${tool.display_name} 不可用`);
fetchTools();
} catch { toast.error('检测失败'); }
@@ -119,9 +120,9 @@ export default function ToolConfigTab() {
const start = Date.now();
try {
// Simulate a quick reachability test by fetching the version URL
const res: Record<string, any> = await deployApi.getToolVersions(tool.id);
const res: JsonData = await deployApi.getToolVersions(tool.id);
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
const available = (res as Record<string, any>)?.versions?.length > 0;
const available = (res as JsonData)?.versions?.length > 0;
if (available) {
toast.success(`${tool.display_name} 镜像可访问 (${elapsed}s)`);
} else {
@@ -8,6 +8,7 @@ import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { deployApi } from '@/api';
import type { JsonData } from '@/types/common';
interface Group {
id: number; name: string; description: string;
@@ -35,7 +36,7 @@ export default function UserGroupsManager() {
const loadGroups = async () => {
setLoading(true);
try { const res = await deployApi.getGroups(); setGroups((res as Record<string, any>)?.list || []); }
try { const res = await deployApi.getGroups(); setGroups((res as JsonData)?.list || []); }
catch { toast.error('加载失败'); }
finally { setLoading(false); }
};
@@ -12,6 +12,7 @@ import { Maximize2, Minimize2, X, FolderOpen } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useUserStore } from '@/store';
import { pipelineApi } from '@/api';
import type { JsonData } from '@/types/common';
interface WorkspaceTermDialogProps {
open: boolean;
@@ -41,7 +42,7 @@ export function WorkspaceTermDialog({
useEffect(() => {
if (!open || !runId) return;
pipelineApi.getWorkspace(runId)
.then(( data: any) => setWorkspace(data?.workspace_path || ''))
.then((data: JsonData) => setWorkspace(data?.workspace_path || ''))
.catch(() => {});
}, [open, runId]);
@@ -1,5 +1,6 @@
import { Badge } from '@/components/ui/badge';
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
import type { JsonData } from '@/types/common';
// 流水线阶段定义 // Logan.Li
export interface PipelineStage {
@@ -19,7 +20,7 @@ export const pipelineTemplates = [
];
// 解析 stages 字段(兼容 JSON 数组 / 逗号分隔字符串) // Logan.Li
export function parseStages( stages: any): PipelineStage[] {
export function parseStages( stages: string): PipelineStage[] {
if (Array.isArray(stages)) return stages;
if (typeof stages === 'string') {
try {
@@ -42,7 +43,7 @@ export function nameToCode(name: string): string {
}
// 状态徽章(使用后端 last_status 字段) // Logan.Li
export function getStatusBadge( status: any) {
export function getStatusBadge( status: string) {
switch (status) {
case 'success':
return (
@@ -70,7 +70,7 @@ export function PipelineCard({
#{String(pipeline.run_count)}
</Badge>
)}
{getStatusBadge(pipeline.last_status)}
{getStatusBadge(String(pipeline.last_status))}
{environment && (
<Badge
style={{ backgroundColor: `${environment.color}20`, color: environment.color as string }}
@@ -1,5 +1,6 @@
// 发布相关共享工具函数与常量 // Logan.Li
import { Badge } from '@/components/ui/badge';
import type { JsonData } from '@/types/common';
import {
CheckCircle,
XCircle,
@@ -9,7 +10,7 @@ import {
} from 'lucide-react';
// 发布状态徽章(列表 + 详情共用) // Logan.Li
export function getStatusBadge( status: any) {
export function getStatusBadge( status: string) {
switch (status) {
case 'success':
return (
@@ -59,7 +60,7 @@ export function getStatusBadge( status: any) {
}
// 环境徽章 // Logan.Li
export function getEnvBadge( env: any) {
export function getEnvBadge( env: string) {
const colors: Record<string, string> = {
dev: 'border-slate-500/30 text-slate-400 bg-slate-500/10',
test: 'border-blue-500/30 text-blue-400 bg-blue-500/10',
@@ -1,6 +1,7 @@
// 发布管理(审批流)共享工具函数与常量 // Logan.Li
// 注意:本文件专用于 ReleaseManager(含审批流,状态标签与 AppRelease 的 releaseHelpers 不同)
import { Badge } from '@/components/ui/badge';
import type { JsonData } from '@/types/common';
import {
Clock,
Check,
@@ -19,7 +20,7 @@ export const ENVIRONMENTS: Record<string, { label: string; color: string }> = {
};
// 发布状态徽章(审批流:待审批/已审批/已驳回/发布中/发布成功/发布失败/已回滚) // Logan.Li
export function getStatusBadge( status: any) {
export function getStatusBadge( status: string) {
const map: Record<string, { icon: any; label: string; cls: string }> = {
pending: { icon: Clock, label: '待审批', cls: 'border-amber-500/30 text-amber-400 bg-amber-500/10' },
approved: { icon: Check, label: '已审批', cls: 'border-purple-500/30 text-purple-400 bg-purple-500/10' },
@@ -41,7 +42,7 @@ export function getStatusBadge( status: any) {
}
// 环境徽章 // Logan.Li
export function getEnvironmentBadge( env: any) {
export function getEnvironmentBadge( env: string) {
const cfg = ENVIRONMENTS[env];
return (
<Badge variant="outline" className={cfg?.color || 'border-slate-500/30 text-slate-400'}>