Bläddra i källkod

feat(mobile-oa-h5): add service-account OAuth bind page for OA notify

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 månader sedan
förälder
incheckning
dc53c6e871

+ 2 - 0
mobile-oa-h5/.env.development

@@ -1,3 +1,5 @@
 VITE_API_BASE=/api
 # 与 web-admin `.env.development` / mobile-h5 LOCAL_MINIO 对齐
 VITE_MINIO_BASE_URL=http://47.99.164.66:20004/zkxw
+# 微信服务号 AppID(OA 审批通知网页授权绑定;勿放 Secret)
+VITE_WX_MP_APPID=

+ 5 - 0
mobile-oa-h5/.env.example

@@ -0,0 +1,5 @@
+VITE_API_BASE=/api
+VITE_MINIO_BASE_URL=
+# 微信服务号 AppID(OA 审批通知网页授权绑定;勿放 Secret)
+# 运维须在服务号后台配置网页授权回调域名,并在小程序配置业务域名
+VITE_WX_MP_APPID=

+ 3 - 1
mobile-oa-h5/src/router.js

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
 import FormSink from './views/FormSink.vue'
 import OaStart from './views/OaStart.vue'
 import OaDetail from './views/OaDetail.vue'
+import OaMpBind from './views/OaMpBind.vue'
 
 export default createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
@@ -12,6 +13,7 @@ export default createRouter({
     },
     { path: '/oa-form-sink', component: FormSink },
     { path: '/oa-start', component: OaStart },
-    { path: '/oa-detail', component: OaDetail }
+    { path: '/oa-detail', component: OaDetail },
+    { path: '/oa-mp-bind', component: OaMpBind }
   ]
 })

+ 183 - 0
mobile-oa-h5/src/views/OaMpBind.vue

@@ -0,0 +1,183 @@
+<template>
+  <div class="oa-mp-bind">
+    <div v-if="phase === 'loading'" class="oa-mp-bind__card">
+      <van-loading type="spinner" color="#0a9f6d" vertical>处理中…</van-loading>
+    </div>
+    <div v-else-if="phase === 'success'" class="oa-mp-bind__card oa-mp-bind__card--ok">
+      <div class="oa-mp-bind__icon" aria-hidden="true">✓</div>
+      <p class="oa-mp-bind__title">已开启审批通知</p>
+      <p class="oa-mp-bind__hint">可返回小程序继续使用</p>
+      <van-button type="primary" block class="oa-mp-bind__btn" @click="goBack">
+        返回
+      </van-button>
+    </div>
+    <div v-else class="oa-mp-bind__card oa-mp-bind__card--err">
+      <p class="oa-mp-bind__title">{{ errorText || '绑定失败' }}</p>
+      <van-button plain block class="oa-mp-bind__btn" @click="goBack">返回</van-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { showToast } from 'vant'
+import { captureTokenFromUrl, getToken } from '../utils/auth.js'
+import { post } from '../utils/http.js'
+import { closeToHost } from '../oa/close-to-host.js'
+
+const route = useRoute()
+/** @type {import('vue').Ref<'loading' | 'success' | 'error'>} */
+const phase = ref('loading')
+const errorText = ref('')
+
+function readCode() {
+  const fromRoute = String(route.query.code || '').trim()
+  if (fromRoute) return fromRoute
+  try {
+    return String(new URLSearchParams(window.location.search || '').get('code') || '').trim()
+  } catch {
+    return ''
+  }
+}
+
+function stablePageUrl() {
+  return `${window.location.origin}/oa-h5/oa-mp-bind`
+}
+
+function redirectToWechatOAuth(appid) {
+  const redirectUri = encodeURIComponent(stablePageUrl())
+  const url =
+    `https://open.weixin.qq.com/connect/oauth2/authorize` +
+    `?appid=${encodeURIComponent(appid)}` +
+    `&redirect_uri=${redirectUri}` +
+    `&response_type=code&scope=snsapi_base&state=mpbind#wechat_redirect`
+  window.location.replace(url)
+}
+
+function fail(msg) {
+  errorText.value = msg
+  phase.value = 'error'
+}
+
+function goBack() {
+  closeToHost(
+    { action: 'oa-mp-bind-done', message: phase.value === 'success' ? '已开启审批通知' : '' },
+    {
+      showToast: (msg) => showToast(msg),
+      fallbackMessage: '请关闭本页返回小程序',
+      fallbackMiniPath: '/pages/index/index'
+    }
+  )
+}
+
+async function bindWithCode(code) {
+  try {
+    await post('/mini/auth/mp-bind', { code })
+    phase.value = 'success'
+    // 成功后尝试回到小程序(可选)
+    try {
+      closeToHost(
+        { action: 'oa-mp-bind-done', message: '已开启审批通知' },
+        {
+          showToast: (msg) => showToast(msg),
+          fallbackMessage: '已开启审批通知',
+          fallbackMiniPath: '/pages/index/index'
+        }
+      )
+    } catch {
+      /* ignore host close errors */
+    }
+  } catch (e) {
+    fail((e && e.message) || '绑定失败')
+  }
+}
+
+onMounted(() => {
+  captureTokenFromUrl()
+  const code = readCode()
+  const appid = String(import.meta.env.VITE_WX_MP_APPID || '').trim()
+  const token = getToken()
+
+  if (!code) {
+    if (!appid) {
+      fail('未配置服务号 AppID')
+      return
+    }
+    if (!token) {
+      fail('请从小程序入口打开')
+      return
+    }
+    redirectToWechatOAuth(appid)
+    return
+  }
+
+  if (!token) {
+    fail('请从小程序入口打开')
+    return
+  }
+  void bindWithCode(code)
+})
+</script>
+
+<style scoped>
+.oa-mp-bind {
+  min-height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px 20px;
+  box-sizing: border-box;
+  background: #f5f7f6;
+}
+
+.oa-mp-bind__card {
+  width: 100%;
+  max-width: 360px;
+  padding: 32px 24px 24px;
+  border-radius: 16px;
+  background: #fff;
+  text-align: center;
+  box-shadow: 0 4px 16px rgba(10, 159, 109, 0.08);
+}
+
+.oa-mp-bind__icon {
+  width: 56px;
+  height: 56px;
+  margin: 0 auto 16px;
+  border-radius: 50%;
+  background: #e9f9f2;
+  color: #0a9f6d;
+  font-size: 28px;
+  line-height: 56px;
+  font-weight: 600;
+}
+
+.oa-mp-bind__title {
+  margin: 0 0 8px;
+  font-size: 18px;
+  font-weight: 600;
+  color: #0f291e;
+  line-height: 1.4;
+}
+
+.oa-mp-bind__hint {
+  margin: 0 0 24px;
+  font-size: 14px;
+  color: #5f7a6d;
+  line-height: 1.5;
+}
+
+.oa-mp-bind__card--err .oa-mp-bind__title {
+  margin-bottom: 24px;
+  color: #b91c1c;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.oa-mp-bind__btn {
+  --van-button-primary-background: #0a9f6d;
+  --van-button-primary-border-color: #0a9f6d;
+  border-radius: 10px;
+}
+</style>