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.
This commit is contained in:
2026-07-08 22:23:49 +08:00
parent a84e5dcb54
commit 3f3a8347eb
4 changed files with 126 additions and 3 deletions

View File

@ -36,6 +36,20 @@ export const fetchStylePresets = (params: ListParams) =>
export const updateRobotAuthor = (id: string, payload: Partial<StudioRobotAuthor>) =>
api.put<ApiResponse<StudioRobotAuthor>>(`${BASE}/robot-authors/${id}`, payload)
export interface ArticlePreview {
kind: 'studio_article_preview'
article_id: string
version_status: 'draft' | 'published' | null
title: string | null
summary: string | null
cover_image: { url: string; alt?: string | null } | null
body_html: string
has_content: boolean
}
export const fetchArticlePreview = (id: string) =>
api.get<ApiResponse<ArticlePreview>>(`${BASE}/articles/${id}/preview`)
// ---- agent tokens (show-once) ----
export const fetchAgentTokens = () =>

View File

@ -0,0 +1,94 @@
import { useQuery } from '@tanstack/react-query'
import { fetchArticlePreview } from '@/api/modules/studio'
import type { StudioArticle } from '@/api/types'
import { StatusPill } from '@/components/status-pill'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
interface ArticlePreviewSheetProps {
article: StudioArticle | null
onOpenChange: (open: boolean) => void
}
/** Renders the article's Dimzou draft (title/summary/cover + body HTML). */
export function ArticlePreviewSheet({ article, onOpenChange }: ArticlePreviewSheetProps) {
const preview = useQuery({
queryKey: ['studio-article-preview', article?.id],
queryFn: () => fetchArticlePreview(article!.id),
enabled: article !== null,
})
const data = preview.data?.data
return (
<Sheet open={article !== null} onOpenChange={onOpenChange}>
<SheetContent className="w-full overflow-y-auto sm:max-w-2xl">
<SheetHeader>
<SheetTitle className="flex items-center gap-2">
稿
{data?.version_status && (
<Badge variant={data.version_status === 'draft' ? 'secondary' : 'default'}>
{data.version_status === 'draft' ? '草稿版本' : '已发布版本'}
</Badge>
)}
{article && <StatusPill status={article.status} />}
</SheetTitle>
<SheetDescription className="break-all">
{article?.dimzou_document_id
? `Dimzou 文档 #${article.dimzou_document_id}`
: '尚未建稿'}
</SheetDescription>
</SheetHeader>
<div className="px-4 pb-10">
{preview.isPending ? (
<div className="flex flex-col gap-3">
<Skeleton className="h-40 w-full" />
<Skeleton className="h-6 w-2/3" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
</div>
) : !data || (!data.has_content && !data.title) ? (
<p className="py-16 text-center text-sm text-muted-foreground"></p>
) : (
<article>
{data.cover_image?.url && (
<img
src={data.cover_image.url}
alt={data.cover_image.alt ?? data.title ?? ''}
className="mb-5 aspect-video w-full rounded-lg border object-cover"
/>
)}
{data.title && (
<h1 className="mb-2 text-2xl font-semibold tracking-tight text-foreground">
{data.title}
</h1>
)}
{data.summary && (
<p className="mb-6 border-l-2 pl-3 text-sm text-muted-foreground">{data.summary}</p>
)}
{data.has_content ? (
<div
className="prose prose-sm max-w-none dark:prose-invert prose-headings:font-semibold prose-img:rounded-lg"
// Body is our own agent-authored content rendered by the Dimzou
// ProseMirror serializer (structured nodes, not arbitrary HTML).
dangerouslySetInnerHTML={{ __html: data.body_html }}
/>
) : (
<p className="text-sm text-muted-foreground">/</p>
)}
</article>
)}
</div>
</SheetContent>
</Sheet>
)
}

View File

@ -4,6 +4,7 @@ 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'
@ -26,6 +27,7 @@ const STATUSES = ['planned', 'drafting', 'drafted', 'published', 'scheduled'] as
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,
@ -67,9 +69,19 @@ function ArticlesPage() {
{
header: '操作',
cell: ({ row }) => (
<Button size="sm" variant="ghost" onClick={() => setDetail(row.original)}>
</Button>
<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>
),
},
]
@ -132,6 +144,8 @@ function ArticlesPage() {
{ label: '发布排期', value: detail?.publish_schedule },
]}
/>
<ArticlePreviewSheet article={preview} onOpenChange={(open) => !open && setPreview(null)} />
</div>
)
}

View File

@ -1,5 +1,6 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));