feat(settings,questions,school,textbooks): add brand config, question components, school dialogs, textbooks hooks

settings:

- Add actions-brand, brand-config, data-access-brand for brand management

- Add admin-file-upload-card, admin-notification-config-card, admin-school-info-card, admin-security-policy-card

- Add ai-provider-delete-dialog, ai-provider-selector, brand-config-card

- Add security-recent-logins-section, security-two-factor-section

- Add config/profile-overview-config, data-access-profile-overview, lib/system-settings-utils

questions:

- Add batch-operations, import-export-buttons, knowledge-point-selector, options-editor

- Add question-bank-results-client, question-cascade-filter, question-content-renderer, utils

school:

- Add grade-delete-dialog, grade-form-dialog, grade-list-toolbar, grade-overview-cards

- Add use-grade-data hook

textbooks:

- Add textbook-form-fields component

- Add use-kp-create, use-kp-delete, use-kp-update hooks
This commit is contained in:
SpecialX
2026-07-03 10:26:00 +08:00
parent 138b6f1b00
commit f3c223d914
77 changed files with 5397 additions and 2864 deletions

View File

@@ -0,0 +1,108 @@
"use client"
import { useTranslations } from "next-intl"
import { type ReactElement } from "react"
import { type AiProviderSummary } from "@/modules/settings/actions"
import { Badge } from "@/shared/components/ui/badge"
import { Label } from "@/shared/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
const NEW_PROVIDER_VALUE = "__new__"
interface AiProviderSelectorProps {
/** 已有服务商列表 */
providers: AiProviderSummary[]
/** 当前选中的服务商 ID空字符串表示新建 */
selectedId: string
/** 当前用户 ID用于判断是否为创建者 */
currentUserId?: string
/** 选择变更回调 */
onSelectChange: (value: string) => void
}
/**
* AI 服务商选择器
*
* 负责:
* - 渲染已有服务商下拉选择(含"新建"选项)
* - 展示当前选中服务商的密钥状态与可见性徽章
*
* 不包含任何业务逻辑,仅做展示与事件转发。
*/
export function AiProviderSelector({
providers,
selectedId,
currentUserId,
onSelectChange,
}: AiProviderSelectorProps): ReactElement {
const t = useTranslations("settings.ai.providers")
const selectedProvider = providers.find((item) => item.id === selectedId) ?? null
const renderProviderLabel = (item: AiProviderSummary): string => {
const parts = [item.provider, "·", item.model]
if (item.isDefault) parts.push(`(${t("setDefault")})`)
return parts.join(" ")
}
const renderVisibilityBadge = (item: AiProviderSummary): ReactElement => {
const isOwner = currentUserId !== undefined && item.createdBy === currentUserId
return (
<span className="ml-2 inline-flex gap-1">
{item.visibility === "public" ? (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4">
{t("badgePublic")}
</Badge>
) : (
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4">
{t("badgePrivate")}
</Badge>
)}
{isOwner ? (
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 text-muted-foreground">
{t("badgeOwner")}
</Badge>
) : null}
</span>
)
}
return (
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t("existing")}</Label>
<Select value={selectedId || NEW_PROVIDER_VALUE} onValueChange={onSelectChange}>
<SelectTrigger>
<SelectValue placeholder={t("selectPlaceholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={NEW_PROVIDER_VALUE}>{t("createNew")}</SelectItem>
{providers.map((item) => (
<SelectItem key={item.id} value={item.id}>
{renderProviderLabel(item)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>{t("keyStatus")}</Label>
<div className="flex items-center rounded-md border px-3 py-2 text-sm text-muted-foreground">
{selectedProvider ? renderVisibilityBadge(selectedProvider) : null}
<span className="ml-auto">
{selectedProvider?.apiKeyLast4
? `${t("stored")} • ****${selectedProvider.apiKeyLast4}`
: t("noKey")}
</span>
</div>
</div>
</div>
)
}