소스 검색

feat(mobile-h5): restore session on OA notify deep link with returnUrl

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 달 전
부모
커밋
722f634b61
4개의 변경된 파일과 183개의 추가작업 그리고 87개의 파일을 삭제
  1. 15 5
      mobile-h5/pages/login/index.vue
  2. 119 79
      mobile-h5/pages/oa/webview/index.vue
  3. 24 1
      mobile-h5/utils/auth-session.js
  4. 25 2
      mobile-h5/utils/auth.js

+ 15 - 5
mobile-h5/pages/login/index.vue

@@ -149,7 +149,8 @@
     isLoggedIn,
     passwordLogin,
     wechatLogin,
-    clearLegacyMockSessionIfNeeded
+    clearLegacyMockSessionIfNeeded,
+    takeReturnUrl
   } from '../../utils/auth.js'
   import { validateCredentials } from '../../utils/validate-credentials.js'
   import { MP_GH_USERNAME, MP_QRCODE_URL } from '../../utils/config.js'
@@ -182,7 +183,7 @@
     onShow() {
       clearLegacyMockSessionIfNeeded()
       if (isLoggedIn()) {
-        uni.reLaunch({ url: '/pages/index/index' })
+        this.goAfterLogin()
       }
     },
     methods: {
@@ -246,9 +247,18 @@
         }
         this.showMpQrcode = true
       },
-      goHome() {
+      goAfterLogin() {
+        const back = takeReturnUrl()
+        if (back) {
+          const url = back.startsWith('/') ? back : '/' + back
+          uni.reLaunch({ url })
+          return
+        }
         uni.reLaunch({ url: '/pages/index/index' })
       },
+      goHome() {
+        this.goAfterLogin()
+      },
       async onPasswordLogin() {
         if (this.loading) return
         this.errors = { username: '', password: '' }
@@ -266,7 +276,7 @@
             password: this.password
           })
           uni.showToast({ title: '登录成功', icon: 'success' })
-          setTimeout(() => this.goHome(), 400)
+          setTimeout(() => this.goAfterLogin(), 400)
         } catch (e) {
           uni.showToast({ title: e.message || '登录失败', icon: 'none' })
         } finally {
@@ -313,7 +323,7 @@
             phoneCode
           })
           uni.showToast({ title: '登录成功', icon: 'success' })
