diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 23c2212..122553b 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -144,11 +144,20 @@ type NavLink = { isEnabled: boolean; }; +type DingTalkOrganization = { + id: string; + name: string; + appKey: string; + appSecret: string; +}; + type AuthSettings = { - dingTalkAppKey: string | null; - dingTalkAppSecret: string | null; + // 保留单一配置用于兼容旧数据(后端会自动同步为第一个组织) + dingTalkAppKey?: string | null; + dingTalkAppSecret?: string | null; isDingTalkEnabled: boolean; isRegistrationEnabled?: boolean; + dingTalkOrganizations: DingTalkOrganization[]; }; type GeneratedImageRecord = { @@ -266,7 +275,10 @@ export function AdminSettingsClient() { const { data: storageData, mutate: mutateStorage } = useSWR<{ configs: StorageConfig[] }>("/api/admin/storage", fetcher); const { data: siteSettingsData, mutate: mutateSiteSettings } = useSWR("/api/site-settings", fetcher); const { data: navLinksData, mutate: mutateNavLinks } = useSWR<{ links: NavLink[] }>("/api/nav-links", fetcher); - const { data: authSettingsData, mutate: mutateAuthSettings } = useSWR("/api/auth-settings", fetcher); + const { data: authSettingsData, mutate: mutateAuthSettings } = useSWR( + "/api/auth-settings", + fetcher, + ); // 🆕 生图记录数据 const imageQueryParams = new URLSearchParams({ @@ -406,17 +418,36 @@ export function AdminSettingsClient() { dingTalkAppSecret: "", isDingTalkEnabled: false, isRegistrationEnabled: true, + dingTalkOrganizations: [], }); useEffect(() => { - if (authSettingsData) { - setAuthSettings({ - dingTalkAppKey: authSettingsData.dingTalkAppKey || "", - dingTalkAppSecret: authSettingsData.dingTalkAppSecret || "", - isDingTalkEnabled: authSettingsData.isDingTalkEnabled || false, - isRegistrationEnabled: authSettingsData.isRegistrationEnabled ?? true, - }); - } + if (!authSettingsData) return; + + const organizations: DingTalkOrganization[] = + (authSettingsData.dingTalkOrganizations as DingTalkOrganization[] | null) ?? []; + + // 如果没有多组织配置,但有单一 AppKey/Secret,则自动构造一个默认组织 + const normalizedOrgs = + organizations.length > 0 || + !authSettingsData.dingTalkAppKey + ? organizations + : [ + { + id: "default", + name: "默认组织", + appKey: authSettingsData.dingTalkAppKey ?? "", + appSecret: authSettingsData.dingTalkAppSecret ?? "", + }, + ]; + + setAuthSettings({ + dingTalkAppKey: authSettingsData.dingTalkAppKey || "", + dingTalkAppSecret: authSettingsData.dingTalkAppSecret || "", + isDingTalkEnabled: authSettingsData.isDingTalkEnabled || false, + isRegistrationEnabled: authSettingsData.isRegistrationEnabled ?? true, + dingTalkOrganizations: normalizedOrgs, + }); }, [authSettingsData]); // AI 提供商 & 模型状态 @@ -2066,25 +2097,125 @@ export function AdminSettingsClient() {

钉钉登录

-
- - setAuthSettings({ ...authSettings, dingTalkAppKey: e.target.value })} - type="password" - /> -
-
- - setAuthSettings({ ...authSettings, dingTalkAppSecret: e.target.value })} - type="password" - /> +

+ 支持配置多个钉钉组织,每个组织包含名称、AppKey、AppSecret。默认使用第一个组织作为登录入口。 +

