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:
@ -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 = () =>
|
||||
|
||||
94
src/features/studio/article-preview-sheet.tsx
Normal file
94
src/features/studio/article-preview-sheet.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user