From a5970315be8974a2e4b96754162cadf66c815fa8 Mon Sep 17 00:00:00 2001 From: attacker Date: Mon, 15 Jun 2026 04:06:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=85=A8=E6=A0=88=E5=AE=8C=E5=96=84=E2=80=94=E2=80=94=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=80=BC=E4=BF=AE=E5=A4=8D+=E9=A9=B3=E5=9B=9EAPI+toas?= =?UTF-8?q?t=E5=8F=8D=E9=A6=88+=E8=AF=A6=E6=83=85=E5=BC=B9=E7=AA=97+?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - pipeline.go: 新增 RejectRelease handler(仅pending可驳回,状态置rejected) - routes.go: 注册 POST /releases/:id/reject 路由 前端: - ReleaseManager.tsx 重写(553→581行): - 修复环境值不匹配: development/production → dev/prod(与后端binding一致) - 移除硬编码mock applications数组,改自由输入应用名 - 修复字段名: release.deployer → release.author - 新增toast反馈(create/approve/reject/deploy/rollback均有success/error) - 新增确认框(deploy/rollback操作前确认) - 新增发布详情弹窗(原缺失,Eye按钮无响应) - 新增空状态展示 - 新增rejected状态徽章 - 主题感知: bg-slate-800 → var(--dg-bg-secondary) - Select替换原生select下拉框 - api.ts: 新增 rejectRelease 方法 // Logan.Li --- backend/internal/api/handlers/pipeline.go | 37 + backend/internal/api/routes/routes.go | 1 + .../src/components/deploy/ReleaseManager.tsx | 712 +++++++++--------- frontend/src/services/api.ts | 3 + 4 files changed, 411 insertions(+), 342 deletions(-) diff --git a/backend/internal/api/handlers/pipeline.go b/backend/internal/api/handlers/pipeline.go index 77ab7a1..f9c0829 100644 --- a/backend/internal/api/handlers/pipeline.go +++ b/backend/internal/api/handlers/pipeline.go @@ -463,6 +463,43 @@ func (h *PipelineHandler) DeployRelease(c *gin.Context) { }) } +// RejectRelease 驳回发布申请 // Logan.Li +func (h *PipelineHandler) RejectRelease(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + + var release models.AppRelease + if err := h.db.First(&release, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{ + Code: 404, + Message: "发布不存在", + }) + return + } + + if release.Status != "pending" { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "仅待审批状态可驳回", + }) + return + } + + release.Status = "rejected" + if err := h.db.Save(&release).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{ + Code: 500, + Message: "驳回失败", + }) + return + } + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "已驳回", + Data: release, + }) +} + // RollbackRelease 回滚发布 // @Summary 回滚发布 // @Description 回滚应用发布 diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index ef4aaa1..655464b 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -236,6 +236,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { releases.GET("", pipelineHandler.ListReleases) releases.POST("", pipelineHandler.CreateRelease) releases.POST("/:id/approve", pipelineHandler.ApproveRelease) + releases.POST("/:id/reject", pipelineHandler.RejectRelease) // 驳回发布 // Logan.Li releases.POST("/:id/deploy", pipelineHandler.DeployRelease) releases.POST("/:id/rollback", pipelineHandler.RollbackRelease) } diff --git a/frontend/src/components/deploy/ReleaseManager.tsx b/frontend/src/components/deploy/ReleaseManager.tsx index 5e83432..9e267a0 100644 --- a/frontend/src/components/deploy/ReleaseManager.tsx +++ b/frontend/src/components/deploy/ReleaseManager.tsx @@ -1,11 +1,8 @@ -import { useState, useEffect } from 'react'; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { useState, useEffect, useCallback } from 'react'; +import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Progress } from '@/components/ui/progress'; -import { ScrollArea } from '@/components/ui/scroll-area'; import { Dialog, DialogContent, @@ -23,14 +20,7 @@ import { } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { ScrollArea } from '@/components/ui/scroll-area'; import { Rocket, Plus, @@ -39,32 +29,30 @@ import { XCircle, Clock, RotateCcw, - GitBranch, GitCommit, User, Eye, - MoreHorizontal, - FileText, AlertCircle, - History, - ChevronRight, Loader2, Play, - Pause, Check, X, + PackageOpen, } from 'lucide-react'; import { releaseApi } from '@/services/api'; +import { useConfirm } from '@/components/ui/confirm-dialog'; +import { toast } from 'sonner'; -const applications = [ - { id: 'app-1', name: '用户中心服务', currentVersion: 'v2.3.0' }, - { id: 'app-2', name: '订单服务', currentVersion: 'v1.4.5' }, - { id: 'app-3', name: '支付网关', currentVersion: 'v3.0.1' }, - { id: 'app-4', name: '消息推送服务', currentVersion: 'v1.1.9' }, - { id: 'app-5', name: '数据分析平台', currentVersion: 'v1.9.8' }, -]; +// 环境配置(值与后端 oneof=dev test staging prod 一致) // Logan.Li +const ENVIRONMENTS: Record = { + dev: { label: '开发环境', color: 'border-blue-500/30 text-blue-400 bg-blue-500/10' }, + test: { label: '测试环境', color: 'border-cyan-500/30 text-cyan-400 bg-cyan-500/10' }, + staging: { label: '预发环境', color: 'border-amber-500/30 text-amber-400 bg-amber-500/10' }, + prod: { label: '生产环境', color: 'border-red-500/30 text-red-400 bg-red-500/10' }, +}; export function ReleaseManager() { + const _confirm = useConfirm(); const [searchQuery, setSearchQuery] = useState(''); const [envFilter, setEnvFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); @@ -74,30 +62,31 @@ export function ReleaseManager() { const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false); const [isApproveDialogOpen, setIsApproveDialogOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); const [newRelease, setNewRelease] = useState({ - appId: '', + appName: '', version: '', - environment: 'staging', - branch: '', + environment: 'dev', commitId: '', + commitMsg: '', description: '', }); - const loadData = async () => { + const loadData = useCallback(async () => { try { setLoading(true); const data = await releaseApi.getReleases(); setReleases(data.list || []); - } catch (error) { - console.error('Failed to load releases:', error); + } catch { + toast.error('获取发布列表失败'); } finally { setLoading(false); } - }; + }, []); useEffect(() => { loadData(); - }, []); + }, [loadData]); const filteredReleases = releases.filter((release) => { const matchesSearch = @@ -109,120 +98,109 @@ export function ReleaseManager() { }); const getStatusBadge = (status: any) => { - switch (status) { - case 'success': - return ( - - - 发布成功 - - ); - case 'failed': - return ( - - - 发布失败 - - ); - case 'deploying': - return ( - - - 发布中 - - ); - case 'pending': - return ( - - - 待审批 - - ); - case 'approved': - return ( - - - 已审批 - - ); - case 'rolled_back': - return ( - - - 已回滚 - - ); - default: - return null; - } + const map: Record = { + 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' }, + rejected: { icon: XCircle, label: '已驳回', cls: 'border-slate-500/30 text-slate-400 bg-slate-500/10' }, + deploying: { icon: Loader2, label: '发布中', cls: 'border-blue-500/30 text-blue-400 bg-blue-500/10 animate-pulse' }, + success: { icon: CheckCircle, label: '发布成功', cls: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10' }, + failed: { icon: XCircle, label: '发布失败', cls: 'border-red-500/30 text-red-400 bg-red-500/10' }, + rolled_back: { icon: RotateCcw, label: '已回滚', cls: 'border-slate-500/30 text-slate-400 bg-slate-500/10' }, + }; + const cfg = map[status]; + if (!cfg) return null; + const Icon = cfg.icon; + return ( + + + {cfg.label} + + ); }; const getEnvironmentBadge = (env: any) => { - const colors: Record = { - production: 'border-red-500/30 text-red-400 bg-red-500/10', - staging: 'border-amber-500/30 text-amber-400 bg-amber-500/10', - development: 'border-blue-500/30 text-blue-400 bg-blue-500/10', - }; - const labels: Record = { - production: '生产环境', - staging: '预发环境', - development: '开发环境', - }; + const cfg = ENVIRONMENTS[env]; return ( - - {labels[env] || env} + + {cfg?.label || env} ); }; const handleCreateRelease = async () => { + if (!newRelease.appName.trim() || !newRelease.version.trim()) { + toast.error('请填写应用名称和版本号'); + return; + } + setSubmitting(true); try { await releaseApi.createRelease({ - app_name: applications.find((a) => a.id === newRelease.appId)?.name || newRelease.appId, - version: newRelease.version, + app_name: newRelease.appName.trim(), + version: newRelease.version.trim(), environment: newRelease.environment, - branch: newRelease.branch, - commit_id: newRelease.commitId, - description: newRelease.description, + commit_id: newRelease.commitId.trim(), + commit_msg: newRelease.commitMsg.trim(), + description: newRelease.description.trim(), }); + toast.success('发布申请已提交'); setIsCreateDialogOpen(false); - setNewRelease({ appId: '', version: '', environment: 'staging', branch: '', commitId: '', description: '' }); + setNewRelease({ appName: '', version: '', environment: 'dev', commitId: '', commitMsg: '', description: '' }); loadData(); - } catch (error) { - console.error('Failed to create release:', error); + } catch { + toast.error('创建失败'); + } finally { + setSubmitting(false); } }; const handleApprove = async (approved: boolean) => { if (!selectedRelease) return; + setSubmitting(true); try { if (approved) { await releaseApi.approveRelease(selectedRelease.id); + toast.success('审批通过'); } else { - await releaseApi.rollbackRelease(selectedRelease.id); + await releaseApi.rejectRelease(selectedRelease.id); + toast.success('已驳回'); } setIsApproveDialogOpen(false); loadData(); - } catch (error) { - console.error('Failed to process approval:', error); + } catch { + toast.error(approved ? '审批失败' : '驳回失败'); + } finally { + setSubmitting(false); } }; - const handleDeploy = async (id: any) => { + const handleDeploy = async (id: any, appName: string, version: string) => { + const ok = await _confirm({ + title: '确认部署', + description: `确定部署 ${appName} ${version} 到目标环境?`, + }); + if (!ok) return; try { await releaseApi.deployRelease(id); + toast.success('部署已启动'); loadData(); - } catch (error) { - console.error('Failed to deploy release:', error); + } catch { + toast.error('部署失败'); } }; - const handleRollback = async (id: any) => { + const handleRollback = async (id: any, appName: string, version: string) => { + const ok = await _confirm({ + title: '确认回滚', + description: `确定回滚 ${appName} ${version}?此操作将恢复到上一版本。`, + variant: 'destructive', + }); + if (!ok) return; try { await releaseApi.rollbackRelease(id); + toast.success('回滚成功'); loadData(); - } catch (error) { - console.error('Failed to rollback release:', error); + } catch { + toast.error('回滚失败'); } }; @@ -231,10 +209,10 @@ export function ReleaseManager() { {/* Header */}
-

