From 5582f168c23cc9b0794fd2f1be975f84c9137560 Mon Sep 17 00:00:00 2001 From: kx_guo Date: Sun, 13 Sep 2026 16:28:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=92=89=E9=92=89=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=87=AA=E5=8A=A8=E5=85=8D=E7=99=BB=E4=B8=8E=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 26 +++++++++++++++++++++++--- src/api/dingtalk.ts | 35 ++++++++++++++++++++++++++++++++--- src/pages/LoginPage.vue | 13 +++++++++---- src/stores/auth.store.ts | 4 ++-- 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/App.vue b/src/App.vue index 1ba91c8..3ae44b8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,6 +2,7 @@ import { onMounted, onUnmounted } from "vue"; import { RouterView } from "vue-router"; +import { isDingTalkContainer } from "@/api/dingtalk"; import router from "@/router"; import { useAuthStore } from "@/stores/auth.store"; @@ -17,9 +18,28 @@ function handleAuthExpired() { } } -onMounted(() => - window.addEventListener("risk-auth-expired", handleAuthExpired), -); +async function syncDingTalkUser() { + if ( + !isDingTalkContainer() || + !auth.user?.dingtalkId || + auth.user.dingtalkId.startsWith("local-") + ) { + return; + } + try { + await auth.dingTalkLogin(true); + } catch { + // A stale token must remain usable if DingTalk is temporarily unavailable. + } +} + +onMounted(async () => { + window.addEventListener("risk-auth-expired", handleAuthExpired); + // Restore /auth/me first: on a hard refresh auth.user starts as null, so a + // synchronous check would skip the DingTalk profile resync entirely. + if (auth.token && !auth.user) await auth.restore(); + await syncDingTalkUser(); +}); onUnmounted(() => window.removeEventListener("risk-auth-expired", handleAuthExpired), ); diff --git a/src/api/dingtalk.ts b/src/api/dingtalk.ts index e9b4824..91aeac6 100644 --- a/src/api/dingtalk.ts +++ b/src/api/dingtalk.ts @@ -3,14 +3,43 @@ import dd from "dingtalk-jsapi"; import { getDingTalkConfig, loginWithDingTalk } from "@/api/auth"; import type { AuthTokenData } from "@/types/auth"; +declare global { + interface Window { + dingtalk?: { platform?: { invokeAPI?: unknown } }; + DingTalkJSBridge?: unknown; + } +} + export function isDingTalkContainer() { - return ( - typeof navigator !== "undefined" && /DingTalk/i.test(navigator.userAgent) + if (typeof window === "undefined" || typeof navigator === "undefined") + return false; + const userAgent = + navigator.userAgent || + (navigator as Navigator & { swuserAgent?: string }).swuserAgent || + ""; + return Boolean( + /DingTalk|dingtalk-win|dd-web/i.test(userAgent) || + window.DingTalkJSBridge || + window.dingtalk?.platform?.invokeAPI || + // DingTalk desktop can expose its container identity through window.name + // even when the embedded browser omits DingTalk from userAgent. + /dingtalk/i.test(window.name || ""), ); } +export async function waitForDingTalkContainer(timeoutMs = 5000) { + if (isDingTalkContainer()) return true; + const startedAt = Date.now(); + while (Date.now() - startedAt < timeoutMs) { + await new Promise((resolve) => window.setTimeout(resolve, 100)); + if (isDingTalkContainer()) return true; + } + return false; +} + export async function authenticateWithDingTalk(): Promise { - if (!isDingTalkContainer()) throw new Error("请从钉钉工作台打开此应用"); + if (!(await waitForDingTalkContainer())) + throw new Error("请从钉钉工作台打开此应用"); const configResponse = await getDingTalkConfig(); const config = configResponse.data.data; if (!config) throw new Error("钉钉免登配置不完整"); diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue index 016be24..1d1152b 100644 --- a/src/pages/LoginPage.vue +++ b/src/pages/LoginPage.vue @@ -9,7 +9,7 @@ import { import { computed, onMounted, ref } from "vue"; import { useRouter } from "vue-router"; -import { isDingTalkContainer } from "@/api/dingtalk"; +import { waitForDingTalkContainer } from "@/api/dingtalk"; import { useAuthStore } from "@/stores/auth.store"; const auth = useAuthStore(); @@ -19,6 +19,7 @@ const useDevelopmentLogin = ref(true); const username = ref("local-admin"); const password = ref("risk-admin-2026"); const autoDingTalkAttempted = ref(false); +const autoDingTalk = ref(false); // 登录后的统一落点是智能对话页,避免从其他受保护页面跳转到登录后 // 又回到原页面,保证用户进入系统后先看到对话工作台。 @@ -49,11 +50,14 @@ async function submitDingTalk() { } } -onMounted(() => { - if (!isDingTalkContainer() || autoDingTalkAttempted.value) return; +onMounted(async () => { + if (autoDingTalkAttempted.value) return; + const inDingTalk = await waitForDingTalkContainer(); + if (!inDingTalk || autoDingTalkAttempted.value) return; autoDingTalkAttempted.value = true; + autoDingTalk.value = true; mode.value = "dingtalk"; - void submitDingTalk(); + await submitDingTalk(); }); @@ -144,6 +148,7 @@ onMounted(() => { }}