feat(studio/robot-authors): category filter + display_name & category columns
- searchable CategoryPicker (Popover+Command) reusable across studio pages, plus useCategoryOptions hook (shared tree query + id→name resolver) - filter bar with category + status filters and a reset action - new columns: account (avatar + display_name + uid), category name; persona demoted to a secondary column; display_name/category surfaced in detail sheet
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { toast } from 'sonner'
|
||||
import type { StudioRobotAuthor } from '@/api/types'
|
||||
import type { ListParams, StudioRobotAuthor } from '@/api/types'
|
||||
import { fetchRobotAuthors, updateRobotAuthor } from '@/api/modules/studio'
|
||||
import { handleApiError } from '@/lib/errors'
|
||||
import { useListPage } from '@/lib/list-page'
|
||||
import { CategoryPicker } from '@/features/categories/category-picker'
|
||||
import { useCategoryOptions } from '@/features/categories/use-categories'
|
||||
import { DetailSheet } from '@/features/studio/detail-sheet'
|
||||
import { DataTable } from '@/components/data-table/data-table'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
import { StatusPill } from '@/components/status-pill'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -30,12 +35,21 @@ const STATUSES = ['pending', 'profiling', 'ready', 'paused'] as const
|
||||
function RobotAuthorsPage() {
|
||||
const queryClient = useQueryClient()
|
||||
const [status, setStatus] = useState('all')
|
||||
const [categoryId, setCategoryId] = useState<string | null>(null)
|
||||
const [detail, setDetail] = useState<StudioRobotAuthor | null>(null)
|
||||
const list = useListPage(
|
||||
'studio-robot-authors',
|
||||
fetchRobotAuthors,
|
||||
status === 'all' ? {} : { status },
|
||||
)
|
||||
const { nameOf } = useCategoryOptions()
|
||||
|
||||
const filters: ListParams = {}
|
||||
if (status !== 'all') filters.status = status
|
||||
if (categoryId) filters.category_id = categoryId
|
||||
|
||||
const list = useListPage('studio-robot-authors', fetchRobotAuthors, filters)
|
||||
const hasFilters = status !== 'all' || categoryId !== null
|
||||
|
||||
const resetFilters = () => {
|
||||
setStatus('all')
|
||||
setCategoryId(null)
|
||||
}
|
||||
|
||||
const setAuthorStatus = async (row: StudioRobotAuthor, next: 'ready' | 'paused') => {
|
||||
try {
|
||||
@@ -50,13 +64,33 @@ function RobotAuthorsPage() {
|
||||
|
||||
const columns: ColumnDef<StudioRobotAuthor>[] = [
|
||||
{
|
||||
header: '作者',
|
||||
header: '账号',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<div className="max-w-72 truncate font-medium">
|
||||
{row.original.persona?.slice(0, 40) || '(人设待补全)'}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src={row.original.avatar ?? undefined} alt="" />
|
||||
<AvatarFallback>
|
||||
{(row.original.display_name ?? 'R').slice(0, 1).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<div className="max-w-44 truncate font-medium">
|
||||
{row.original.display_name || '(未命名)'}
|
||||
</div>
|
||||
<code className="font-mono text-xs text-muted-foreground">{row.original.user_uid}</code>
|
||||
</div>
|
||||
<code className="font-mono text-xs text-muted-foreground">{row.original.user_uid}</code>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: '分类',
|
||||
cell: ({ row }) => <span className="text-sm">{nameOf(row.original.category_id)}</span>,
|
||||
},
|
||||
{
|
||||
header: '人设',
|
||||
cell: ({ row }) => (
|
||||
<div className="max-w-64 truncate text-sm text-muted-foreground">
|
||||
{row.original.persona?.slice(0, 40) || '(待补全)'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -94,10 +128,15 @@ function RobotAuthorsPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="机器人作者"
|
||||
description="由 cron 播种、Agent 补全人设;可在此暂停/恢复创作"
|
||||
actions={
|
||||
<PageHeader title="机器人作者" description="由 cron 播种、Agent 补全人设;可在此暂停/恢复创作" />
|
||||
|
||||
<div className="mb-4 flex flex-wrap items-end gap-4 rounded-lg border bg-card p-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label className="text-xs text-muted-foreground">分类</Label>
|
||||
<CategoryPicker value={categoryId} onChange={setCategoryId} />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label className="text-xs text-muted-foreground">状态</Label>
|
||||
<Select value={status} onValueChange={setStatus}>
|
||||
<SelectTrigger size="sm" className="w-32">
|
||||
<SelectValue />
|
||||
@@ -111,8 +150,15 @@ function RobotAuthorsPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{hasFilters && (
|
||||
<Button variant="ghost" size="sm" onClick={resetFilters}>
|
||||
<X className="size-4" />
|
||||
重置
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={list.rows}
|
||||
@@ -121,13 +167,17 @@ function RobotAuthorsPage() {
|
||||
onPageChange={list.setPage}
|
||||
onPageSizeChange={list.setPageSize}
|
||||
/>
|
||||
|
||||
<DetailSheet
|
||||
open={detail !== null}
|
||||
onOpenChange={(open) => !open && setDetail(null)}
|
||||
title="机器人作者详情"
|
||||
title={detail?.display_name || '机器人作者详情'}
|
||||
description={detail ? nameOf(detail.category_id) : undefined}
|
||||
fields={[
|
||||
{ label: 'ID', value: <code className="font-mono text-xs">{detail?.id}</code> },
|
||||
{ label: '账号昵称', value: detail?.display_name },
|
||||
{ label: '用户 UID', value: <code className="font-mono text-xs">{detail?.user_uid}</code> },
|
||||
{ label: '分类', value: detail && nameOf(detail.category_id) },
|
||||
{ label: '状态', value: detail && <StatusPill status={detail.status} /> },
|
||||
{ label: '定位', value: detail?.positioning },
|
||||
{ label: '目标读者', value: detail?.target_readers },
|
||||
|
||||
Reference in New Issue
Block a user