feat(dimzou): 文档/发布 monitoring pages

New Dimzou 内容 pages against the Dimzou admin API:
- /dimzou/documents — state filter (活跃/已归档/回收站), detail sheet
  with versions, translations and pending review counts
- /dimzou/publications — visibility filter, detail from row data

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 06:42:09 +08:00
parent ddf07bbf58
commit 6ed8de4de6
3 changed files with 322 additions and 0 deletions

View File

@ -1,6 +1,8 @@
import { api } from '@/api/client'
import type {
ApiResponse,
DimzouDocument,
DimzouPublication,
DimzouTranslation,
ListParams,
PaginatedResponse,
@ -9,6 +11,18 @@ import type {
const BASE = '/api/admin/v1/dimzou'
export function fetchDimzouDocuments(params: ListParams) {
return api.get<PaginatedResponse<DimzouDocument>>(`${BASE}/documents`, params)
}
export function fetchDimzouDocument(id: number) {
return api.get<ApiResponse<DimzouDocument>>(`${BASE}/documents/${id}`)
}
export function fetchDimzouPublications(params: ListParams) {
return api.get<PaginatedResponse<DimzouPublication>>(`${BASE}/publications`, params)
}
export function fetchDimzouTranslations(params: ListParams) {
return api.get<PaginatedResponse<DimzouTranslation>>(`${BASE}/translations`, params)
}

View File

@ -0,0 +1,171 @@
import { createFileRoute } from '@tanstack/react-router'
import { PERM, requirePermission } from '@/auth/permissions'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import type { DimzouDocument, DimzouDocumentState } from '@/api/types'
import { fetchDimzouDocument, fetchDimzouDocuments } from '@/api/modules/dimzou'
import { useListPage } from '@/lib/list-page'
import { DataTable } from '@/components/data-table/data-table'
import { PageHeader } from '@/components/page-header'
import { DetailSheet } from '@/features/studio/detail-sheet'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export const Route = createFileRoute('/_authed/dimzou/documents')({
beforeLoad: () => requirePermission(PERM.DIMZOU_READ),
component: DimzouDocumentsPage,
})
const STATE_LABELS: Record<DimzouDocumentState, string> = {
active: '活跃',
archived: '已归档',
trashed: '回收站',
}
function StateBadge({ state }: { state: DimzouDocumentState }) {
if (state === 'active') {
return <Badge className="bg-emerald-500/12 text-emerald-700 dark:text-emerald-400"></Badge>
}
if (state === 'archived') {
return <Badge variant="secondary"></Badge>
}
return <Badge className="bg-red-500/12 text-red-700 dark:text-red-400"></Badge>
}
const formatTime = (value?: string | null) =>
value ? new Date(value).toLocaleString('zh-CN') : '—'
function DimzouDocumentsPage() {
const [state, setState] = useState<string>('all')
const [detailId, setDetailId] = useState<number | null>(null)
const list = useListPage(
'dimzou-documents',
fetchDimzouDocuments,
state === 'all' ? {} : { state },
)
const detail = useQuery({
queryKey: ['dimzou-document', detailId],
queryFn: () => fetchDimzouDocument(detailId as number),
enabled: detailId !== null,
})
const doc = detail.data?.data
const columns: ColumnDef<DimzouDocument>[] = [
{
header: '文档',
cell: ({ row }) => (
<Button
variant="link"
className="h-auto max-w-72 justify-start p-0 text-left"
onClick={() => setDetailId(row.original.id)}
>
<div className="min-w-0">
<div className="truncate font-medium">{row.original.title ?? '—'}</div>
<div className="text-xs text-muted-foreground">
<code className="font-mono">#{row.original.id}</code>
<span className="ml-2">{row.original.owner?.display_name ?? '—'}</span>
</div>
</div>
</Button>
),
},
{
header: '状态',
cell: ({ row }) => <StateBadge state={row.original.state} />,
},
{
header: '分类',
cell: ({ row }) => row.original.category?.name ?? '—',
},
{
header: '版本数',
cell: ({ row }) => row.original.versions_count,
},
{
header: '已发布 / 草稿',
cell: ({ row }) =>
`${row.original.published_version ? `v${row.original.published_version}` : '—'} / ${row.original.draft_version ? `v${row.original.draft_version}` : '—'}`,
},
{
header: '更新时间',
cell: ({ row }) => formatTime(row.original.updated_at),
},
]
return (
<div>
<PageHeader
title="文档监控"
description="全站协作文档及其版本、译文与待审情况(只读)"
actions={
<Select value={state} onValueChange={setState}>
<SelectTrigger size="sm" className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{Object.entries(STATE_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
}
/>
<DataTable
columns={columns}
data={list.rows}
pagination={list.pagination}
loading={list.loading}
emptyText="没有文档"
onPageChange={list.setPage}
onPageSizeChange={list.setPageSize}
/>
<DetailSheet
open={detailId !== null}
onOpenChange={(open) => !open && setDetailId(null)}
title={doc?.title ?? `文档 #${detailId ?? ''}`}
description={doc ? `状态 ${STATE_LABELS[doc.state] ?? doc.state}` : undefined}
fields={[
{ label: '所有者', value: doc?.owner?.display_name ?? '—' },
{ label: '分类', value: doc?.category?.name ?? '—' },
{ label: '版本数', value: doc?.versions_count ?? 0 },
{ label: '待审修订', value: doc?.pending_revisions_count ?? 0 },
{ label: '待审加入申请', value: doc?.pending_join_requests_count ?? 0 },
{ label: '归档时间', value: formatTime(doc?.archived_at) },
{ label: '删除时间', value: formatTime(doc?.deleted_at) },
{ label: '创建时间', value: formatTime(doc?.created_at) },
]}
jsonBlocks={[
{
label: `版本(${doc?.versions?.length ?? 0}`,
value: doc?.versions?.length
? doc.versions.map((v) => ({
version: v.version_number,
status: v.status,
visibility: v.visibility,
language: v.language,
published_at: v.published_at,
}))
: null,
},
{
label: `译文(${doc?.translations?.length ?? 0}`,
value: doc?.translations?.length ? doc.translations : null,
},
]}
/>
</div>
)
}

View File

@ -0,0 +1,137 @@
import { createFileRoute } from '@tanstack/react-router'
import { PERM, requirePermission } from '@/auth/permissions'
import { useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import type { DimzouPublication } from '@/api/types'
import { fetchDimzouPublications } from '@/api/modules/dimzou'
import { useListPage } from '@/lib/list-page'
import { DataTable } from '@/components/data-table/data-table'
import { PageHeader } from '@/components/page-header'
import { DetailSheet } from '@/features/studio/detail-sheet'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export const Route = createFileRoute('/_authed/dimzou/publications')({
beforeLoad: () => requirePermission(PERM.DIMZOU_READ),
component: DimzouPublicationsPage,
})
const formatTime = (value?: string | null) =>
value ? new Date(value).toLocaleString('zh-CN') : '—'
function DimzouPublicationsPage() {
const [visibility, setVisibility] = useState<string>('all')
const [detail, setDetail] = useState<DimzouPublication | null>(null)
const list = useListPage(
'dimzou-publications',
fetchDimzouPublications,
visibility === 'all' ? {} : { visibility },
)
const columns: ColumnDef<DimzouPublication>[] = [
{
header: '标题',
cell: ({ row }) => (
<Button
variant="link"
className="h-auto max-w-72 justify-start p-0 text-left"
onClick={() => setDetail(row.original)}
>
<div className="min-w-0">
<div className="truncate font-medium">{row.original.title ?? '—'}</div>
<div className="text-xs text-muted-foreground">
<code className="font-mono">#{row.original.id}</code>
<span className="ml-2">{row.original.author?.display_name ?? '—'}</span>
</div>
</div>
</Button>
),
},
{
header: '可见性',
cell: ({ row }) =>
row.original.visibility === 'public' ? (
<Badge className="bg-emerald-500/12 text-emerald-700 dark:text-emerald-400"></Badge>
) : (
<Badge variant="secondary"></Badge>
),
},
{
header: '语言',
cell: ({ row }) => row.original.language ?? '—',
},
{
header: '版本',
cell: ({ row }) => `v${row.original.version_number}`,
},
{
header: '块数',
cell: ({ row }) => row.original.block_count ?? 0,
},
{
header: '分类',
cell: ({ row }) => row.original.category?.name ?? '—',
},
{
header: '发布时间',
cell: ({ row }) => formatTime(row.original.published_at),
},
]
return (
<div>
<PageHeader
title="发布监控"
description="全站已发布内容快照(只读)"
actions={
<Select value={visibility} onValueChange={setVisibility}>
<SelectTrigger size="sm" className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="public"></SelectItem>
<SelectItem value="group_only"></SelectItem>
</SelectContent>
</Select>
}
/>
<DataTable
columns={columns}
data={list.rows}
pagination={list.pagination}
loading={list.loading}
emptyText="没有发布记录"
onPageChange={list.setPage}
onPageSizeChange={list.setPageSize}
/>
<DetailSheet
open={detail !== null}
onOpenChange={(open) => !open && setDetail(null)}
title={detail?.title ?? ''}
description={detail ? `文档 #${detail.document_id} · v${detail.version_number}` : undefined}
fields={[
{ label: '作者', value: detail?.author?.display_name ?? '—' },
{ label: '可见性', value: detail?.visibility === 'public' ? '公开' : '仅协作组' },
{ label: '语言', value: detail?.language ?? '—' },
{ label: '块数', value: detail?.block_count ?? 0 },
{ label: '分类', value: detail?.category?.name ?? '—' },
{ label: '摘要', value: detail?.summary ?? '—' },
{ label: '发布时间', value: formatTime(detail?.published_at) },
]}
jsonBlocks={[
{ label: '关键词', value: detail?.keywords?.length ? detail.keywords : null },
{ label: 'Feed 卡片', value: detail?.feed_card ?? null },
]}
/>
</div>
)
}