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:
@ -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)
|
||||
}
|
||||
|
||||
171
src/routes/_authed/dimzou/documents.tsx
Normal file
171
src/routes/_authed/dimzou/documents.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
137
src/routes/_authed/dimzou/publications.tsx
Normal file
137
src/routes/_authed/dimzou/publications.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user