应用发布

-

管理应用版本发布与审批流程

+

发布管理

+

管理应用版本发布与审批流程

- @@ -242,307 +220,357 @@ export function ReleaseManager() { {/* Stats */}
- - -

待审批

-

- {releases.filter((r) => r.status === 'pending').length} -

-
-
- - -

发布中

-

- {releases.filter((r) => r.status === 'deploying').length} -

-
-
- - -

今日成功

-

- {releases.filter((r) => r.status === 'success').length} -

-
-
- - -

今日失败

-

- {releases.filter((r) => r.status === 'failed').length} -

-
-
- - -

已回滚

-

- {releases.filter((r) => r.status === 'rolled_back').length} -

-
-
-
- - {/* Filters */} -
-
- - setSearchQuery(e.target.value)} - className="pl-10 bg-slate-800 border-slate-700 text-slate-100" - /> -
- - -
- - {loading ? ( -
- -
- ) : null} - - {/* Release List */} -
- {filteredReleases.map((release) => ( - + {[ + { label: '待审批', val: releases.filter((r) => r.status === 'pending').length, color: 'text-amber-400' }, + { label: '发布中', val: releases.filter((r) => r.status === 'deploying').length, color: 'text-blue-400' }, + { label: '发布成功', val: releases.filter((r) => r.status === 'success').length, color: 'text-emerald-400' }, + { label: '发布失败', val: releases.filter((r) => r.status === 'failed').length, color: 'text-red-400' }, + { label: '已回滚', val: releases.filter((r) => r.status === 'rolled_back').length, color: 'text-slate-400' }, + ].map((s) => ( + -
-
-
-

{release.app_name}

- {getStatusBadge(release.status)} - {getEnvironmentBadge(release.environment)} -
-
- - - {release.version} - - - - {release.branch} - - - - {release.commit_id} - - - - 发布人: {release.deployer} - -
-
- 创建: {release.created_at ? new Date(release.created_at).toLocaleString() : '--'} - {release.updated_at && 部署: {new Date(release.updated_at).toLocaleString()}} - {release.rollback_to && ( - 回滚至: {release.rollback_to} - )} -
-
- -
- {release.status === 'pending' && ( - - )} - {release.status === 'approved' && ( - - )} - {(release.status === 'success' || release.status === 'failed') && ( - - )} - -
-
+

{s.label}

+

{s.val}

))}
+ {/* Filters */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ + +
+ + {loading ? ( +
+ +
+ ) : filteredReleases.length === 0 ? ( +
+ +

暂无发布记录

+
+ ) : ( +
+ {filteredReleases.map((release) => ( + + +
+
+
+

{release.app_name}

+ {getStatusBadge(release.status)} + {getEnvironmentBadge(release.environment)} +
+
+ + + {release.version} + + {release.commit_id && ( + + + {release.commit_id} + + )} + {release.author && ( + + + {release.author} + + )} +
+ {release.commit_msg && ( +

{release.commit_msg}

+ )} +
+ 创建: {release.created_at ? new Date(release.created_at).toLocaleString() : '--'} + {release.deployed_at && 部署: {new Date(release.deployed_at).toLocaleString()}} +
+
+ +
+ {release.status === 'pending' && ( + + )} + {release.status === 'approved' && ( + + )} + {(release.status === 'success' || release.status === 'failed') && ( + + )} + +
+
+
+
+ ))} +
+ )} + {/* Create Release Dialog */} - + 新建发布 - 创建新的应用版本发布申请 + 创建新的应用版本发布申请
- - + + setNewRelease({ ...newRelease, appName: e.target.value })} + placeholder="如:用户中心服务" + className="mt-1" + />
- + setNewRelease({ ...newRelease, version: e.target.value })} placeholder="v1.0.0" - className="bg-slate-800 border-slate-700 text-slate-100 mt-1" + className="mt-1" />
- +
- - setNewRelease({ ...newRelease, branch: e.target.value })} - placeholder="main" - className="bg-slate-800 border-slate-700 text-slate-100 mt-1" - /> -
-
- + setNewRelease({ ...newRelease, commitId: e.target.value })} placeholder="abc1234" - className="bg-slate-800 border-slate-700 text-slate-100 mt-1" + className="mt-1" + /> +
+
+ + setNewRelease({ ...newRelease, commitMsg: e.target.value })} + placeholder="feat: 新增功能" + className="mt-1" />
- +