From afe4a4b9190762aa9ab318c0c5c5215b6b8de16f Mon Sep 17 00:00:00 2001 From: kongkx Date: Thu, 10 Sep 2026 14:03:59 +0800 Subject: [PATCH] feat(studio): manage article series --- src/api/modules/studio.ts | 17 ++++++++ src/features/studio/series-edit-sheet.tsx | 49 +++++++++++++++++++++++ src/routes/_authed/studio/series.tsx | 23 ++++++++--- 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 src/features/studio/series-edit-sheet.tsx diff --git a/src/api/modules/studio.ts b/src/api/modules/studio.ts index 028ac0e..90c84d4 100644 --- a/src/api/modules/studio.ts +++ b/src/api/modules/studio.ts @@ -21,6 +21,23 @@ export const fetchRobotAuthors = (params: ListParams) => export const fetchSeries = (params: ListParams) => api.get>(`${BASE}/series`, params) +export interface SeriesPayload { + category_id?: string + subtopic_id?: string | null + robot_author_id?: string + goal?: string | null + positioning?: string | null + target_readers?: string | null + reader_journey?: string | null + target_article_count?: number + planned_articles?: unknown[] | null + status?: StudioSeries['status'] +} + +export const createSeries = (payload: SeriesPayload) => api.post>(`${BASE}/series`, payload) +export const updateSeries = (id: string, payload: SeriesPayload) => api.put>(`${BASE}/series/${id}`, payload) +export const deleteSeries = (id: string) => api.delete>(`${BASE}/series/${id}`) + export const fetchArticles = (params: ListParams) => api.get>(`${BASE}/articles`, params) diff --git a/src/features/studio/series-edit-sheet.tsx b/src/features/studio/series-edit-sheet.tsx new file mode 100644 index 0000000..04c8938 --- /dev/null +++ b/src/features/studio/series-edit-sheet.tsx @@ -0,0 +1,49 @@ +import { useEffect } from 'react' +import { useQuery } from '@tanstack/react-query' +import { Controller, useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { toast } from 'sonner' +import type { StudioSeries } from '@/api/types' +import { createSeries, fetchRobotAuthors, fetchSubtopics, updateSeries, type SeriesPayload } from '@/api/modules/studio' +import { handleApiError } from '@/lib/errors' +import { CategoryPicker } from '@/features/categories/category-picker' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' +import { Textarea } from '@/components/ui/textarea' + +const statuses = ['planned', 'in_progress', 'complete'] as const +const schema = z.object({ + category_id: z.string(), robot_author_id: z.string(), subtopic_id: z.string(), + goal: z.string(), positioning: z.string(), target_readers: z.string(), reader_journey: z.string(), + target_article_count: z.number().int('必须是整数').min(0, '不能小于 0'), + planned_articles: z.string().refine((value) => { if (!value.trim()) return true; try { return Array.isArray(JSON.parse(value)) } catch { return false } }, '必须是 JSON 数组'), + status: z.enum(statuses), +}).superRefine((values, context) => { + if (!values.category_id) context.addIssue({ code: 'custom', path: ['category_id'], message: '请选择分类' }) + if (!values.robot_author_id) context.addIssue({ code: 'custom', path: ['robot_author_id'], message: '请选择机器人作者' }) +}) +type FormValues = z.infer +interface Props { series: StudioSeries | null; creating: boolean; onOpenChange: (open: boolean) => void; onSaved: () => void } + +export function SeriesEditSheet({ series, creating, onOpenChange, onSaved }: Props) { + const form = useForm({ resolver: zodResolver(schema), defaultValues: { category_id: '', robot_author_id: '', subtopic_id: '', goal: '', positioning: '', target_readers: '', reader_journey: '', target_article_count: 0, planned_articles: '', status: 'planned' } }) + useEffect(() => { form.reset({ category_id: series?.category_id ?? '', robot_author_id: series?.robot_author_id ?? '', subtopic_id: series?.subtopic_id ?? '', goal: series?.goal ?? '', positioning: series?.positioning ?? '', target_readers: series?.target_readers ?? '', reader_journey: series?.reader_journey ?? '', target_article_count: series?.target_article_count ?? 0, planned_articles: series?.planned_articles ? JSON.stringify(series.planned_articles, null, 2) : '', status: series?.status ?? 'planned' }) }, [series, creating, form]) + const categoryId = form.watch('category_id') + const authors = useQuery({ queryKey: ['studio-robot-authors', 'series-editor', categoryId], queryFn: () => fetchRobotAuthors({ category_id: categoryId, page_size: 100 }), enabled: creating && categoryId !== '' }) + const subtopics = useQuery({ queryKey: ['studio-subtopics', 'series-editor', categoryId], queryFn: () => fetchSubtopics({ category_id: categoryId, page_size: 100 }), enabled: creating && categoryId !== '' }) + const submit = async (values: FormValues) => { + const payload: SeriesPayload = { goal: values.goal.trim() || null, positioning: values.positioning.trim() || null, target_readers: values.target_readers.trim() || null, reader_journey: values.reader_journey.trim() || null, target_article_count: values.target_article_count, planned_articles: values.planned_articles.trim() ? JSON.parse(values.planned_articles) : null, status: values.status } + if (creating) Object.assign(payload, { category_id: values.category_id, robot_author_id: values.robot_author_id, subtopic_id: values.subtopic_id || null }) + try { if (series) await updateSeries(series.id, payload); else await createSeries(payload); toast.success(series ? '系列已更新' : '系列已创建'); onSaved(); onOpenChange(false) } catch (error) { handleApiError(error, form.setError) } + } + const error = (name: keyof FormValues) => form.formState.errors[name]?.message + return {series ? '编辑文章系列' : '新建文章系列'}
+ {creating && <> { field.onChange(value ?? ''); form.setValue('robot_author_id', ''); form.setValue('subtopic_id', '') }} />} /> } /> } />} +