refactor(modules): update classes, course-plans, diagnostic, questions, settings, student, layout

- Update classes data-access (invitations, main) for invitation management

- Update course-plans actions, data-access, and types

- Update diagnostic data-access for report queries

- Update questions data-access for question bank queries

- Update settings actions, ai-provider-settings-card, data-access, and types

- Update student course-filters, student-courses-view, student-schedule-filters, student-schedule-view

- Update layout app-sidebar, site-header, and navigation config
This commit is contained in:
SpecialX
2026-06-24 12:03:35 +08:00
parent c9e46f9f80
commit 8c2fe14c20
18 changed files with 712 additions and 189 deletions

View File

@@ -1,6 +1,6 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react"
import { useCallback, useEffect, useMemo, useRef, useState, useTransition, type ReactElement } from "react"
import { useTranslations } from "next-intl"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
@@ -26,6 +26,7 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -39,9 +40,11 @@ import {
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { Badge } from "@/shared/components/ui/badge"
import { deleteAiProviderAction, getAiProviderSummaries, testAiProviderAction, upsertAiProviderAction, type AiProviderSummary } from "@/modules/settings/actions"
const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom"])
const VisibilitySchema = z.enum(["public", "private"])
const AiProviderFormSchema = z.object({
id: z.string().optional(),
@@ -50,19 +53,26 @@ const AiProviderFormSchema = z.object({
model: z.string().min(1, "Model is required"),
apiKey: z.string().optional(),
isDefault: z.boolean().optional(),
visibility: VisibilitySchema.optional(),
})
type AiProviderFormValues = z.infer<typeof AiProviderFormSchema>
const NEW_PROVIDER_VALUE = "__new__"
type AiProviderSettingsCardProps = {
onProvidersChanged?: (rows: AiProviderSummary[]) => void
initialMode?: "new" | "first"
isAdmin?: boolean
currentUserId?: string
}
export function AiProviderSettingsCard({
onProvidersChanged,
initialMode = "first",
}: {
onProvidersChanged?: (rows: AiProviderSummary[]) => void
initialMode?: "new" | "first"
}) {
isAdmin = false,
currentUserId,
}: AiProviderSettingsCardProps) {
const t = useTranslations("settings.ai.providers")
const [isPending, startTransition] = useTransition()
const [providers, setProviders] = useState<AiProviderSummary[]>([])
@@ -80,6 +90,7 @@ export function AiProviderSettingsCard({
model: "",
apiKey: "",
isDefault: false,
visibility: "private",
},
})
@@ -108,6 +119,7 @@ export function AiProviderSettingsCard({
model: "",
apiKey: "",
isDefault: false,
visibility: "private",
})
}, [form])
@@ -138,6 +150,7 @@ export function AiProviderSettingsCard({
model: next.model,
apiKey: "",
isDefault: next.isDefault,
visibility: next.visibility,
})
}
} catch {
@@ -163,6 +176,7 @@ export function AiProviderSettingsCard({
model: next.model,
apiKey: "",
isDefault: next.isDefault,
visibility: next.visibility,
})
}
@@ -193,6 +207,7 @@ export function AiProviderSettingsCard({
model: values.model.trim(),
apiKey: apiKey || undefined,
isDefault: values.isDefault ?? false,
visibility: values.visibility,
}
const result = await testAiProviderAction(payload)
if (result.success) {
@@ -220,6 +235,7 @@ export function AiProviderSettingsCard({
model: values.model.trim(),
apiKey: values.apiKey?.trim() || undefined,
isDefault: values.isDefault ?? false,
visibility: values.visibility,
}
const result = await upsertAiProviderAction(payload)
if (result.success) {
@@ -245,6 +261,7 @@ export function AiProviderSettingsCard({
model: next.model,
apiKey: "",
isDefault: next.isDefault,
visibility: next.visibility,
})
}
} else {
@@ -278,6 +295,7 @@ export function AiProviderSettingsCard({
model: next.model,
apiKey: "",
isDefault: next.isDefault,
visibility: next.visibility,
})
} else {
resetToNew()
@@ -291,6 +309,34 @@ export function AiProviderSettingsCard({
})
}
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 (
<Card>
<CardHeader>
@@ -312,7 +358,7 @@ export function AiProviderSettingsCard({
<SelectItem value={NEW_PROVIDER_VALUE}>{t("createNew")}</SelectItem>
{providers.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.provider} · {item.model}
{renderProviderLabel(item)}
</SelectItem>
))}
</SelectContent>
@@ -320,10 +366,13 @@ export function AiProviderSettingsCard({
</div>
<div className="space-y-2">
<Label>{t("keyStatus")}</Label>
<div className="rounded-md border px-3 py-2 text-sm text-muted-foreground">
{selectedProvider?.apiKeyLast4
? `${t("stored")} • ****${selectedProvider.apiKeyLast4}`
: t("noKey")}
<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>
@@ -374,6 +423,36 @@ export function AiProviderSettingsCard({
/>
</div>
<FormField
control={form.control}
name="visibility"
render={({ field }) => (
<FormItem>
<FormLabel>{t("visibility")}</FormLabel>
<FormControl>
<Select
value={field.value ?? "private"}
onValueChange={field.onChange}
disabled={!isAdmin}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="private">{t("visibilityPrivateLabel")}</SelectItem>
{isAdmin ? (
<SelectItem value="public">{t("visibilityPublicLabel")}</SelectItem>
) : null}
</SelectContent>
</Select>
</FormControl>
<FormDescription>
{isAdmin ? t("visibilityDesc") : t("visibilityReadOnly")}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="isDefault"