feat: M4-08 — release admin page (changelogs, ADR, checklist)
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 9s
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 9s
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
8b6c957a30
commit
adfeeaa657
@ -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() {
|
||||
<Route path="learning-data" element={<Suspense fallback={<PageLoading />}><LearningData /></Suspense>} />
|
||||
<Route path="backup" element={<Suspense fallback={<PageLoading />}><BackupAdmin /></Suspense>} />
|
||||
<Route path="reporting" element={<Suspense fallback={<PageLoading />}><ReportingAdmin /></Suspense>} />
|
||||
<Route path="release" element={<Suspense fallback={<PageLoading />}><ReleaseAdmin /></Suspense>} />
|
||||
<Route path="notification-admin" element={<Suspense fallback={<PageLoading />}><NotificationAdmin /></Suspense>} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
|
||||
@ -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: <RocketOutlined />, requiredRole: 'ADMIN' },
|
||||
{ path: '/imports', name: '文档导入', icon: <ImportOutlined /> },
|
||||
{ path: '/files', name: '文件与 COS', icon: <FileOutlined /> },
|
||||
{ path: '/reporting', name: '报表导出', icon: <FileOutlined />, requiredRole: 'ADMIN' },
|
||||
|
||||
163
src/pages/ReleaseAdmin.tsx
Normal file
163
src/pages/ReleaseAdmin.tsx
Normal file
@ -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<any>(null)
|
||||
const [form] = Form.useForm()
|
||||
const qc = useQueryClient()
|
||||
|
||||
const { data: changelogs, isLoading: cLoading } = useQuery({
|
||||
queryKey: ['release', 'changelogs'],
|
||||
queryFn: () => api.get<any[]>('/admin-api/release/changelogs').then(d => d ?? []),
|
||||
enabled: tab === 'changelogs',
|
||||
})
|
||||
|
||||
const { data: decisions, isLoading: dLoading } = useQuery({
|
||||
queryKey: ['release', 'decisions'],
|
||||
queryFn: () => api.get<any[]>('/admin-api/release/decisions').then(d => d ?? []),
|
||||
enabled: tab === 'decisions',
|
||||
})
|
||||
|
||||
const { data: checklist } = useQuery({
|
||||
queryKey: ['release', 'checklist'],
|
||||
queryFn: () => api.get<any[]>('/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) => <Tag>{p}</Tag> },
|
||||
{ title: '发布时间', dataIndex: 'publishedAt', width: 110, render: (d: string) => d ? new Date(d).toLocaleDateString() : '-' },
|
||||
{
|
||||
title: '操作', width: 100,
|
||||
render: (_: any, r: any) => (
|
||||
<Space>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => openEdit(r)} />
|
||||
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => deleteMutation.mutate(r.id)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const decisionColumns = [
|
||||
{ title: '标题', dataIndex: 'title', width: 200, ellipsis: true },
|
||||
{ title: '上下文', dataIndex: 'context', width: 200, ellipsis: true, render: (c: string) => c?.slice(0, 100) || '-' },
|
||||
{ title: '决策', dataIndex: 'decision', width: 250, ellipsis: true, render: (d: string) => d?.slice(0, 150) || '-' },
|
||||
{ title: '状态', dataIndex: 'status', width: 80, render: (s: string) => <Tag color={s === 'accepted' ? 'green' : s === 'proposed' ? 'blue' : 'default'}>{s}</Tag> },
|
||||
{
|
||||
title: '操作', width: 100,
|
||||
render: (_: any, r: any) => (
|
||||
<Space>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => openEdit(r)} />
|
||||
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => deleteMutation.mutate(r.id)} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4}><RocketOutlined /> 发布与决策</Title>
|
||||
|
||||
<Tabs activeKey={tab} onChange={setTab} items={[
|
||||
{
|
||||
key: 'changelogs', label: '版本日志',
|
||||
children: (
|
||||
<div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate} style={{ marginBottom: 16 }}>新建版本日志</Button>
|
||||
<Table dataSource={changelogs || []} columns={changelogColumns} rowKey="id" loading={cLoading} size="small" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'decisions', label: '架构决策 (ADR)',
|
||||
children: (
|
||||
<div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate} style={{ marginBottom: 16 }}>新建决策记录</Button>
|
||||
<Table dataSource={decisions || []} columns={decisionColumns} rowKey="id" loading={dLoading} size="small" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'checklist', label: '回归清单',
|
||||
children: (
|
||||
<Card size="small">
|
||||
{(checklist || []).map((item: any) => (
|
||||
<div key={item.id} style={{ padding: '6px 0', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Checkbox checked={item.checked} onChange={e => toggleCheck.mutate({ id: item.id, checked: e.target.checked })}>
|
||||
<span style={item.checked ? { textDecoration: 'line-through', color: '#999' } : {}}>{item.item}</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
]} />
|
||||
|
||||
<Modal
|
||||
title={editing ? '编辑' : '新建'}
|
||||
open={modalOpen}
|
||||
onCancel={() => { setModalOpen(false); setEditing(null) }}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={saveMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={saveMutation.mutate}>
|
||||
{tab === 'changelogs' ? (
|
||||
<>
|
||||
<Form.Item name="version" label="版本" rules={[{ required: true }]}><Input placeholder="1.0.0" /></Form.Item>
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true }]}><Input placeholder="版本标题" /></Form.Item>
|
||||
<Form.Item name="content" label="内容"><TextArea rows={4} placeholder="更新内容" /></Form.Item>
|
||||
<Form.Item name="platform" label="平台" initialValue="ios"><Input /></Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true }]}><Input placeholder="决策标题" /></Form.Item>
|
||||
<Form.Item name="context" label="上下文"><TextArea rows={2} placeholder="背景和上下文" /></Form.Item>
|
||||
<Form.Item name="decision" label="决策内容"><TextArea rows={3} placeholder="决策内容" /></Form.Item>
|
||||
<Form.Item name="rationale" label="理由"><TextArea rows={2} placeholder="决策理由" /></Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user