feat(ai): 统一 AI 配置入口到 /admin/ai-settings

## 新增
- 创建 /admin/ai-settings 统一配置页(AiProviderSettingsCard + AiUsageDashboard)
- admin 侧边栏新增"AI 配置"菜单项(权限 AI_CONFIGURE,图标 Sparkles)
- 新增 deleteAiProvider 数据访问层(事务删除 + 自动转移默认)
- 新增 deleteAiProviderAction Server Action(Zod 校验 + 权限校验)
- AiProviderSettingsCard 新增删除按钮(AlertDialog 确认 + destructive 变体)
- 新增 i18n 翻译键(delete/deleteConfirm/deleteSuccess 等,zh-CN + en)

## 移除
- 从 /settings 移除 AI 标签页(原 VALID_TABS 含 "ai",现仅 4 标签页)
- 从考试页面移除 AI 配置弹窗(Dialog + AiProviderSettingsCard 内嵌)
- 从 ai-provider-selector.tsx 移除配置弹窗(managePanel/manageOpen props)
- 移除 settings-view.tsx 中 canConfigureAi 逻辑和未使用 import

## 变更
- 考试页面"管理"按钮改为 Link 跳转到 /admin/ai-settings
- ai-provider-selector.tsx"管理"按钮改为 Link 跳转到 /admin/ai-settings
- exam-form.tsx 移除 providerDialogOpen/providerDialogKey 状态
- 修正架构文档 004 中 Action 命名(getAiProvidersAction → getAiProviderSummaries 等)

## 架构文档同步
- 004 更新 settings 模块章节(V3 标记/修正 Action 名称/新增 deleteAiProvider)
- 005 新增 deleteAiProviderAction 节点 + /admin/ai-settings 路由
This commit is contained in:
SpecialX
2026-06-23 19:33:28 +08:00
parent d884c6d513
commit 7e320d78c1
13 changed files with 618 additions and 213 deletions

View File

@@ -2,6 +2,7 @@
import type { Control, UseFormReturn } from "react-hook-form"
import { useTranslations } from "next-intl"
import Link from "next/link"
import { Settings } from "lucide-react"
import {
FormField,
@@ -27,15 +28,6 @@ import {
CardHeader,
CardTitle,
} from "@/shared/components/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/components/ui/dialog"
import { AiProviderSettingsCard } from "@/modules/settings/components/ai-provider-settings-card"
import type { AiProviderSummary } from "@/modules/settings/actions"
import { formatDateTime } from "@/shared/lib/utils"
import type { ExamFormValues, PreviewBackgroundTask } from "./exam-form-types"
@@ -47,10 +39,6 @@ type ExamAiGeneratorProps = {
aiProviders: AiProviderSummary[]
setAiProviders: (providers: AiProviderSummary[]) => void
loadingAiProviders: boolean
providerDialogOpen: boolean
setProviderDialogOpen: (open: boolean) => void
providerDialogKey: number
setProviderDialogKey: (key: number | ((prev: number) => number)) => void
handlePreview: () => void
handleBackgroundPreview: () => void
previewLoading: boolean
@@ -62,15 +50,11 @@ type ExamAiGeneratorProps = {
}
export function ExamAiGenerator({
form,
form: _form,
control,
aiProviders,
setAiProviders,
setAiProviders: _setAiProviders,
loadingAiProviders,
providerDialogOpen,
setProviderDialogOpen,
providerDialogKey,
setProviderDialogKey,
handlePreview,
handleBackgroundPreview,
previewLoading,
@@ -104,41 +88,12 @@ export function ExamAiGenerator({
<FormItem>
<div className="flex items-center justify-between gap-2">
<FormLabel>{t("provider.label")}</FormLabel>
<Dialog
open={providerDialogOpen}
onOpenChange={(open) => {
setProviderDialogOpen(open)
if (open) {
setProviderDialogKey((value) => value + 1)
}
}}
>
<DialogTrigger asChild>
<Button type="button" variant="ghost" size="sm" className="h-7 px-2 text-muted-foreground hover:text-foreground">
<Settings className="mr-1 h-3.5 w-3.5" />
{t("provider.manage")}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[960px]">
<DialogHeader>
<DialogTitle>{t("provider.manageTitle")}</DialogTitle>
<DialogDescription>
{t("provider.manageDescription")}
</DialogDescription>
</DialogHeader>
<AiProviderSettingsCard
key={providerDialogKey}
initialMode="new"
onProvidersChanged={(rows) => {
setAiProviders(rows)
const preferred = rows.find((item) => item.isDefault) ?? rows[0]
if (preferred) {
form.setValue("aiProviderId", preferred.id)
}
}}
/>
</DialogContent>
</Dialog>
<Button asChild type="button" variant="ghost" size="sm" className="h-7 px-2 text-muted-foreground hover:text-foreground">
<Link href="/admin/ai-settings">
<Settings className="mr-1 h-3.5 w-3.5" />
{t("provider.manage")}
</Link>
</Button>
</div>
<Select value={field.value} onValueChange={field.onChange} disabled={loadingAiProviders}>
<FormControl>

View File

@@ -25,8 +25,6 @@ export type { ExamFormValues } from "./exam-form-types"
export function ExamForm() {
const router = useRouter()
const [isPending, startTransition] = useTransition()
const [providerDialogOpen, setProviderDialogOpen] = useState(false)
const [providerDialogKey, setProviderDialogKey] = useState(0)
const [subjects, setSubjects] = useState<{ id: string; name: string }[]>([])
const [loadingSubjects, setLoadingSubjects] = useState(true)
const [grades, setGrades] = useState<{ id: string; name: string }[]>([])
@@ -219,10 +217,6 @@ export function ExamForm() {
aiProviders={aiProviders}
setAiProviders={setAiProviders}
loadingAiProviders={loadingAiProviders}
providerDialogOpen={providerDialogOpen}
setProviderDialogOpen={setProviderDialogOpen}
providerDialogKey={providerDialogKey}
setProviderDialogKey={setProviderDialogKey}
handlePreview={preview.handlePreview}
handleBackgroundPreview={preview.handleBackgroundPreview}
previewLoading={preview.previewLoading}