-          setTimeout(() => this.goHome(), 400)
+          setTimeout(() => this.goAfterLogin(), 400)
         } catch (err) {
           uni.showToast({
             title: (err && err.message) || '登录失败',

+ 119 - 79
mobile-h5/pages/oa/webview/index.vue

@@ -12,7 +12,11 @@ import {
   takeOaStartTitle
 } from '../../../utils/oa-h5.js'
 import { navigateBackFromOaHost } from '../../../utils/oa-navigate-back.js'
-import { forceRelogin, getToken } from '../../../utils/auth.js'
+import {
+  forceRelogin,
+  getToken,
+  silentWechatLogin
+} from '../../../utils/auth.js'
 import { fetchOaTemplateDetail } from '../../../utils/oa-api.js'
 
 const CLOSE_ACTIONS = new Set([
@@ -26,90 +30,38 @@ const CLOSE_ACTIONS = new Set([
   'oa-detail-restart'
 ])
 
+function buildQueryString(query) {
+  const q = query || {}
+  const parts = []
+  Object.keys(q).forEach((key) => {
+    const val = q[key]
+    if (val === undefined || val === null || val === '') return
+    parts.push(
+      encodeURIComponent(key) + '=' + encodeURIComponent(String(val))
+    )
+  })
+  return parts.join('&')
+}
+
 export default {
   data() {
-    return { src: '', _closing: false }
+    return { src: '', _closing: false, _query: {} }
   },
-  onLoad(query) {
+  async onLoad(query) {
+    this._query = query || {}
     if (!getToken()) {
-      forceRelogin('请先登录')
-      return
-    }
-
-    const instanceId = String((query && query.instanceId) || '')
-    const taskId = String((query && query.taskId) || '')
-    const mode = String((query && query.mode) || '') || 'view'
-    const autoPrint = String((query && query.autoPrint) || '') === '1'
-    const appId = String((query && query.appId) || '')
-    const draftId = String((query && query.draftId) || '')
-    const fromInstanceId = String((query && query.fromInstanceId) || '')
-
-    // 传给 OA H5:真机 navigateBack 静默失败时 wx.reLaunch/redirectTo 用
-    const returnPath = resolveOaReturnPath(query)
-
-    let src = ''
-
-    if (instanceId) {
-      const cachedName = takeOaDetailTitle()
-      if (cachedName) {
-        uni.setNavigationBarTitle({ title: cachedName })
-      }
-      src = buildOaDetailUrl({ instanceId, taskId, mode, autoPrint, returnPath })
-    } else if (draftId || appId || fromInstanceId) {
-      const cachedName = takeOaStartTitle()
-      if (cachedName) {
-        uni.setNavigationBarTitle({ title: cachedName })
-      } else if (appId) {
-        this.resolveTitle(appId)
+      try {
+        await silentWechatLogin()
+      } catch (e) {
+        const qs = buildQueryString(this._query)
+        forceRelogin(
+          '请先登录',
+          '/pages/oa/webview/index' + (qs ? '?' + qs : '')
+        )
+        return
       }
-      src = buildOaStartUrl({ appId, draftId, fromInstanceId, returnPath })
-    } else {
-      uni.showToast({ title: '参数错误', icon: 'none' })
-      setTimeout(() => {
-        uni.navigateBack({
-          fail: () => {}
-        })
-      }, 400)
-      return
-    }
-
-    if (!/\btoken=/.test(src)) {
-      forceRelogin('请先登录')
-      return
-    }
-
-    console.log('[oa webview] onLoad', {
-      returnPath,
-      mode,
-      instanceId,
-      taskId,
-      appId,
-      draftId,
-      fromInstanceId,
-      src
-    })
-    try {
-      const pages = getCurrentPages() || []
-      console.log(
-        '[oa webview] stack',
-        pages.map((p) => (p && p.route) || '')
-      )
-    } catch (e) {
-      console.log('[oa webview] stack err', e)
     }
-    this.src = src
-
-    // H5:OA 页在 iframe 内,靠 postMessage 通知本页 uni.navigateBack
-    // (微信小程序由 OA 内 wx.miniProgram.navigateBack,勿在此再退一层)
-    // #ifdef H5
-    this._onParentMessage = (e) => {
-      const d = e && e.data
-      if (!d || d.source !== 'zkxw-oa-h5') return
-      console.log('[oa webview] iframe message', d)
-      this.navigateBackFromOa(d)
-    }
-    window.addEventListener('message', this._onParentMessage)
-    // #endif
+    this.openFromQuery(this._query)
   },
   onShow() {
     console.log('[oa webview] onShow closing=', this._closing)
@@ -124,6 +76,94 @@ export default {
     // #endif
   },
   methods: {
+    openFromQuery(query) {
+      const instanceId = String((query && query.instanceId) || '')
+      const taskId = String((query && query.taskId) || '')
+      const mode = String((query && query.mode) || '') || 'view'
+      const autoPrint = String((query && query.autoPrint) || '') === '1'
+      const appId = String((query && query.appId) || '')
+      const draftId = String((query && query.draftId) || '')
+      const fromInstanceId = String((query && query.fromInstanceId) || '')
+
+      // 传给 OA H5:真机 navigateBack 静默失败时 wx.reLaunch/redirectTo 用
+      const returnPath = resolveOaReturnPath(query)
+
+      let src = ''
+
+      if (instanceId) {
+        const cachedName = takeOaDetailTitle()
+        if (cachedName) {
+          uni.setNavigationBarTitle({ title: cachedName })
+        }
+        src = buildOaDetailUrl({
+          instanceId,
+          taskId,
+          mode,
+          autoPrint,
+          returnPath
+        })
+      } else if (draftId || appId || fromInstanceId) {
+        const cachedName = takeOaStartTitle()
+        if (cachedName) {
+          uni.setNavigationBarTitle({ title: cachedName })
+        } else if (appId) {
+          this.resolveTitle(appId)
+        }
+        src = buildOaStartUrl({ appId, draftId, fromInstanceId, returnPath })
+      } else {
+        uni.showToast({ title: '参数错误', icon: 'none' })
+        setTimeout(() => {
+          uni.navigateBack({
+            fail: () => {}
+          })
+        }, 400)
+        return
+      }
+
+      if (!/\btoken=/.test(src)) {
+        const qs = buildQueryString(query)
+        forceRelogin(
+          '请先登录',
+          '/pages/oa/webview/index' + (qs ? '?' + qs : '')
+        )
+        return
+      }
+
+      console.log('[oa webview] onLoad', {
+        returnPath,
+        mode,
+        instanceId,
+        taskId,
+        appId,
+        draftId,
+        fromInstanceId,
+        src
+      })
+      try {
+        const pages = getCurrentPages() || []
+        console.log(
+          '[oa webview] stack',
+          pages.map((p) => (p && p.route) || '')
+        )
+      } catch (e) {
+        console.log('[oa webview] stack err', e)
+      }
+      this.src = src
+
+      // H5:OA 页在 iframe 内,靠 postMessage 通知本页 uni.navigateBack
+      // (微信小程序由 OA 内 wx.miniProgram.navigateBack,勿在此再退一层)
+      // #ifdef H5
+      if (!this._onParentMessage) {
+        this._onParentMessage = (e) => {
+          const d = e && e.data
+          if (!d || d.source !== 'zkxw-oa-h5') return
+          console.log('[oa webview] iframe message', d)
+          this.navigateBackFromOa(d)
+        }
+        window.addEventListener('message', this._onParentMessage)
+      }
+      // #endif
+    },
     async resolveTitle(appId) {
       try {
         const detail = await fetchOaTemplateDetail(appId)

+ 24 - 1
mobile-h5/utils/auth-session.js

@@ -3,6 +3,25 @@
 const TOKEN_KEY = 'zkxw_mobile_token'
 const REFRESH_TOKEN_KEY = 'zkxw_mobile_refresh_token'
 const USER_KEY = 'zkxw_mobile_user'
+const RETURN_URL_KEY = 'zkxw_mobile_return_url'
+
+export function setReturnUrl(url) {
+  try {
+    uni.setStorageSync(RETURN_URL_KEY, url || '')
+  } catch (e) {
+    /* ignore */
+  }
+}
+
+export function takeReturnUrl() {
+  try {
+    const u = uni.getStorageSync(RETURN_URL_KEY) || ''
+    uni.removeStorageSync(RETURN_URL_KEY)
+    return typeof u === 'string' ? u : ''
+  } catch (e) {
+    return ''
+  }
+}
 
 export function getToken() {
   try {
@@ -74,9 +93,13 @@ function isOnLoginPage() {
 /**
  * Token 失效:清本地会话并回到登录页(防抖,避免并发 401 连跳)。
  * @param {string} [message]
+ * @param {string} [returnUrl] 登录成功后回跳路径(写入本地,登录页 takeReturnUrl)
  */
-export function forceRelogin(message) {
+export function forceRelogin(message, returnUrl) {
   if (_reloginPending) return
+  if (typeof returnUrl === 'string' && returnUrl.trim()) {
+    setReturnUrl(returnUrl.trim())
+  }
   clearSession()
   if (isOnLoginPage()) return
   _reloginPending = true

+ 25 - 2
mobile-h5/utils/auth.js

@@ -9,7 +9,9 @@ import {
   clearSession,
   saveSession,
   updateStoredUser,
-  forceRelogin
+  forceRelogin,
+  setReturnUrl,
+  takeReturnUrl
 } from './auth-session.js'
 
 export { validateCredentials }
@@ -20,7 +22,9 @@ export {
   getUser,
   clearSession,
   updateStoredUser,
-  forceRelogin
+  forceRelogin,
+  setReturnUrl,
+  takeReturnUrl
 }
 
 /**
@@ -101,6 +105,25 @@ export async function wechatLogin({ loginCode, phoneCode }) {
   return data
 }
 
+/**
+ * 深链无 JWT 时静默换票(仅 loginCode,后端按已绑 openid 签发)。
+ */
+export async function silentWechatLogin() {
+  const loginRes = await new Promise((resolve, reject) => {
+    uni.login({ provider: 'weixin', success: resolve, fail: reject })
+  })
+  const loginCode = loginRes && loginRes.code
+  if (!loginCode) throw new Error('静默登录失败')
+  const data = await post('/mini/auth/wechat-login', { loginCode })
+  if (!data || !data.token) throw new Error('静默登录失败')
+  saveSession({
+    token: data.token,
+    refreshToken: data.refreshToken,
+    userInfo: data.userInfo
+  })
+  return data
+}
+
 /**
  * 已登录态下用 wx.login code 绑定 openid;失败静默。
  */