feat: M4-06 — project center page with repos/issues/milestones/releases/runners
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 6s

- Replace GiteaEmbed iframe with full ProjectCenter page
- Tabs: repos, issues, milestones, releases, runners, Gitea embed
- Rename menu 代码仓库 → 项目中心

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 18:06:38 +08:00
parent 8795b4d045
commit 471669c6cf
3 changed files with 136 additions and 9 deletions

View File

@ -12,7 +12,7 @@ import AdminLayout from './layouts/AdminLayout'
const Login = lazy(() => import('./pages/Login')) const Login = lazy(() => import('./pages/Login'))
const KnowledgeBasesPage = lazy(() => import('./pages/KnowledgeBases')) const KnowledgeBasesPage = lazy(() => import('./pages/KnowledgeBases'))
const BillingPage = lazy(() => import('./pages/Billing')) const BillingPage = lazy(() => import('./pages/Billing'))
const GiteaEmbed = lazy(() => import('./pages/GiteaEmbed')) const ProjectCenter = lazy(() => import('./pages/GiteaEmbed'))
const ConfigPage = lazy(() => import("./pages/Config")) const ConfigPage = lazy(() => import("./pages/Config"))
const SecurityEventsPage = lazy(() => import('./pages/SecurityEvents')) const SecurityEventsPage = lazy(() => import('./pages/SecurityEvents'))
const ThrottlePage = lazy(() => import('./pages/Throttle')) const ThrottlePage = lazy(() => import('./pages/Throttle'))
@ -131,7 +131,7 @@ function App() {
<Route <Route
path="git" path="git"
element={ element={
<Suspense fallback={<PageLoading />}><GiteaEmbed /></Suspense> <Suspense fallback={<PageLoading />}><ProjectCenter /></Suspense>
} }
/> />
<Route <Route

View File

@ -30,7 +30,7 @@ export const adminMenuItems: AdminMenuItem[] = [
{ path: '/reporting', name: '报表导出', icon: <FileOutlined />, requiredRole: 'ADMIN' }, { path: '/reporting', name: '报表导出', icon: <FileOutlined />, requiredRole: 'ADMIN' },
{ path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' }, { path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
{ path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' }, { path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' },
{ path: '/git', name: '代码仓库', icon: <CodeOutlined /> }, { path: '/git', name: '项目中心', icon: <CodeOutlined /> },
{ path: '/ops', name: '系统运维', icon: <CloudServerOutlined />, requiredRole: 'SUPER_ADMIN', children: [ { path: '/ops', name: '系统运维', icon: <CloudServerOutlined />, requiredRole: 'SUPER_ADMIN', children: [
{ path: '/throttle', name: '限流管理' }, { path: '/throttle', name: '限流管理' },
{ path: '/security-events', name: '安全事件' }, { path: '/security-events', name: '安全事件' },

View File

@ -1,10 +1,137 @@
export default function GiteaEmbed() { import { useState } from 'react'
import { Table, Tag, Tabs, Typography, Card, Row, Col, Spin } from 'antd'
import { CodeOutlined, BranchesOutlined, RocketOutlined } from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query'
import { api } from '@/services/http-client'
const { Title, Text } = Typography
export default function ProjectCenter() {
const [selectedRepo, setSelectedRepo] = useState<string | null>(null)
const [tab, setTab] = useState('repos')
const { data: repos, isLoading } = useQuery({
queryKey: ['gitea', 'repos'],
queryFn: () => api.get<any[]>('/admin-api/projects/repos').then(d => d ?? []),
})
const repoParts = selectedRepo?.split('/') || []
const { data: issues } = useQuery({
queryKey: ['gitea', 'issues', selectedRepo],
queryFn: () => api.get<any[]>(`/admin-api/projects/repos/${repoParts[0]}/${repoParts[1]}/issues?state=all`).then(d => d ?? []),
enabled: tab === 'issues' && repoParts.length === 2,
})
const { data: milestones } = useQuery({
queryKey: ['gitea', 'milestones', selectedRepo],
queryFn: () => api.get<any[]>(`/admin-api/projects/repos/${repoParts[0]}/${repoParts[1]}/milestones`).then(d => d ?? []),
enabled: tab === 'milestones' && repoParts.length === 2,
})
const { data: releases } = useQuery({
queryKey: ['gitea', 'releases', selectedRepo],
queryFn: () => api.get<any[]>(`/admin-api/projects/repos/${repoParts[0]}/${repoParts[1]}/releases`).then(d => d ?? []),
enabled: tab === 'releases' && repoParts.length === 2,
})
const { data: runners } = useQuery({
queryKey: ['gitea', 'runners'],
queryFn: () => api.get<any[]>('/admin-api/projects/runners').then(d => d ?? []),
enabled: tab === 'runners',
})
const repoColumns = [
{ title: '仓库', dataIndex: 'fullName', width: 200, render: (n: string, r: any) => <a onClick={() => { setSelectedRepo(n); setTab('issues') }}>{n}</a> },
{ title: '描述', dataIndex: 'description', width: 200, ellipsis: true, render: (d: string) => d || '-' },
{ title: 'Issues', dataIndex: 'openIssues', width: 70 },
{ title: 'PRs', dataIndex: 'openPulls', width: 70 },
{ title: '里程碑', dataIndex: 'milestones', width: 70 },
{ title: 'Stars', dataIndex: 'stars', width: 60 },
{ title: '分支', dataIndex: 'defaultBranch', width: 80, render: (b: string) => <Tag>{b}</Tag> },
{ title: '更新', dataIndex: 'updatedAt', width: 100, render: (d: string) => d ? new Date(d).toLocaleDateString() : '-' },
]
const issueColumns = [
{ title: '#', dataIndex: 'number', width: 60 },
{ title: '标题', dataIndex: 'title', width: 300, ellipsis: true },
{ title: '状态', dataIndex: 'state', width: 70, render: (s: string) => <Tag color={s === 'open' ? 'green' : 'default'}>{s}</Tag> },
{ title: '创建者', dataIndex: ['user', 'login'], width: 100 },
{ title: '更新', dataIndex: 'updated_at', width: 100, render: (d: string) => d ? new Date(d).toLocaleDateString() : '-' },
]
return ( return (
<div style={{ width: '100%', height: 'calc(100vh - 112px)', overflow: 'hidden' }}> <div>
<iframe <Row justify="space-between" align="middle" style={{ marginBottom: 16 }}>
src="https://git.admin.longde.cloud" <Col><Title level={4} style={{ margin: 0 }}><CodeOutlined /> </Title></Col>
style={{ width: '100%', height: '100%', border: 'none' }} {selectedRepo && <Col><Text strong>{selectedRepo}</Text></Col>}
title="Gitea" </Row>
<Tabs activeKey={tab} onChange={setTab} items={[
{
key: 'repos', label: '仓库列表',
children: <Table dataSource={repos || []} columns={repoColumns} rowKey="id" loading={isLoading} size="small" pagination={false} scroll={{ x: 900 }} />,
},
{
key: 'issues', label: `Issues${selectedRepo ? ` · ${selectedRepo}` : ''}`,
children: selectedRepo ? (
<Table dataSource={issues || []} columns={issueColumns} rowKey="id" size="small" pagination={{ pageSize: 30 }} scroll={{ x: 700 }} />
) : <Text type="secondary"></Text>,
},
{
key: 'milestones', label: '里程碑',
children: selectedRepo ? (
<Row gutter={[16, 16]}>
{(milestones || []).map((m: any) => (
<Col xs={24} sm={12} lg={8} key={m.id}>
<Card size="small" title={m.title}>
<Text type="secondary">{m.description || '无描述'}</Text>
<br />
<Tag color={m.state === 'open' ? 'blue' : 'default'}>{m.state}</Tag>
{m.due_on && <Tag> {new Date(m.due_on).toLocaleDateString()}</Tag>}
</Card>
</Col>
))}
</Row>
) : <Text type="secondary"></Text>,
},
{
key: 'releases', label: 'Release',
children: selectedRepo ? (
<Table
dataSource={releases || []} rowKey="id" size="small" pagination={false}
columns={[
{ title: '标签', dataIndex: 'tag_name', width: 120, render: (t: string) => <Tag color="blue"><RocketOutlined /> {t}</Tag> },
{ title: '标题', dataIndex: 'name', width: 250, ellipsis: true },
{ title: '发布者', dataIndex: ['author', 'login'], width: 100 },
{ title: '发布时间', dataIndex: 'published_at', width: 120, render: (d: string) => d ? new Date(d).toLocaleDateString() : '-' },
]}
/>
) : <Text type="secondary"></Text>,
},
{
key: 'runners', label: 'Runner 状态',
children: runners ? (
<Table
dataSource={runners} rowKey="id" size="small" pagination={false}
columns={[
{ title: '名称', dataIndex: 'name', width: 150 },
{ title: '状态', dataIndex: 'is_online', width: 80, render: (o: boolean) => <Tag color={o ? 'green' : 'red'}>{o ? '在线' : '离线'}</Tag> },
{ title: '标签', dataIndex: 'tags', width: 150, render: (t: string[]) => (t || []).join(', ') || '-' },
{ title: '最后活跃', dataIndex: 'last_active', width: 120, render: (d: string) => d ? new Date(d).toLocaleString() : '-' },
]}
/>
) : <Spin />,
},
tab === 'gitea' ? undefined : {
key: 'gitea', label: 'Gitea 面板',
children: (
<div style={{ width: '100%', height: 'calc(100vh - 200px)' }}>
<iframe src="https://git.admin.longde.cloud" style={{ width: '100%', height: '100%', border: 'none' }} title="Gitea" />
</div>
),
},
].filter(Boolean) as any}
/> />
</div> </div>
) )