diff --git a/app/(app)/app/settings/global/page.tsx b/app/(app)/app/settings/global/page.tsx index 82be15d..25c6fb1 100644 --- a/app/(app)/app/settings/global/page.tsx +++ b/app/(app)/app/settings/global/page.tsx @@ -1,6 +1,5 @@ import { redirect } from "next/navigation"; -import { Badge } from "@/components/ui/badge"; import { getCurrentAuthContext } from "@/modules/core/server/auth"; import { hasPermission } from "@/modules/core/server/permissions"; import { getSystemSettings } from "@/modules/core/server/system-settings"; @@ -20,12 +19,6 @@ export default async function GlobalSettingsPage(): Promise return (
-
- Global -

全局配置

-

平台级注册策略、OIDC 默认配置和用户头像声明映射。

-
-
); diff --git a/app/globals.css b/app/globals.css index 895ae1b..3f98922 100644 --- a/app/globals.css +++ b/app/globals.css @@ -124,17 +124,6 @@ a, } } -@keyframes dialog-panel-enter { - from { - opacity: 0; - transform: translate(-50%, -48%) scale(0.97); - } - to { - opacity: 1; - transform: translate(-50%, -50%) scale(1); - } -} - .page-enter { animation: page-enter 180ms ease-out both; } @@ -147,10 +136,6 @@ a, animation: page-enter 160ms ease-out both; } -.dialog-panel-enter { - animation: dialog-panel-enter 160ms ease-out both; -} - :where([role="presentation"].fixed.inset-0.bg-kumo-recessed) { z-index: 40; } @@ -380,6 +365,22 @@ a, font-weight: 600; } +.teatea-checkbox { + display: inline-flex; + width: 100%; + align-items: flex-start; + gap: 0.75rem; +} + +.teatea-checkbox :where([data-kumo-component="Checkbox"]), +.teatea-checkbox-control { + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + min-height: 1.25rem; + flex-shrink: 0; +} + .teatea-checkbox :where([data-kumo-component="Checkbox"]) { border: 1px solid var(--input); border-radius: var(--radius-sm); @@ -387,6 +388,12 @@ a, color: var(--primary-foreground); } +.teatea-checkbox :where([data-kumo-component="Checkbox"][data-checked]), +.teatea-checkbox :where([data-kumo-component="Checkbox"][aria-checked="true"]) { + border-color: var(--primary); + background: var(--primary); +} + .teatea-badge { border-radius: var(--radius-sm); font-weight: 600; @@ -430,11 +437,9 @@ a, inset: 0 !important; height: fit-content; margin: auto !important; - max-width: calc(100vw - 1.5rem); - min-width: min(calc(100vw - 1.5rem), 48rem) !important; + min-width: 0 !important; transform: none !important; translate: 0 0 !important; - width: min(calc(100vw - 1.5rem), 48rem) !important; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--card); @@ -453,7 +458,6 @@ a, .page-enter, .route-transition, .table-row-enter, - .dialog-panel-enter, .teatea-select-panel, .loading-progress { animation: none; diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx index d5b0108..fc6cb89 100644 --- a/components/ui/checkbox.tsx +++ b/components/ui/checkbox.tsx @@ -12,20 +12,27 @@ type CheckboxProps = Omit & { }; export function Checkbox({ + "aria-label": ariaLabel, + "aria-labelledby": ariaLabelledBy, checked, className, defaultChecked = false, + label, name, onCheckedChange, ...props }: CheckboxProps): React.ReactElement { + const generatedId = React.useId().replace(/:/g, ""); + const labelId = label ? `teatea-checkbox-label-${generatedId}` : undefined; const [uncontrolledChecked, setUncontrolledChecked] = React.useState(defaultChecked); const isControlled = checked !== undefined; const currentChecked = isControlled ? checked : uncontrolledChecked; return ( - + { @@ -37,6 +44,11 @@ export function Checkbox({ }} {...props} /> + {label ? ( + + {label} + + ) : null} {name && currentChecked ? : null} ); diff --git a/modules/settings/components/GlobalSettingsClient.tsx b/modules/settings/components/GlobalSettingsClient.tsx index 2aafabc..277b305 100644 --- a/modules/settings/components/GlobalSettingsClient.tsx +++ b/modules/settings/components/GlobalSettingsClient.tsx @@ -16,8 +16,19 @@ type GlobalSettingsClientProps = { settings: SystemSettings; }; +type SettingsTab = "registration" | "oidc"; + +const tabs: Array<{ label: string; value: SettingsTab }> = [ + { label: "注册入口", value: "registration" }, + { label: "OIDC 默认配置", value: "oidc" }, +]; + export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): React.ReactElement { const router = useRouter(); + const [activeTab, setActiveTab] = useState("registration"); + const [registrationEnabled, setRegistrationEnabled] = useState(settings.registrationEnabled); + const [oidcEnabled, setOidcEnabled] = useState(settings.oidcEnabled); + const [oidcAutoProvision, setOidcAutoProvision] = useState(settings.oidcAutoProvision); const [message, setMessage] = useState(""); const [isPending, setIsPending] = useState(false); @@ -31,15 +42,15 @@ export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): R method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ - registrationEnabled: formData.get("registrationEnabled") === "on", - oidcEnabled: formData.get("oidcEnabled") === "on", + registrationEnabled, + oidcEnabled, oidcIssuerUrl: String(formData.get("oidcIssuerUrl") ?? ""), oidcClientId: String(formData.get("oidcClientId") ?? ""), oidcClientSecret: String(formData.get("oidcClientSecret") ?? ""), oidcScopes: String(formData.get("oidcScopes") ?? ""), oidcRedirectUri: String(formData.get("oidcRedirectUri") ?? ""), oidcAvatarClaim: String(formData.get("oidcAvatarClaim") ?? ""), - oidcAutoProvision: formData.get("oidcAutoProvision") === "on", + oidcAutoProvision, }), }); const result = (await response.json()) as ApiResult<{ settings: SystemSettings }>; @@ -55,115 +66,131 @@ export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): R } return ( -
- - -
-
- 控制全站公开注册能力和 OIDC 登录入口总开关。 -
- -
- - 开放公开注册 - 关闭后普通注册不可用;邀请链接仍可使用。 - - } - name="registrationEnabled" - /> -
- -
- - 启用 OIDC 登录 - 作为平台级总开关,机构级 OIDC 配置仍可单独管理。 - - } - name="oidcEnabled" - /> -
- -
- - OIDC 自动开户 - 首次 OIDC 登录时按声明信息创建账号,默认进入等待分配。 - - } - name="oidcAutoProvision" - /> -
-
-
+ +
+ {tabs.map((tab) => ( + + ))} +
- -
-
- 保存标准 OIDC issuer、client、scope 和头像 claim。 -
- -
+ + - +
+ + +
-
- - -
- - - - {message ? ( -

- {message} -

- ) : null} - -
- - {settings.oidcEnabled ? "OIDC 已启用" : "OIDC 未启用"} - - -
- + + +
+ + {message ? ( +

+ {message} +

+ ) : null} + +
+ + {oidcEnabled ? "OIDC 已启用" : "OIDC 未启用"} + + +
); }