From adfeeaa657cd2428518899242e9c97209360a46a Mon Sep 17 00:00:00 2001 From: WangDL Date: Sun, 24 May 2026 18:15:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M4-08=20=E2=80=94=20release=20admin=20p?= =?UTF-8?q?age=20(changelogs,=20ADR,=20checklist)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ReleaseAdmin page with 3 tabs: version logs, architecture decisions, release checklist - CRUD modals for changelogs and decisions - Checklist with check/uncheck toggle Co-Authored-By: Claude Opus 4.7 --- src/App.tsx | 2 + src/config/menu.tsx | 3 +- src/pages/ReleaseAdmin.tsx | 163 +++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/pages/ReleaseAdmin.tsx diff --git a/src/App.tsx b/src/App.tsx index ef6bd3d..fa33ef5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,7 @@ const ImportMonitorPage = lazy(() => import('./pages/ImportMonitor')) const KnowledgeOpsPage = lazy(() => import('./pages/KnowledgeOps')) const ChatLogsPage = lazy(() => import('./pages/ChatLogs')) const HermesSettings = lazy(() => import('./pages/HermesSettings')) +const ReleaseAdmin = lazy(() => import('./pages/ReleaseAdmin')) const TaskAssistant = lazy(() => import('./pages/TaskAssistant')) const Placeholder = lazy(() => import('./pages/Placeholder')) const ForbiddenPage = lazy(() => import('./pages/403')) @@ -182,6 +183,7 @@ function App() { }>} /> }>} /> }>} /> + }>} /> }>} /> } /> diff --git a/src/config/menu.tsx b/src/config/menu.tsx index e4605df..58a55d0 100644 --- a/src/config/menu.tsx +++ b/src/config/menu.tsx @@ -1,5 +1,5 @@ import type React from 'react' -import { DashboardOutlined, RobotOutlined, UserOutlined, DollarOutlined, BookOutlined, ImportOutlined, FileOutlined, SafetyOutlined, CodeOutlined, CloudServerOutlined, SettingOutlined } from '@ant-design/icons' +import { DashboardOutlined, RobotOutlined, UserOutlined, DollarOutlined, BookOutlined, ImportOutlined, FileOutlined, SafetyOutlined, CodeOutlined, CloudServerOutlined, SettingOutlined, RocketOutlined } from '@ant-design/icons' import type { AdminRole } from '@/types/admin' import { hasRole } from '@/constants/roles' @@ -25,6 +25,7 @@ export const adminMenuItems: AdminMenuItem[] = [ { path: '/knowledge/sources', name: '知识源列表' }, { path: '/knowledge/ops', name: '知识运维' }, ]}, + { path: '/release', name: '发布决策', icon: , requiredRole: 'ADMIN' }, { path: '/imports', name: '文档导入', icon: }, { path: '/files', name: '文件与 COS', icon: }, { path: '/reporting', name: '报表导出', icon: , requiredRole: 'ADMIN' }, diff --git a/src/pages/ReleaseAdmin.tsx b/src/pages/ReleaseAdmin.tsx new file mode 100644 index 0000000..4b4fa71 --- /dev/null +++ b/src/pages/ReleaseAdmin.tsx @@ -0,0 +1,163 @@ +import { useState } from 'react' +import { Table, Button, Modal, Form, Input, Checkbox, Tag, Space, Typography, Tabs, Card } from 'antd' +import { PlusOutlined, EditOutlined, DeleteOutlined, RocketOutlined } from '@ant-design/icons' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { api } from '@/services/http-client' +import { message } from 'antd' + +const { Title } = Typography +const { TextArea } = Input + +export default function ReleaseAdmin() { + const [tab, setTab] = useState('changelogs') + const [modalOpen, setModalOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [form] = Form.useForm() + const qc = useQueryClient() + + const { data: changelogs, isLoading: cLoading } = useQuery({ + queryKey: ['release', 'changelogs'], + queryFn: () => api.get('/admin-api/release/changelogs').then(d => d ?? []), + enabled: tab === 'changelogs', + }) + + const { data: decisions, isLoading: dLoading } = useQuery({ + queryKey: ['release', 'decisions'], + queryFn: () => api.get('/admin-api/release/decisions').then(d => d ?? []), + enabled: tab === 'decisions', + }) + + const { data: checklist } = useQuery({ + queryKey: ['release', 'checklist'], + queryFn: () => api.get('/admin-api/release/checklists/v1.0').then(d => d ?? []), + enabled: tab === 'checklist', + }) + + const saveMutation = useMutation({ + mutationFn: (values: any) => { + const endpoint = tab === 'changelogs' ? 'changelogs' : 'decisions' + return editing + ? api.patch(`/admin-api/release/${endpoint}/${editing.id}`, values) + : api.post(`/admin-api/release/${endpoint}`, values) + }, + onSuccess: () => { + message.success(editing ? '已更新' : '已创建') + qc.invalidateQueries({ queryKey: ['release'] }) + setModalOpen(false); setEditing(null); form.resetFields() + }, + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => api.delete(`/admin-api/release/${tab === 'changelogs' ? 'changelogs' : 'decisions'}/${id}`), + onSuccess: () => { message.success('已删除'); qc.invalidateQueries({ queryKey: ['release'] }) }, + }) + + const toggleCheck = useMutation({ + mutationFn: ({ id, checked }: { id: string; checked: boolean }) => + api.patch(`/admin-api/release/checklists/${id}`, { checked }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['release', 'checklist'] }), + }) + + const openCreate = () => { setEditing(null); form.resetFields(); setModalOpen(true) } + const openEdit = (r: any) => { setEditing(r); form.setFieldsValue(r); setModalOpen(true) } + + const changelogColumns = [ + { title: '版本', dataIndex: 'version', width: 80 }, + { title: '标题', dataIndex: 'title', width: 200, ellipsis: true }, + { title: '内容', dataIndex: 'content', width: 300, ellipsis: true, render: (c: string) => c?.slice(0, 120) }, + { title: '平台', dataIndex: 'platform', width: 80, render: (p: string) => {p} }, + { title: '发布时间', dataIndex: 'publishedAt', width: 110, render: (d: string) => d ? new Date(d).toLocaleDateString() : '-' }, + { + title: '操作', width: 100, + render: (_: any, r: any) => ( + + + + + ), + }, + { + key: 'decisions', label: '架构决策 (ADR)', + children: ( +
+ +
+ + ), + }, + { + key: 'checklist', label: '回归清单', + children: ( + + {(checklist || []).map((item: any) => ( +
+ toggleCheck.mutate({ id: item.id, checked: e.target.checked })}> + {item.item} + +
+ ))} +
+ ), + }, + ]} /> + + { setModalOpen(false); setEditing(null) }} + onOk={() => form.submit()} + confirmLoading={saveMutation.isPending} + > +
+ {tab === 'changelogs' ? ( + <> + + +