Files
gig-admin/src/routes/_authed/studio/articles.tsx
kongkx 3f3a8347eb feat(studio/articles): Dimzou draft preview
Adds a 预览 action (enabled once an article has a Dimzou document) opening a
sheet that fetches and renders the draft — cover, title, summary, and body HTML
(typography/prose). Re-enables the @tailwindcss/typography plugin for prose.
2026-07-08 22:23:49 +08:00

152 lines
5.2 KiB
TypeScript

import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import type { StudioArticle } from '@/api/types'
import { fetchArticles } from '@/api/modules/studio'
import { useListPage } from '@/lib/list-page'
import { ArticlePreviewSheet } from '@/features/studio/article-preview-sheet'
import { DetailSheet } from '@/features/studio/detail-sheet'
import { DataTable } from '@/components/data-table/data-table'
import { PageHeader } from '@/components/page-header'
import { StatusPill } from '@/components/status-pill'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export const Route = createFileRoute('/_authed/studio/articles')({
component: ArticlesPage,
})
const STATUSES = ['planned', 'drafting', 'drafted', 'published', 'scheduled'] as const
function ArticlesPage() {
const [status, setStatus] = useState('all')
const [detail, setDetail] = useState<StudioArticle | null>(null)
const [preview, setPreview] = useState<StudioArticle | null>(null)
const list = useListPage(
'studio-articles',
fetchArticles,
status === 'all' ? {} : { status },
)
const columns: ColumnDef<StudioArticle>[] = [
{
header: '文章',
cell: ({ row }) => (
<div>
<div className="max-w-80 truncate font-medium">
{row.original.title ?? '(未命名)'}
</div>
<span className="text-xs text-muted-foreground">
{row.original.article_number}
</span>
</div>
),
},
{ header: '状态', cell: ({ row }) => <StatusPill status={row.original.status} /> },
{
header: 'Dimzou',
cell: ({ row }) =>
row.original.dimzou_publication_id ? (
<code className="font-mono text-xs">
doc #{row.original.dimzou_document_id} · pub #{row.original.dimzou_publication_id}
</code>
) : row.original.dimzou_document_id ? (
<code className="font-mono text-xs">稿 doc #{row.original.dimzou_document_id}</code>
) : (
<span className="text-xs text-muted-foreground">稿</span>
),
},
{
header: '更新时间',
cell: ({ row }) => new Date(row.original.updated_at).toLocaleString('zh-CN'),
},
{
header: '操作',
cell: ({ row }) => (
<div className="flex gap-1.5">
<Button
size="sm"
variant="outline"
disabled={!row.original.dimzou_document_id}
onClick={() => setPreview(row.original)}
>
</Button>
<Button size="sm" variant="ghost" onClick={() => setDetail(row.original)}>
</Button>
</div>
),
},
]
return (
<div>
<PageHeader
title="文章"
description="正文存于 Dimzou 草稿(块修订即编辑历史);此处为计划元数据"
actions={
<Select value={status} onValueChange={setStatus}>
<SelectTrigger size="sm" className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{STATUSES.map((s) => (
<SelectItem key={s} value={s}>
<StatusPill status={s} />
</SelectItem>
))}
</SelectContent>
</Select>
}
/>
<DataTable
columns={columns}
data={list.rows}
pagination={list.pagination}
loading={list.loading}
onPageChange={list.setPage}
onPageSizeChange={list.setPageSize}
/>
<DetailSheet
open={detail !== null}
onOpenChange={(open) => !open && setDetail(null)}
title={detail?.title ?? '文章详情'}
description={detail?.summary ?? undefined}
fields={[
{ label: 'ID', value: <code className="font-mono text-xs">{detail?.id}</code> },
{ label: '状态', value: detail && <StatusPill status={detail.status} /> },
{ label: '系列内序号', value: detail?.article_number },
{ label: '标签', value: detail?.tags?.join('、') },
{ label: 'SEO 关键词', value: detail?.seo_keywords?.join('、') },
{ label: 'CTA', value: detail?.cta },
{ label: '封面提示词', value: detail?.cover_image_prompt },
{
label: 'Dimzou 文档',
value: detail?.dimzou_document_id ? `#${detail.dimzou_document_id}` : '未建稿',
},
{
label: 'Dimzou 发布',
value: detail?.dimzou_publication_id ? `#${detail.dimzou_publication_id}` : '未发布',
},
]}
jsonBlocks={[
{ label: '要点 key_takeaways', value: detail?.key_takeaways },
{ label: '大纲 outline', value: detail?.outline },
{ label: '计划 plan', value: detail?.plan },
{ label: '发布排期', value: detail?.publish_schedule },
]}
/>
<ArticlePreviewSheet article={preview} onOpenChange={(open) => !open && setPreview(null)} />
</div>
)
}