zzd
This commit is contained in:
1 parent
2f7a9d55a2
commit
b8d0379467
12 files changed
+417
-13
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
Vendored
+5
@@ -1,5 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "gdt-jsapi" {
|
||||
const dd: { getAuthCode: (opts: { corpId?: string }) => Promise<unknown> }
|
||||
export default dd
|
||||
}
|
||||
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
|
||||
@@ -383,6 +383,13 @@
|
||||
"navigationBarTitleText": "用户身份",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "zzdSmsVerify",
|
||||
"style": {
|
||||
"navigationBarTitleText": "手机号核验",
|
||||
"disableScroll": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { onLoad } from "@dcloudio/uni-app"
|
||||
import { constant } from "@@/index"
|
||||
import { userApi } from "@@/api"
|
||||
|
||||
const bindSessionId = ref("")
|
||||
const displayName = ref("")
|
||||
const mobile = ref("")
|
||||
const smsCode = ref("")
|
||||
|
||||
onLoad((q: Record<string, string | undefined>) => {
|
||||
bindSessionId.value = decodeURIComponent(q.bindSessionId || "")
|
||||
displayName.value = decodeURIComponent(q.displayName || "")
|
||||
})
|
||||
|
||||
async function sendSms() {
|
||||
const m = mobile.value.trim()
|
||||
if (!m) {
|
||||
uni.showToast({ title: "请输入手机号", icon: "none" })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const r: any = await userApi.zzdLoginSms(m)
|
||||
if (r?.code !== 0) {
|
||||
uni.showToast({ title: r?.msg || "发送失败", icon: "none" })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" })
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message || "发送失败", icon: "none" })
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
const m = mobile.value.trim()
|
||||
const code = smsCode.value.trim()
|
||||
if (!bindSessionId.value || !m || !code) {
|
||||
uni.showToast({ title: "请填写手机号与验证码", icon: "none" })
|
||||
return
|
||||
}
|
||||
uni.showLoading({ title: "核验中...", mask: true })
|
||||
try {
|
||||
const r: any = await userApi.zzdSsoConfirmPhone({
|
||||
bindSessionId: bindSessionId.value,
|
||||
mobilePhone: m,
|
||||
smsCode: code,
|
||||
})
|
||||
if (r?.code !== 0) {
|
||||
uni.showToast({ title: r?.msg || "核验失败", icon: "none" })
|
||||
return
|
||||
}
|
||||
const tokenKey = constant.storageConstant.TOKEN
|
||||
const userInfoKey = constant.storageConstant.USERINFO
|
||||
uni.setStorageSync(tokenKey, r.data)
|
||||
const { data: userData } = await userApi.userInfo()
|
||||
uni.setStorageSync(userInfoKey, userData)
|
||||
uni.showToast({ title: "登录成功", icon: "none" })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: "/pages/tabbar/index" })
|
||||
}, 400)
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message || "核验失败", icon: "none" })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<crud-layout title="手机号核验" bg="/static/page/blue-bg.png">
|
||||
<view class="h-30rpx" />
|
||||
<view class="card p-x-30rpx p-y-40rpx">
|
||||
<view v-if="displayName" class="m-b-24rpx text-28rpx color-#666">浙政钉用户:{{ displayName }}</view>
|
||||
<tm-form direction="vertical">
|
||||
<tm-form-item label="手机号" :showBottom="false">
|
||||
<tm-input v-model="mobile" type="number" placeholder="请输入手机号" />
|
||||
</tm-form-item>
|
||||
<view class="h-30rpx" />
|
||||
<tm-form-item label="短信验证码" :showBottom="false">
|
||||
<tm-input v-model="smsCode" type="number" placeholder="验证码" />
|
||||
</tm-form-item>
|
||||
</tm-form>
|
||||
</view>
|
||||
<view
|
||||
class="bg-#f0f5ff p-y-20rpx text-center rounded-40rpx m-x-30rpx m-t-40rpx sub-text-30 color-#0958d9!"
|
||||
@tap="sendSms"
|
||||
>
|
||||
获取验证码
|
||||
</view>
|
||||
<view
|
||||
class="bg-#0094ff p-y-20rpx text-center rounded-40rpx m-x-30rpx m-t-30rpx sub-text-30 color-white!"
|
||||
@tap="confirm"
|
||||
>
|
||||
核验并进入应用
|
||||
</view>
|
||||
</crud-layout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep() {
|
||||
.tmFormItemLabel {
|
||||
padding-bottom: 10rpx !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+16
-3
@@ -12,12 +12,25 @@ export default {
|
||||
method: "GET",
|
||||
}),
|
||||
|
||||
// 浙政钉授权登录
|
||||
auth: (token: string): Promise<any> =>
|
||||
// 浙政钉授权登录(token:旧 uabaseToken;authCode:开放平台 H5 getAuthCode)
|
||||
auth: (payload: { token?: string; authCode?: string }): Promise<any> =>
|
||||
request({
|
||||
url: "/thirdparty/thirdPartyOAth2/zzdsso",
|
||||
method: "POST",
|
||||
data: { token },
|
||||
data: payload,
|
||||
}),
|
||||
|
||||
zzdLoginSms: (mobilePhone: string): Promise<any> =>
|
||||
request({
|
||||
url: `/thirdparty/thirdPartyOAth2/loginSms?mobilePhone=${encodeURIComponent(mobilePhone.trim())}`,
|
||||
method: "POST",
|
||||
}),
|
||||
|
||||
zzdSsoConfirmPhone: (data: { bindSessionId: string; mobilePhone: string; smsCode: string }): Promise<any> =>
|
||||
request({
|
||||
url: "/thirdparty/thirdPartyOAth2/zzdsso/confirmPhone",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
|
||||
// 诗画授权登录
|
||||
|
||||
@@ -12,6 +12,9 @@ export const getLaunchParam = (): string => {
|
||||
|
||||
// 检测app信息判断运行环境
|
||||
export const getAppInfo = async (): Promise<string> => {
|
||||
if (import.meta.env.VITE_DEV_FORCE_ZZD === "true") {
|
||||
return APPENV.ZZD
|
||||
}
|
||||
try {
|
||||
const res = await (window as any).CP2.getAppInfo()
|
||||
// 浙政钉环境
|
||||
|
||||
@@ -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<unknown>
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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<never>((_, 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<string, unknown>
|
||||
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<string> {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function loadScript(src: string): Promise<void> {
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -219,39 +219,99 @@ const autoLogin = async (): Promise<boolean> => {
|
||||
return false
|
||||
}
|
||||
|
||||
// 浙政钉登录
|
||||
async function loginByZzd(): Promise<boolean> {
|
||||
function isZzdNeedSmsVerify(data: unknown): data is { needSmsVerify: true; bindSessionId: string; displayName?: string } {
|
||||
if (!data || typeof data !== "object") return false
|
||||
const o = data as Record<string, unknown>
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user