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:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user