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>) =>
|
export const updateRobotAuthor = (id: string, payload: Partial<StudioRobotAuthor>) =>
|
||||||
api.put<ApiResponse<StudioRobotAuthor>>(`${BASE}/robot-authors/${id}`, payload)
|
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) ----
|
// ---- agent tokens (show-once) ----
|
||||||
|
|
||||||
export const fetchAgentTokens = () =>
|
export const fetchAgentTokens = () =>
|
||||||
|
|||||||
@@ -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 type { StudioArticle } from '@/api/types'
|
||||||
import { fetchArticles } from '@/api/modules/studio'
|
import { fetchArticles } from '@/api/modules/studio'
|
||||||
import { useListPage } from '@/lib/list-page'
|
import { useListPage } from '@/lib/list-page'
|
||||||
|
import { ArticlePreviewSheet } from '@/features/studio/article-preview-sheet'
|
||||||
import { DetailSheet } from '@/features/studio/detail-sheet'
|
import { DetailSheet } from '@/features/studio/detail-sheet'
|
||||||
import { DataTable } from '@/components/data-table/data-table'
|
import { DataTable } from '@/components/data-table/data-table'
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
@@ -26,6 +27,7 @@ const STATUSES = ['planned', 'drafting', 'drafted', 'published', 'scheduled'] as
|
|||||||
function ArticlesPage() {
|
function ArticlesPage() {
|
||||||
const [status, setStatus] = useState('all')
|
const [status, setStatus] = useState('all')
|
||||||
const [detail, setDetail] = useState<StudioArticle | null>(null)
|
const [detail, setDetail] = useState<StudioArticle | null>(null)
|
||||||
|
const [preview, setPreview] = useState<StudioArticle | null>(null)
|
||||||
const list = useListPage(
|
const list = useListPage(
|
||||||
'studio-articles',
|
'studio-articles',
|
||||||
fetchArticles,
|
fetchArticles,
|
||||||
@@ -67,9 +69,19 @@ function ArticlesPage() {
|
|||||||
{
|
{
|
||||||
header: '操作',
|
header: '操作',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Button size="sm" variant="ghost" onClick={() => setDetail(row.original)}>
|
<div className="flex gap-1.5">
|
||||||
详情
|
<Button
|
||||||
</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 },
|
{ label: '发布排期', value: detail?.publish_schedule },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ArticlePreviewSheet article={preview} onOpenChange={(open) => !open && setPreview(null)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import 'tw-animate-css';
|
@import 'tw-animate-css';
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user