+ +
+ {authSettings.dingTalkOrganizations.map((org, index) => ( +
+
+ + { + const value = e.target.value; + setAuthSettings({ + ...authSettings, + dingTalkOrganizations: + authSettings.dingTalkOrganizations.map( + (item, i) => + i === index ? { ...item, name: value } : item, + ), + }); + }} + /> +
+
+ + { + const value = e.target.value; + setAuthSettings({ + ...authSettings, + dingTalkOrganizations: + authSettings.dingTalkOrganizations.map( + (item, i) => + i === index ? { ...item, appKey: value } : item, + ), + }); + }} + /> +
+
+ +
+ { + const value = e.target.value; + setAuthSettings({ + ...authSettings, + dingTalkOrganizations: + authSettings.dingTalkOrganizations.map( + (item, i) => + i === index + ? { ...item, appSecret: value } + : item, + ), + }); + }} + /> + +
+
+
+ ))} + +
-
+ +
(null); const [isDingTalkEnabled, setIsDingTalkEnabled] = useState(false); const [dingTalkAppKey, setDingTalkAppKey] = useState(null); + const [dingTalkOrganizations, setDingTalkOrganizations] = useState([]); const [isRegistrationEnabled, setIsRegistrationEnabled] = useState(true); const [showDingTalkModal, setShowDingTalkModal] = useState(false); const [dingTalkIframeSrc, setDingTalkIframeSrc] = useState(""); + const [showOrgMenu, setShowOrgMenu] = useState(false); const { setTheme, resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); const [isHandlingToken, setIsHandlingToken] = useState(false); @@ -204,7 +206,16 @@ export default function Page() { .then((data) => { if (data.isDingTalkEnabled) setIsDingTalkEnabled(true); if (data.dingTalkAppKey) setDingTalkAppKey(data.dingTalkAppKey); - if (data.isRegistrationEnabled !== undefined) setIsRegistrationEnabled(data.isRegistrationEnabled); + if (Array.isArray(data.organizations)) { + setDingTalkOrganizations( + data.organizations + .map((org: { name?: string }) => org.name) + .filter((name: string | undefined): name is string => !!name), + ); + } + if (data.isRegistrationEnabled !== undefined) { + setIsRegistrationEnabled(data.isRegistrationEnabled); + } }) .catch(console.error); }, []); @@ -269,37 +280,88 @@ export default function Page() { formAction(formData); }; - // 钉钉登录 - 严格按照钉钉配置文档实现 + // 根据组织名称启动钉钉登录(在 iframe 中打开) + const startDingTalkLoginForOrg = (orgName: string) => { + // 清除 URL 中的 token 参数,避免干扰 + if ( + typeof window !== "undefined" && + window.location.search.includes("dingtalk_token") + ) { + const newUrl = window.location.pathname; + window.history.replaceState({}, "", newUrl); + } + + const loginUrl = `/api/auth/dingtalk/login?org=${encodeURIComponent( + orgName, + )}`; + + console.log("[DingTalk] Opening org login:", orgName, loginUrl); + setDingTalkIframeSrc(loginUrl); + setShowDingTalkModal(true); + setShowOrgMenu(false); + }; + + // 钉钉登录 - 支持多组织 const handleDingTalkLogin = () => { - if (!dingTalkAppKey) { - toast({ type: "error", description: "钉钉登录未正确配置" }); + if (!isDingTalkEnabled) { + toast({ type: "error", description: "钉钉登录未启用" }); return; } - // 清除 URL 中的 token 参数,避免干扰 - if (typeof window !== 'undefined' && window.location.search.includes('dingtalk_token')) { - const newUrl = window.location.pathname; - window.history.replaceState({}, '', newUrl); + // 没有配置多组织:回退到单一 appKey 流程 + if (dingTalkOrganizations.length === 0) { + if (!dingTalkAppKey) { + toast({ type: "error", description: "钉钉登录未正确配置" }); + return; + } + + // 兼容:直接使用旧的方式在 iframe 中打开 + if ( + typeof window === "undefined" || + typeof window.location === "undefined" + ) { + toast({ type: "error", description: "当前环境不支持钉钉登录" }); + return; + } + + // 清除 URL 中的 token 参数,避免干扰 + if (window.location.search.includes("dingtalk_token")) { + const newUrl = window.location.pathname; + window.history.replaceState({}, "", newUrl); + } + + const param = { + redirect_uri: encodeURIComponent( + window.location.origin + "/dingtalk-callback.html", + ), + client_id: dingTalkAppKey, + scope: "openid", + response_type: "code", + state: "default", + prompt: "consent", + }; + + const redirectUrl = + "https://login.dingtalk.com/oauth2/auth?" + + Object.keys(param) + .map((key) => key + "=" + (param as any)[key]) + .join("&"); + + console.log("[DingTalk] Opening OAuth URL (legacy):", redirectUrl); + + setDingTalkIframeSrc(redirectUrl); + setShowDingTalkModal(true); + return; } - // 构建钉钉 OAuth2 授权 URL - const param = { - redirect_uri: encodeURIComponent(window.location.origin + "/dingtalk-callback.html"), - client_id: dingTalkAppKey, - scope: "openid", - response_type: "code", - state: "default", - prompt: "consent" - }; - - const redirectUrl = "https://login.dingtalk.com/oauth2/auth?" + - Object.keys(param).map(key => key + "=" + param[key as keyof typeof param]).join("&"); - - console.log("[DingTalk] Opening OAuth URL:", redirectUrl); - - // 在 iframe 中打开钉钉登录页面 - setDingTalkIframeSrc(redirectUrl); - setShowDingTalkModal(true); + // 只有一个组织:直接使用该组织登录 + if (dingTalkOrganizations.length === 1) { + startDingTalkLoginForOrg(dingTalkOrganizations[0] as string); + return; + } + + // 多组织:点击时展开/收起选择菜单(hover 时也会展开) + setShowOrgMenu((prev) => !prev); }; // 关闭钉钉登录弹窗 @@ -347,8 +409,8 @@ export default function Page() { 登录 - {isDingTalkEnabled && dingTalkAppKey && ( -
+ {isDingTalkEnabled && (dingTalkAppKey || dingTalkOrganizations.length > 0) && ( +
@@ -372,6 +434,22 @@ export default function Page() { 钉钉登录 + + {/* 多组织下的小型选择框(悬浮在按钮附近) */} + {dingTalkOrganizations.length > 1 && showOrgMenu && ( +
+ {dingTalkOrganizations.map((org) => ( + + ))} +
+ )}
)} @@ -390,9 +468,9 @@ export default function Page() {
- {/* 钉钉登录 iframe 弹窗 */} + {/* 钉钉登录 iframe 弹窗 / 多组织选择 */} {showDingTalkModal && ( -
{ if (e.target === e.currentTarget) closeDingTalkModal(); @@ -400,20 +478,61 @@ export default function Page() { >
-

钉钉登录

-
-