From 3b4596315737a0c5d64833a6ecd9e0c0a14a9f71 Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Thu, 7 May 2026 12:09:37 +0800
Subject: [PATCH 01/13] ..
---
.env | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.env b/.env
index 235df28..added0b 100644
--- a/.env
+++ b/.env
@@ -27,8 +27,8 @@ VITE_APP_DEV_SERVICE = http://127.0.0.1:9999 # 邓-本地
# VITE_IMAGE_DEV_PATH = https://stgl.pj.gov.cn/api # 域名线上
#VITE_IMAGE_DEV_PATH = http://59.202.170.152:81/api # 信创线上
# VITE_IMAGE_DEV_PATH = http://192.168.101.174:9999 # 邓-本地
-#VITE_IMAGE_DEV_PATH = http://127.0.0.1:9999
-VITE_IMAGE_DEV_PATH = http://123.157.250.162:8089/api # 测试线上
+VITE_IMAGE_DEV_PATH = http://127.0.0.1:9999
+#VITE_IMAGE_DEV_PATH = http://123.157.250.162:8089/api # 测试线上
# --------------------------- 生产环境 api地址 ----------------------------------
VITE_APP_PROD_SERVICE = https://stgl.pj.gov.cn/api # 域名线上
From b8d0379467166de99442eaa7390e3fe20a6484b4 Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Fri, 8 May 2026 22:24:46 +0800
Subject: [PATCH 02/13] zzd
---
package.json | 1 +
src/App.vue | 8 ++
src/env.d.ts | 5 +
src/pages.json | 7 ++
src/pages/tabbar/index.vue | 5 +-
src/pages/tabbar/my.vue | 2 +-
src/pages/user/zzdSmsVerify.vue | 106 ++++++++++++++++++
src/sheep/api/user.ts | 19 +++-
src/sheep/utils/app.ts | 3 +
src/sheep/utils/dingAuthCode.ts | 188 ++++++++++++++++++++++++++++++++
src/sheep/utils/loadScript.ts | 10 ++
src/sheep/utils/request.ts | 76 +++++++++++--
12 files changed, 417 insertions(+), 13 deletions(-)
create mode 100644 src/pages/user/zzdSmsVerify.vue
create mode 100644 src/sheep/utils/dingAuthCode.ts
create mode 100644 src/sheep/utils/loadScript.ts
diff --git a/package.json b/package.json
index c9270c2..9b3772a 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"@zebra-ui/swiper": "^2.2.8",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
+ "gdt-jsapi": "^1.9.55-beta.14",
"luch-request": "^3.1.1",
"pinia": "^2.2.6",
"qrcode": "^1.5.4",
diff --git a/src/App.vue b/src/App.vue
index 2511212..9b24feb 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -164,6 +164,14 @@ const setTabbar = (launchParam?: string, auth?: string) => {
}
export default {
+ async onLaunch() {
+ try {
+ const { tryZzdEntryLogin } = await import("@/sheep/utils/request")
+ await tryZzdEntryLogin()
+ } catch (e) {
+ console.warn("[App.onLaunch] tryZzdEntryLogin", e)
+ }
+ },
methods: {
async _initTabbar() {
const launchParam = getLaunchParam()
diff --git a/src/env.d.ts b/src/env.d.ts
index cffc168..d3e9c77 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -1,5 +1,10 @@
///
+declare module "gdt-jsapi" {
+ const dd: { getAuthCode: (opts: { corpId?: string }) => Promise }
+ export default dd
+}
+
declare module "*.vue" {
import { DefineComponent } from "vue"
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
diff --git a/src/pages.json b/src/pages.json
index fcbd993..5b7b364 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -383,6 +383,13 @@
"navigationBarTitleText": "用户身份",
"disableScroll": true
}
+ },
+ {
+ "path": "zzdSmsVerify",
+ "style": {
+ "navigationBarTitleText": "手机号核验",
+ "disableScroll": true
+ }
}
]
},
diff --git a/src/pages/tabbar/index.vue b/src/pages/tabbar/index.vue
index 7f7d1ef..7f33c2d 100644
--- a/src/pages/tabbar/index.vue
+++ b/src/pages/tabbar/index.vue
@@ -69,8 +69,11 @@ const jumpByParam = async () => {
const appInfo = await getAppInfo() // 环境判断
const launchParam = getLaunchParam() // 获取启动参数
- // 浙政钉环境
+ // 浙政钉环境(诗画不走此分支)
if (appInfo == APPENV.ZZD) {
+ const { tryZzdEntryLogin } = await import("@/sheep/utils/request")
+ const zzdR = await tryZzdEntryLogin()
+ if (zzdR !== "ok") return
switch (launchParam) {
case "canteen": // 食堂
uni.setStorageSync("launchParam", launchParam)
diff --git a/src/pages/tabbar/my.vue b/src/pages/tabbar/my.vue
index 8c2c3b5..9141a88 100644
--- a/src/pages/tabbar/my.vue
+++ b/src/pages/tabbar/my.vue
@@ -242,7 +242,7 @@ const login = {
if (isEmpty(tokenStorage) || isEmpty(userStorage)) {
try {
uni.showLoading({ title: "登录中...", mask: true })
- const res = await userApi.auth(token)
+ const res = await userApi.auth({ token })
if (res.code == 0) {
storage.saveToken(res.data)
diff --git a/src/pages/user/zzdSmsVerify.vue b/src/pages/user/zzdSmsVerify.vue
new file mode 100644
index 0000000..6865182
--- /dev/null
+++ b/src/pages/user/zzdSmsVerify.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+ 浙政钉用户:{{ displayName }}
+
+
+
+
+
+
+
+
+
+
+
+ 获取验证码
+
+
+ 核验并进入应用
+
+
+
+
+
diff --git a/src/sheep/api/user.ts b/src/sheep/api/user.ts
index e1d8de8..4cfb042 100644
--- a/src/sheep/api/user.ts
+++ b/src/sheep/api/user.ts
@@ -12,12 +12,25 @@ export default {
method: "GET",
}),
- // 浙政钉授权登录
- auth: (token: string): Promise =>
+ // 浙政钉授权登录(token:旧 uabaseToken;authCode:开放平台 H5 getAuthCode)
+ auth: (payload: { token?: string; authCode?: string }): Promise =>
request({
url: "/thirdparty/thirdPartyOAth2/zzdsso",
method: "POST",
- data: { token },
+ data: payload,
+ }),
+
+ zzdLoginSms: (mobilePhone: string): Promise =>
+ request({
+ url: `/thirdparty/thirdPartyOAth2/loginSms?mobilePhone=${encodeURIComponent(mobilePhone.trim())}`,
+ method: "POST",
+ }),
+
+ zzdSsoConfirmPhone: (data: { bindSessionId: string; mobilePhone: string; smsCode: string }): Promise =>
+ request({
+ url: "/thirdparty/thirdPartyOAth2/zzdsso/confirmPhone",
+ method: "POST",
+ data,
}),
// 诗画授权登录
diff --git a/src/sheep/utils/app.ts b/src/sheep/utils/app.ts
index 051bf54..739bd6b 100644
--- a/src/sheep/utils/app.ts
+++ b/src/sheep/utils/app.ts
@@ -12,6 +12,9 @@ export const getLaunchParam = (): string => {
// 检测app信息判断运行环境
export const getAppInfo = async (): Promise => {
+ if (import.meta.env.VITE_DEV_FORCE_ZZD === "true") {
+ return APPENV.ZZD
+ }
try {
const res = await (window as any).CP2.getAppInfo()
// 浙政钉环境
diff --git a/src/sheep/utils/dingAuthCode.ts b/src/sheep/utils/dingAuthCode.ts
new file mode 100644
index 0000000..1deffbe
--- /dev/null
+++ b/src/sheep/utils/dingAuthCode.ts
@@ -0,0 +1,188 @@
+import { loadScript } from "./loadScript"
+
+const JSAPI_LOAD_TIMEOUT_MS = 45000
+/** 仅在不注入 dd 的环境(如普通浏览器)按需拉取;浙政钉内通常自带 window.dd */
+const DEFAULT_OPEN_JS_URL = "https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.12/dingtalk.open.js"
+/** 微应用需 dd.config 完成后 ready 才会回调;超时则不再盲调 requestAuthCode */
+const DD_READY_TIMEOUT_MS = 30000
+
+let ddErrorCallbackRegistered = false
+
+declare global {
+ interface Window {
+ dd?: {
+ env?: { platform?: string }
+ /** 政务钉钉 / 专有钉钉:JSAPI 鉴权(ticket 来自开放平台 get_jsapi_token) */
+ authConfig?: (opts: { ticket: string; jsApiList: string[] }) => Promise
+ config?: (opts: {
+ agentId: string
+ corpId: string
+ timeStamp: string
+ nonceStr: string
+ signature: string
+ type?: number
+ jsApiList: string[]
+ }) => void
+ /** 专有钉钉 / 浙政钉文档:获取登录授权码(Promise),移动端返回 code,PC 返回 auth_code */
+ getAuthCode?: (opts: { corpId?: string }) => Promise<{
+ code?: string
+ auth_code?: string
+ expiresln?: number
+ expires_in?: number
+ }>
+ ready: (cb: () => void) => void
+ /** 鉴权/配置失败时回调,便于排查未触发取码的原因 */
+ error?: (cb: (err: unknown) => void) => void
+ runtime: {
+ permission: {
+ requestAuthCode: (opts: {
+ corpId: string
+ onSuccess: (info: { code: string }) => void
+ onFail?: (err: unknown) => void
+ }) => void
+ }
+ }
+ }
+ }
+}
+
+function registerDdErrorOnce() {
+ const dd = window.dd
+ if (!dd || ddErrorCallbackRegistered || typeof dd.error !== "function") return
+ ddErrorCallbackRegistered = true
+ try {
+ dd.error((err: unknown) => {
+ console.error("[dd.error]", typeof err === "string" ? err : err)
+ })
+ } catch {
+ /* ignore */
+ }
+}
+
+function injectWaitMs(): number {
+ const v = import.meta.env.VITE_DD_INJECT_WAIT_MS
+ if (v === undefined || v === "") return 5000
+ const n = parseInt(String(v), 10)
+ return Number.isFinite(n) && n >= 0 ? n : 5000
+}
+
+/** 部分 IRS/扫码场景下容器晚于首屏注入 dd,短轮询避免误判 */
+function waitForNativeDd(maxMs: number): Promise {
+ if (maxMs <= 0) return Promise.resolve(!!window.dd)
+ if (window.dd) return Promise.resolve(true)
+ return new Promise((resolve) => {
+ const t0 = Date.now()
+ const id = setInterval(() => {
+ if (window.dd) {
+ clearInterval(id)
+ resolve(true)
+ } else if (Date.now() - t0 >= maxMs) {
+ clearInterval(id)
+ resolve(false)
+ }
+ }, 80)
+ })
+}
+
+/**
+ * 浙政钉 / 钉钉容器内需在 dd.ready 之后再调 getAuthCode / requestAuthCode,否则 Android 上常见无回调。
+ */
+export function ensureDdReady(): Promise {
+ return new Promise((resolve, reject) => {
+ const dd = window.dd
+ if (!dd) {
+ reject(new Error("window.dd 不存在"))
+ return
+ }
+ if (typeof dd.ready !== "function") {
+ resolve()
+ return
+ }
+ let settled = false
+ const to = setTimeout(() => {
+ if (settled) return
+ settled = true
+ const msg = `dd.ready 超时(${DD_READY_TIMEOUT_MS / 1000}s)。完整单点需先 dd.authConfig;或调试时跳过 ready 直接 getAuthCode。`
+ reject(new Error(msg))
+ }, DD_READY_TIMEOUT_MS)
+ try {
+ dd.ready(() => {
+ if (settled) return
+ settled = true
+ clearTimeout(to)
+ resolve()
+ })
+ } catch (e) {
+ clearTimeout(to)
+ if (!settled) {
+ settled = true
+ reject(e instanceof Error ? e : new Error(String(e)))
+ }
+ }
+ })
+}
+
+/**
+ * 1) 优先用容器自带的 `window.dd`(浙政钉内嵌常见)。
+ * 2) 若首屏没有 dd:先等待 `VITE_DD_INJECT_WAIT_MS`(默认 5s)以兼容延迟注入。
+ * 3) 仍没有时:默认自动拉 g.alicdn open.js(IRS 测试、扫码开系统浏览器等);纯内嵌且绝不拉脚本可设 `VITE_DD_NO_OPEN_JS_FALLBACK=true`。
+ * 4) 显式 `VITE_DD_LOAD_OPEN_JS=true` 时跳过等待立即拉脚本(本地 Chrome 调试可配合 .env.development)。
+ */
+export async function loadDingTalkOpenJsapi() {
+ if (window.dd) {
+ registerDdErrorOnce()
+ return
+ }
+ const forceScript = import.meta.env.VITE_DD_LOAD_OPEN_JS === "true"
+ const noFallback = import.meta.env.VITE_DD_NO_OPEN_JS_FALLBACK === "true"
+ if (!forceScript) {
+ const waitMs = injectWaitMs()
+ if (waitMs > 0) {
+ const ok = await waitForNativeDd(waitMs)
+ if (ok) {
+ registerDdErrorOnce()
+ return
+ }
+ }
+ if (noFallback) {
+ const msg =
+ "等待后仍无 window.dd。请确认链接在浙政钉内置 WebView 打开;或去掉 VITE_DD_NO_OPEN_JS_FALLBACK、或设 VITE_DD_LOAD_OPEN_JS=true"
+ throw new Error(msg)
+ }
+ }
+ const scriptUrl = (import.meta.env.VITE_DD_JSAPI_URL || "").trim() || DEFAULT_OPEN_JS_URL
+ await Promise.race([
+ loadScript(scriptUrl),
+ new Promise((_, rej) =>
+ setTimeout(
+ () => rej(new Error(`钉钉 JSAPI 脚本加载超时(${JSAPI_LOAD_TIMEOUT_MS / 1000}s)`)),
+ JSAPI_LOAD_TIMEOUT_MS,
+ ),
+ ),
+ ])
+ if (!window.dd) {
+ throw new Error("脚本已执行但 window.dd 仍不存在")
+ }
+ registerDdErrorOnce()
+}
+
+/** gdt-jsapi 容器返回:code / auth_code / authCode(与 zzdhome `src/App.vue` 中 `authCodeFromGetAuthResult` 一致) */
+export function authCodeFromGdtResult(res: unknown): string | null {
+ if (!res || typeof res !== "object") return null
+ const o = res as Record
+ const c = o.code ?? o.auth_code ?? o.authCode
+ if (typeof c === "string" && c.trim()) return c.trim()
+ return null
+}
+
+/**
+ * 与 zzdhome `App.vue` 的 `runZzdLogin` 一致:`loadDingTalkOpenJsapi` 后使用 `gdt-jsapi` 的 `getAuthCode`,不再单独用 window.dd 包一层 Promise。
+ */
+export async function requestZzdAuthCodeViaGdt(corpId: string): Promise {
+ const dd = (await import("gdt-jsapi")).default
+ await loadDingTalkOpenJsapi()
+ const res = await dd.getAuthCode({ corpId: (corpId || "").trim() })
+ const code = authCodeFromGdtResult(res)
+ if (!code) throw new Error("getAuthCode 未返回有效 code(请核对 gdt 返回字段)")
+ return code
+}
diff --git a/src/sheep/utils/loadScript.ts b/src/sheep/utils/loadScript.ts
new file mode 100644
index 0000000..a2c20c4
--- /dev/null
+++ b/src/sheep/utils/loadScript.ts
@@ -0,0 +1,10 @@
+export function loadScript(src: string): Promise {
+ return new Promise((resolve, reject) => {
+ const el = document.createElement("script")
+ el.src = src
+ el.async = true
+ el.onload = () => resolve()
+ el.onerror = () => reject(new Error(`脚本加载失败: ${src}`))
+ document.head.appendChild(el)
+ })
+}
diff --git a/src/sheep/utils/request.ts b/src/sheep/utils/request.ts
index 63e1bf2..3a32012 100644
--- a/src/sheep/utils/request.ts
+++ b/src/sheep/utils/request.ts
@@ -219,39 +219,99 @@ const autoLogin = async (): Promise => {
return false
}
-// 浙政钉登录
-async function loginByZzd(): Promise {
+function isZzdNeedSmsVerify(data: unknown): data is { needSmsVerify: true; bindSessionId: string; displayName?: string } {
+ if (!data || typeof data !== "object") return false
+ const o = data as Record
+ return o.needSmsVerify === true && typeof o.bindSessionId === "string"
+}
+
+function goZzdSmsVerify(data: { bindSessionId: string; displayName?: string }) {
+ const name = typeof data.displayName === "string" ? data.displayName : ""
+ const q = `bindSessionId=${encodeURIComponent(data.bindSessionId)}&displayName=${encodeURIComponent(name)}`
+ uni.reLaunch({ url: `/pages/user/zzdSmsVerify?${q}` })
+}
+
+/**
+ * 浙政钉专用:校验本地 token → 无效则 getAuthCode→zzdsso → 需绑手机则去短信页 → 否则换 token。
+ * @returns ok 已可带 token 请求;sms 已跳转核验页;failed 未登录
+ */
+async function zzdLoginCore(): Promise<"ok" | "sms" | "failed"> {
try {
uni.showLoading({ title: "登录中...", mask: true })
- const token = new URL(window.location.href).searchParams.get("uabaseToken")
+ const tokenStorage = uni.getStorageSync(TOKEN_KEY)
+ if (!isEmpty(tokenStorage) && !isTokenExpires(tokenStorage) && getTokenValue(tokenStorage)) {
+ uni.hideLoading()
+ return "ok"
+ }
+
+ if (typeof window !== "undefined") {
+ try {
+ const { requestZzdAuthCodeViaGdt } = await import("./dingAuthCode")
+ const corpId = (import.meta.env.VITE_ZZD_CORP_ID || "").trim()
+ const authCode = await requestZzdAuthCodeViaGdt(corpId)
+ const res: any = await userApi.auth({ authCode })
+ if (res?.code == 0) {
+ if (isZzdNeedSmsVerify(res.data)) {
+ goZzdSmsVerify(res.data)
+ return "sms"
+ }
+ uni.setStorageSync(TOKEN_KEY, res.data)
+ const { data: userData } = await userApi.userInfo()
+ uni.setStorageSync(USER_INFO_KEY, userData)
+ return "ok"
+ }
+ if (res?.msg) {
+ uni.showToast({ title: res.msg, icon: "none", duration: 4000 })
+ }
+ } catch (e) {
+ console.warn("[loginByZzd] authCode 登录未成功,尝试 uabaseToken", e)
+ }
+ }
+
+ const token =
+ typeof window !== "undefined" ? new URL(window.location.href).searchParams.get("uabaseToken") : null
if (!token) {
uni.showToast({ title: "用户凭证已失效,请重新进入智慧后勤应用", icon: "none", duration: 6000 })
- return false
+ return "failed"
}
- const res = await userApi.auth(token)
+ const res = await userApi.auth({ token })
if (res?.code == 0) {
uni.setStorageSync(TOKEN_KEY, res.data)
const { data: userData } = await userApi.userInfo()
uni.setStorageSync(USER_INFO_KEY, userData)
- return true
+ return "ok"
} else {
uni.removeStorageSync(TOKEN_KEY)
uni.removeStorageSync(USER_INFO_KEY)
if (res.msg == "token已失效") {
uni.showToast({ title: "用户凭证已失效,请退出并重新进入智慧后勤应用", icon: "none", duration: 6000 })
}
- return false
+ return "failed"
}
} catch (error: any) {
uni.showToast({ title: error, icon: "none" })
- return false
+ return "failed"
} finally {
uni.hideLoading()
}
}
+async function loginByZzd(): Promise {
+ const r = await zzdLoginCore()
+ return r === "ok"
+}
+
+/**
+ * 从门户/常用入口进入 H5 时主动跑浙政钉单点。非浙政钉环境立即返回 ok,不调用 zzdsso、不碰诗画逻辑。
+ */
+export async function tryZzdEntryLogin(): Promise<"ok" | "sms" | "failed"> {
+ const appInfo = await getAppInfo()
+ if (appInfo !== APPENV.ZZD) return "ok"
+ return await zzdLoginCore()
+}
+
// 诗画登录
async function loginBySh(): Promise {
try {
From 22e0aaf610aa57f71300904793a2a592ad364da6 Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Fri, 8 May 2026 22:36:14 +0800
Subject: [PATCH 03/13] zzd
---
src/sheep/utils/app.ts | 17 ++++++++++++++++-
src/sheep/utils/request.ts | 5 ++---
2 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/src/sheep/utils/app.ts b/src/sheep/utils/app.ts
index 739bd6b..f2db66f 100644
--- a/src/sheep/utils/app.ts
+++ b/src/sheep/utils/app.ts
@@ -5,9 +5,24 @@ import { APPENV } from "../constant"
// export default () => import.meta.env.VITE_DEV_ENV === "dev"
export const isDevEnv = (): boolean => process.env.NODE_ENV === "development"
+/**
+ * 读取当前页 URL 查询参数(H5)。兼容 hash 路由:`/h5/#/?launchParam=canteen` 中参数在 hash 段,不在 location.search。
+ */
+export const getUrlQueryParam = (name: string): string => {
+ if (typeof window === "undefined") return ""
+ const u = new URL(window.location.href)
+ const fromSearch = u.searchParams.get(name)
+ if (fromSearch) return fromSearch
+ const hash = window.location.hash || ""
+ const qi = hash.indexOf("?")
+ if (qi < 0) return ""
+ const fromHash = new URLSearchParams(hash.slice(qi + 1)).get(name)
+ return fromHash || ""
+}
+
// 获取启动参数
export const getLaunchParam = (): string => {
- return new URL(window.location.href).searchParams.get("launchParam") || uni.getStorageSync("launchParam") || ""
+ return getUrlQueryParam("launchParam") || uni.getStorageSync("launchParam") || ""
}
// 检测app信息判断运行环境
diff --git a/src/sheep/utils/request.ts b/src/sheep/utils/request.ts
index 3a32012..7f8289d 100644
--- a/src/sheep/utils/request.ts
+++ b/src/sheep/utils/request.ts
@@ -1,7 +1,7 @@
import Request from "luch-request"
import { router, constant } from "@@/index"
import { isEmpty } from "radash"
-import { getAppInfo, isDevEnv } from "@@/utils/app"
+import { getAppInfo, getUrlQueryParam, isDevEnv } from "@@/utils/app"
import { userApi } from "../api"
import showVerifyDeviceImpModel from "../components/verifyDeviceImpModel/verifyDeviceImpModel"
import { APPENV } from "../constant"
@@ -268,8 +268,7 @@ async function zzdLoginCore(): Promise<"ok" | "sms" | "failed"> {
}
}
- const token =
- typeof window !== "undefined" ? new URL(window.location.href).searchParams.get("uabaseToken") : null
+ const token = typeof window !== "undefined" ? getUrlQueryParam("uabaseToken") || null : null
if (!token) {
uni.showToast({ title: "用户凭证已失效,请重新进入智慧后勤应用", icon: "none", duration: 6000 })
From 4df8ee27da04f5d0737ea644efea595b903747d2 Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Fri, 8 May 2026 22:46:40 +0800
Subject: [PATCH 04/13] zzd
---
src/App.vue | 6 +++++-
src/pages/tabbar/index.vue | 15 +++++++++++++--
2 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/src/App.vue b/src/App.vue
index 9b24feb..78ba379 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,5 +1,5 @@
-
-