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 <[email protected]>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
import type {
|
import type {
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
|
DimzouDocument,
|
||||||
|
DimzouPublication,
|
||||||
DimzouTranslation,
|
DimzouTranslation,
|
||||||
ListParams,
|
ListParams,
|
||||||
PaginatedResponse,
|
PaginatedResponse,
|
||||||
@@ -9,6 +11,18 @@ import type {
|
|||||||
|
|
||||||
const BASE = '/api/admin/v1/dimzou'
|
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) {
|
export function fetchDimzouTranslations(params: ListParams) {
|
||||||
return api.get<PaginatedResponse<DimzouTranslation>>(`${BASE}/translations`, params)
|
return api.get<PaginatedResponse<DimzouTranslation>>(`${BASE}/translations`, params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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