feat(studio): manage style presets
This commit is contained in:
@@ -107,6 +107,11 @@ export const deleteCategoryBrief = (id: string) =>
|
||||
export const fetchStylePresets = (params: ListParams) =>
|
||||
api.get<PaginatedResponse<StudioStylePreset>>(`${BASE}/style-presets`, params)
|
||||
|
||||
export interface StylePresetPayload { type?: StudioStylePreset['type']; name?: string; payload?: Record<string, unknown> | null }
|
||||
export const createStylePreset = (payload: StylePresetPayload) => api.post<ApiResponse<StudioStylePreset>>(`${BASE}/style-presets`, payload)
|
||||
export const updateStylePreset = (id: string, payload: StylePresetPayload) => api.put<ApiResponse<StudioStylePreset>>(`${BASE}/style-presets/${id}`, payload)
|
||||
export const deleteStylePreset = (id: string) => api.delete<ApiResponse<null>>(`${BASE}/style-presets/${id}`)
|
||||
|
||||
export const updateRobotAuthor = (id: string, payload: Partial<StudioRobotAuthor>) =>
|
||||
api.put<ApiResponse<StudioRobotAuthor>>(`${BASE}/robot-authors/${id}`, payload)
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useEffect } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import { z } from 'zod'
|
||||
import { createStylePreset, updateStylePreset } from '@/api/modules/studio'
|
||||
import type { StudioStylePreset } from '@/api/types'
|
||||
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'
|
||||
import { handleApiError } from '@/lib/errors'
|
||||
|
||||
const schema = z.object({ type: z.enum(['article', 'image', 'bundle']), name: z.string().trim().min(1, '请填写名称').max(255), payload: z.string().refine((value) => { if (!value.trim()) return true; try { const parsed = JSON.parse(value); return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) } catch { return false } }, '必须是 JSON 对象') })
|
||||
type FormValues = z.infer<typeof schema>
|
||||
interface Props { preset: StudioStylePreset | null; creating: boolean; onOpenChange: (open: boolean) => void; onSaved: () => void }
|
||||
export function StylePresetEditSheet({ preset, creating, onOpenChange, onSaved }: Props) {
|
||||
const form = useForm<FormValues>({ resolver: zodResolver(schema), defaultValues: { type: 'article', name: '', payload: '' } })
|
||||
useEffect(() => { form.reset({ type: preset?.type ?? 'article', name: preset?.name ?? '', payload: preset?.payload ? JSON.stringify(preset.payload, null, 2) : '' }) }, [preset, creating, form])
|
||||
const submit = async (values: FormValues) => { const payload = { type: values.type, name: values.name.trim(), payload: values.payload.trim() ? JSON.parse(values.payload) as Record<string, unknown> : null }; try { if (preset) await updateStylePreset(preset.id, payload); else await createStylePreset(payload); toast.success(preset ? '风格预设已更新' : '风格预设已创建'); onSaved(); onOpenChange(false) } catch (error) { handleApiError(error, form.setError) } }
|
||||
return <Sheet open={creating || preset !== null} onOpenChange={onOpenChange}><SheetContent className="w-full overflow-y-auto sm:max-w-xl"><SheetHeader><SheetTitle>{preset ? '编辑风格预设' : '新建风格预设'}</SheetTitle></SheetHeader><form className="grid gap-5 px-4 pb-8" onSubmit={form.handleSubmit(submit)}><Field label="类型" error={form.formState.errors.type?.message}><Controller control={form.control} name="type" render={({ field }) => <Select value={field.value} onValueChange={field.onChange}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="article">文章风格</SelectItem><SelectItem value="image">图片风格</SelectItem><SelectItem value="bundle">组合</SelectItem></SelectContent></Select>} /></Field><Field label="名称" error={form.formState.errors.name?.message}><Input maxLength={255} {...form.register('name')} /></Field><Field label="风格定义(JSON 对象)" error={form.formState.errors.payload?.message}><Textarea rows={12} className="font-mono text-xs" {...form.register('payload')} /></Field><Button type="submit" disabled={form.formState.isSubmitting}>{form.formState.isSubmitting ? '保存中…' : '保存'}</Button></form></SheetContent></Sheet>
|
||||
}
|
||||
function Field({ label, error, children }: { label: string; error?: string; children: React.ReactNode }) { return <div className="grid gap-2"><Label>{label}</Label>{children}{error && <p className="text-sm text-destructive">{error}</p>}</div> }
|
||||
@@ -1,9 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useCan } from '@/auth/store'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { StudioStylePreset } from '@/api/types'
|
||||
import { fetchStylePresets } from '@/api/modules/studio'
|
||||
import { deleteStylePreset, fetchStylePresets } from '@/api/modules/studio'
|
||||
import { handleApiError } from '@/lib/errors'
|
||||
import { StylePresetEditSheet } from '@/features/studio/style-preset-edit-sheet'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { useListPage } from '@/lib/list-page'
|
||||
import { DetailSheet } from '@/features/studio/detail-sheet'
|
||||
import { DataTable } from '@/components/data-table/data-table'
|
||||
@@ -30,13 +35,19 @@ const TYPE_LABELS: Record<string, string> = {
|
||||
}
|
||||
|
||||
function StylePresetsPage() {
|
||||
const canManage = useCan(PERM.STUDIO_MANAGE)
|
||||
const queryClient = useQueryClient()
|
||||
const [type, setType] = useState('all')
|
||||
const [detail, setDetail] = useState<StudioStylePreset | null>(null)
|
||||
const [editing, setEditing] = useState<StudioStylePreset | null>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const list = useListPage(
|
||||
'studio-style-presets',
|
||||
fetchStylePresets,
|
||||
type === 'all' ? {} : { type },
|
||||
)
|
||||
const refresh = () => { void queryClient.invalidateQueries({ queryKey: ['studio-style-presets'] }); void list.query.refetch() }
|
||||
const remove = async (row: StudioStylePreset) => { try { await deleteStylePreset(row.id); refresh() } catch (error) { handleApiError(error) } }
|
||||
|
||||
const columns: ColumnDef<StudioStylePreset>[] = [
|
||||
{ header: '名称', cell: ({ row }) => <span className="font-medium">{row.original.name}</span> },
|
||||
@@ -53,9 +64,7 @@ function StylePresetsPage() {
|
||||
{
|
||||
header: '操作',
|
||||
cell: ({ row }) => (
|
||||
<Button size="sm" variant="ghost" onClick={() => setDetail(row.original)}>
|
||||
详情
|
||||
</Button>
|
||||
<div className="flex gap-1.5"><Button size="sm" variant="ghost" onClick={() => setDetail(row.original)}>详情</Button>{canManage && <Button size="sm" variant="outline" onClick={() => setEditing(row.original)}>编辑</Button>}{canManage && <ConfirmDialog trigger={<Button size="sm" variant="destructive">删除</Button>} title="删除风格预设?" confirmLabel="删除" destructive onConfirm={() => remove(row.original)} />}</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
@@ -65,8 +74,7 @@ function StylePresetsPage() {
|
||||
<PageHeader
|
||||
title="风格预设"
|
||||
description="Agent 的风格库(preset / hybrid 模式),按 type + name 幂等"
|
||||
actions={
|
||||
<Select value={type} onValueChange={setType}>
|
||||
actions={<div className="flex gap-2">{canManage && <Button onClick={() => setCreating(true)}>新建预设</Button>}<Select value={type} onValueChange={setType}>
|
||||
<SelectTrigger size="sm" className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -76,9 +84,9 @@ function StylePresetsPage() {
|
||||
<SelectItem value="image">图片风格</SelectItem>
|
||||
<SelectItem value="bundle">组合</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
</Select></div>}
|
||||
/>
|
||||
<StylePresetEditSheet preset={editing} creating={creating} onOpenChange={(open) => { if (!open) { setEditing(null); setCreating(false) } }} onSaved={refresh} />
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={list.rows}
|
||||
|
||||
Reference in New Issue
Block a user