feat(auth): wire permission gating through menu, routes and actions
The store's can()/useCan() helpers existed but were unused. Now: - src/auth/permissions.ts PERM catalog mirrors backend keys - every nav item carries a permission; sidebar filter reuses can() - requirePermission() beforeLoad guard on all child routes (direct URLs redirect home when the session lacks the key) - useCan() hides/disables mutation buttons: moderation actions, retranslate, robot-author edit/pause, category + locale CRUD, and the reserved-SID assign path (admin:sid:override hint) - 403 responses toast 无权限执行此操作; can() unit tests added Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useCan } from '@/auth/store'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { ChevronRight, Pencil, Plus, Trash2 } from 'lucide-react'
|
||||
@@ -15,10 +17,12 @@ import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export const Route = createFileRoute('/_authed/categories/')({
|
||||
beforeLoad: () => requirePermission(PERM.CATEGORY_READ),
|
||||
component: CategoriesPage,
|
||||
})
|
||||
|
||||
function CategoriesPage() {
|
||||
const canCreate = useCan(PERM.CATEGORY_CREATE)
|
||||
const queryClient = useQueryClient()
|
||||
const tree = useQuery({
|
||||
queryKey: ['categories', 'tree'],
|
||||
@@ -57,10 +61,12 @@ function CategoriesPage() {
|
||||
title="分类树"
|
||||
description="平台服务分类体系;含未启用分类"
|
||||
actions={
|
||||
<Button onClick={() => openCreate(null)}>
|
||||
<Plus className="size-4" />
|
||||
新建顶级分类
|
||||
</Button>
|
||||
canCreate && (
|
||||
<Button onClick={() => openCreate(null)}>
|
||||
<Plus className="size-4" />
|
||||
新建顶级分类
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -113,6 +119,9 @@ function TreeNode({
|
||||
}) {
|
||||
const [open, setOpen] = useState(depth === 0)
|
||||
const children = node.children ?? []
|
||||
const canCreate = useCan(PERM.CATEGORY_CREATE)
|
||||
const canUpdate = useCan(PERM.CATEGORY_UPDATE)
|
||||
const canDelete = useCan(PERM.CATEGORY_DELETE)
|
||||
|
||||
return (
|
||||
<div className={cn(depth > 0 && 'border-l')} style={{ marginLeft: depth > 0 ? 20 : 0 }}>
|
||||
@@ -132,24 +141,30 @@ function TreeNode({
|
||||
{!node.is_active && <Badge variant="secondary">未启用</Badge>}
|
||||
{node.approval_status === 'pending' && <Badge variant="outline">待审核</Badge>}
|
||||
<span className="ml-auto hidden items-center gap-1 group-hover:flex">
|
||||
<Button variant="ghost" size="icon-sm" onClick={() => onCreateChild(node)} aria-label="新建子分类">
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" onClick={() => onEdit(node)} aria-label="编辑">
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button variant="ghost" size="icon-sm" aria-label="删除">
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
</Button>
|
||||
}
|
||||
title={`删除分类「${node.name ?? node.slug}」?`}
|
||||
description="删除后不可恢复;有子分类或被引用时后端会拒绝。"
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
onConfirm={() => onDelete(node)}
|
||||
/>
|
||||
{canCreate && (
|
||||
<Button variant="ghost" size="icon-sm" onClick={() => onCreateChild(node)} aria-label="新建子分类">
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
{canUpdate && (
|
||||
<Button variant="ghost" size="icon-sm" onClick={() => onEdit(node)} aria-label="编辑">
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
{canDelete && (
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button variant="ghost" size="icon-sm" aria-label="删除">
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
</Button>
|
||||
}
|
||||
title={`删除分类「${node.name ?? node.slug}」?`}
|
||||
description="删除后不可恢复;有子分类或被引用时后端会拒绝。"
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
onConfirm={() => onDelete(node)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{open &&
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/categories/pending')({
|
||||
beforeLoad: () => requirePermission(PERM.CATEGORY_APPROVE),
|
||||
component: PendingCategoriesPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useCan } from '@/auth/store'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { toast } from 'sonner'
|
||||
@@ -22,10 +24,12 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/comment-reports')({
|
||||
beforeLoad: () => requirePermission(PERM.COMMENT_READ),
|
||||
component: ReportedCommentsPage,
|
||||
})
|
||||
|
||||
function ReportedCommentsPage() {
|
||||
const canModerate = useCan(PERM.COMMENT_MODERATE)
|
||||
const [status, setStatus] = useState<string>('pending')
|
||||
const [detail, setDetail] = useState<ReportedComment | null>(null)
|
||||
const list = useListPage(
|
||||
@@ -92,7 +96,9 @@ function ReportedCommentsPage() {
|
||||
header: '操作',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{row.original.is_blocked ? (
|
||||
{!canModerate ? (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
) : row.original.is_blocked ? (
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="outline">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { NotificationDevice } from '@/api/types'
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/devices')({
|
||||
beforeLoad: () => requirePermission(PERM.NOTIFICATION_READ),
|
||||
component: DevicesPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useCan } from '@/auth/store'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { toast } from 'sonner'
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/dimzou-translations')({
|
||||
beforeLoad: () => requirePermission(PERM.DIMZOU_READ),
|
||||
component: DimzouTranslationsPage,
|
||||
})
|
||||
|
||||
@@ -31,6 +34,7 @@ const STATUS_LABELS: Record<DimzouTranslation['status'], string> = {
|
||||
}
|
||||
|
||||
function DimzouTranslationsPage() {
|
||||
const canRetranslate = useCan(PERM.DIMZOU_RETRANSLATE)
|
||||
const [status, setStatus] = useState<string>('all')
|
||||
// language filter applies on Enter/blur to avoid a request per keystroke
|
||||
const [languageInput, setLanguageInput] = useState('')
|
||||
@@ -111,18 +115,21 @@ function DimzouTranslationsPage() {
|
||||
},
|
||||
{
|
||||
header: '操作',
|
||||
cell: ({ row }) => (
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="outline" disabled={!row.original.draft_version_id}>
|
||||
重新机器翻译
|
||||
</Button>
|
||||
}
|
||||
title={`重新翻译「${row.original.source_document?.title ?? `#${row.original.id}`}」→ ${row.original.language_code}?`}
|
||||
description="将最新草稿版本中待译和失败的块重新入队机器翻译。"
|
||||
onConfirm={() => requeue(row.original)}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
canRetranslate ? (
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="outline" disabled={!row.original.draft_version_id}>
|
||||
重新机器翻译
|
||||
</Button>
|
||||
}
|
||||
title={`重新翻译「${row.original.source_document?.title ?? `#${row.original.id}`}」→ ${row.original.language_code}?`}
|
||||
description="将最新草稿版本中待译和失败的块重新入队机器翻译。"
|
||||
onConfirm={() => requeue(row.original)}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useCan } from '@/auth/store'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { toast } from 'sonner'
|
||||
@@ -23,10 +25,12 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/event-reports')({
|
||||
beforeLoad: () => requirePermission(PERM.FILEX_READ),
|
||||
component: ReportedEventsPage,
|
||||
})
|
||||
|
||||
function ReportedEventsPage() {
|
||||
const canModerate = useCan(PERM.FILEX_MODERATE)
|
||||
const [status, setStatus] = useState<string>('pending')
|
||||
const [detail, setDetail] = useState<ReportedEvent | null>(null)
|
||||
const list = useListPage(
|
||||
@@ -97,7 +101,9 @@ function ReportedEventsPage() {
|
||||
header: '操作',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{row.original.is_blocked ? (
|
||||
{!canModerate ? (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
) : row.original.is_blocked ? (
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button size="sm" variant="outline">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/exc/demands')({
|
||||
beforeLoad: () => requirePermission(PERM.EXC_READ),
|
||||
component: ExcDemandsPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { ExcDispatch } from '@/api/types'
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/exc/dispatches')({
|
||||
beforeLoad: () => requirePermission(PERM.EXC_READ),
|
||||
component: ExcDispatchesPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/exc/orders')({
|
||||
beforeLoad: () => requirePermission(PERM.EXC_READ),
|
||||
component: ExcOrdersPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/exc/providers')({
|
||||
beforeLoad: () => requirePermission(PERM.EXC_READ),
|
||||
component: ExcProvidersPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useCan } from '@/auth/store'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
@@ -23,10 +25,14 @@ import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
|
||||
export const Route = createFileRoute('/_authed/locales')({
|
||||
beforeLoad: () => requirePermission(PERM.LOCALE_READ),
|
||||
component: LocalesPage,
|
||||
})
|
||||
|
||||
function LocalesPage() {
|
||||
const canCreate = useCan(PERM.LOCALE_CREATE)
|
||||
const canUpdate = useCan(PERM.LOCALE_UPDATE)
|
||||
const canDelete = useCan(PERM.LOCALE_DELETE)
|
||||
const queryClient = useQueryClient()
|
||||
const locales = useQuery({ queryKey: ['locales'], queryFn: fetchLocales })
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
@@ -65,25 +71,29 @@ function LocalesPage() {
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
checked={row.original.is_active}
|
||||
disabled={!canUpdate}
|
||||
onCheckedChange={(v) => toggle(row.original, v)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: '操作',
|
||||
cell: ({ row }) => (
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button variant="ghost" size="icon-sm" aria-label="删除">
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
</Button>
|
||||
}
|
||||
title={`删除语言 ${row.original.code}?`}
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
onConfirm={() => remove(row.original)}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
canDelete ? (
|
||||
<ConfirmDialog
|
||||
trigger={
|
||||
<Button variant="ghost" size="icon-sm" aria-label="删除">
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
</Button>
|
||||
}
|
||||
title={`删除语言 ${row.original.code}?`}
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
onConfirm={() => remove(row.original)}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -93,10 +103,12 @@ function LocalesPage() {
|
||||
title="语言设置"
|
||||
description="平台支持的界面/内容语言"
|
||||
actions={
|
||||
<Button onClick={() => setDialogOpen(true)}>
|
||||
<Plus className="size-4" />
|
||||
添加语言
|
||||
</Button>
|
||||
canCreate && (
|
||||
<Button onClick={() => setDialogOpen(true)}>
|
||||
<Plus className="size-4" />
|
||||
添加语言
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DataTable
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useState } from 'react'
|
||||
import { issueRobotSessionToken } from '@/api/modules/auth'
|
||||
import { handleApiError } from '@/lib/errors'
|
||||
@@ -10,6 +11,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
export const Route = createFileRoute('/_authed/robots')({
|
||||
beforeLoad: () => requirePermission(PERM.ROBOT_TOKEN_ISSUE),
|
||||
component: RobotsPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useCan } from '@/auth/store'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
@@ -26,6 +28,7 @@ import { Switch } from '@/components/ui/switch'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
export const Route = createFileRoute('/_authed/sid')({
|
||||
beforeLoad: () => requirePermission(PERM.SID_MANAGE),
|
||||
component: SidPage,
|
||||
})
|
||||
|
||||
@@ -190,6 +193,7 @@ function LuckyTab() {
|
||||
}
|
||||
|
||||
function AssignTab() {
|
||||
const canOverride = useCan(PERM.SID_OVERRIDE)
|
||||
const [sid, setSid] = useState('')
|
||||
const [checkResult, setCheckResult] = useState<SidCheckResult | null>(null)
|
||||
const [userUid, setUserUid] = useState('')
|
||||
@@ -254,6 +258,11 @@ function AssignTab() {
|
||||
可分配
|
||||
</Badge>
|
||||
)}
|
||||
{checkResult.reserved && !canOverride && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
分配保留号段需要 admin:sid:override 权限
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-1.5">
|
||||
@@ -266,7 +275,10 @@ function AssignTab() {
|
||||
placeholder="01JXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={assign} disabled={!sid || !userUid || busy}>
|
||||
<Button
|
||||
onClick={assign}
|
||||
disabled={!sid || !userUid || busy || (checkResult?.reserved === true && !canOverride)}
|
||||
>
|
||||
分配 SID
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { KeyRound } from 'lucide-react'
|
||||
@@ -14,6 +15,7 @@ import { ShowOnceTokenDialog } from '@/components/show-once-token-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export const Route = createFileRoute('/_authed/studio/agent-tokens')({
|
||||
beforeLoad: () => requirePermission(PERM.STUDIO_AGENT_TOKEN_ISSUE),
|
||||
component: AgentTokensPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { StudioArticle } from '@/api/types'
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/studio/articles')({
|
||||
beforeLoad: () => requirePermission(PERM.STUDIO_READ),
|
||||
component: ArticlesPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { StudioCategoryBrief } from '@/api/types'
|
||||
@@ -10,6 +11,7 @@ import { PageHeader } from '@/components/page-header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export const Route = createFileRoute('/_authed/studio/category-briefs')({
|
||||
beforeLoad: () => requirePermission(PERM.STUDIO_READ),
|
||||
component: CategoryBriefsPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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 { X } from 'lucide-react'
|
||||
@@ -28,12 +30,14 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/studio/robot-authors')({
|
||||
beforeLoad: () => requirePermission(PERM.STUDIO_READ),
|
||||
component: RobotAuthorsPage,
|
||||
})
|
||||
|
||||
const STATUSES = ['pending', 'profiling', 'ready', 'paused'] as const
|
||||
|
||||
function RobotAuthorsPage() {
|
||||
const canManage = useCan(PERM.STUDIO_MANAGE)
|
||||
const queryClient = useQueryClient()
|
||||
const [status, setStatus] = useState('all')
|
||||
const [categoryId, setCategoryId] = useState<string | null>(null)
|
||||
@@ -114,17 +118,21 @@ function RobotAuthorsPage() {
|
||||
<Button size="sm" variant="ghost" onClick={() => setDetail(row.original)}>
|
||||
详情
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => setEditing(row.original)}>
|
||||
编辑
|
||||
</Button>
|
||||
{row.original.status === 'paused' ? (
|
||||
<Button size="sm" variant="outline" onClick={() => setAuthorStatus(row.original, 'ready')}>
|
||||
恢复
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="sm" variant="outline" onClick={() => setAuthorStatus(row.original, 'paused')}>
|
||||
暂停
|
||||
</Button>
|
||||
{canManage && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={() => setEditing(row.original)}>
|
||||
编辑
|
||||
</Button>
|
||||
{row.original.status === 'paused' ? (
|
||||
<Button size="sm" variant="outline" onClick={() => setAuthorStatus(row.original, 'ready')}>
|
||||
恢复
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="sm" variant="outline" onClick={() => setAuthorStatus(row.original, 'paused')}>
|
||||
暂停
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { StudioSeries } from '@/api/types'
|
||||
@@ -11,6 +12,7 @@ import { StatusPill } from '@/components/status-pill'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export const Route = createFileRoute('/_authed/studio/series')({
|
||||
beforeLoad: () => requirePermission(PERM.STUDIO_READ),
|
||||
component: SeriesPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { StudioStylePreset } from '@/api/types'
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export const Route = createFileRoute('/_authed/studio/style-presets')({
|
||||
beforeLoad: () => requirePermission(PERM.STUDIO_READ),
|
||||
component: StylePresetsPage,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { StudioSubtopic } from '@/api/types'
|
||||
import { fetchSubtopics } from '@/api/modules/studio'
|
||||
@@ -8,6 +9,7 @@ import { PageHeader } from '@/components/page-header'
|
||||
import { StatusPill } from '@/components/status-pill'
|
||||
|
||||
export const Route = createFileRoute('/_authed/studio/subtopics')({
|
||||
beforeLoad: () => requirePermission(PERM.STUDIO_READ),
|
||||
component: SubtopicsPage,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user