Explorar el Código

feat(mobile-h5): add WeChat notify setup page with follow and bind steps

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu hace 2 meses
padre
commit
df42730cc8
Se han modificado 2 ficheros con 310 adiciones y 0 borrados
  1. 8 0
      mobile-h5/pages.json
  2. 302 0
      mobile-h5/pages/oa/wechat-notify/index.vue

+ 8 - 0
mobile-h5/pages.json

@@ -72,6 +72,14 @@
         "navigationBarTextStyle": "black"
       }
     },
+    {
+      "path": "pages/oa/wechat-notify/index",
+      "style": {
+        "navigationBarTitleText": "开启微信通知",
+        "navigationBarBackgroundColor": "#ecfdf5",
+        "navigationBarTextStyle": "black"
+      }
+    },
     {
       "path": "pages/oa/pdf-preview/index",
       "style": {

+ 302 - 0
mobile-h5/pages/oa/wechat-notify/index.vue

@@ -0,0 +1,302 @@
+<template>
+  <view class="notify-page">
+    <view class="notify-body">
+      <!-- 步骤 1:关注服务号 -->
+      <view class="step-card" :class="{ 'is-done': step1Done }">
+        <view class="step-hd">
+          <view class="step-num">1</view>
+          <view class="step-main">
+            <view class="step-title">关注服务号</view>
+            <view class="step-desc">
+              关注后才能接收微信模板消息。可跳转服务号主页,或长按识别二维码。
+            </view>
+          </view>
+          <text class="step-badge" :class="{ 'is-done': step1Done }">
+            {{ step1Done ? '已完成' : '未完成' }}
+          </text>
+        </view>
+        <view class="step-actions">
+          <button class="btn btn-primary" @click="onFollow">
+            {{ step1Done ? '重新打开关注页' : '去关注' }}
+          </button>
+          <view v-if="showQr && !step1Done" class="qr-box">
+            <image
+              class="qr-img"
+              :src="mpQrcodeUrl"
+              mode="aspectFit"
+              show-menu-by-longpress
+            />
+            <text class="qr-tip">长按识别关注</text>
+            <button class="btn btn-ghost" @click="onConfirmFollowed">我已关注</button>
+          </view>
+        </view>
+      </view>
+
+      <!-- 步骤 2:绑定账号 -->
+      <view class="step-card" :class="{ 'is-done': step2Done }">
+        <view class="step-hd">
+          <view class="step-num">2</view>
+          <view class="step-main">
+            <view class="step-title">绑定当前账号</view>
+            <view class="step-desc">
+              将服务号身份与系统账号关联,审批待办 / 抄送才能推送到你。
+            </view>
+          </view>
+          <text class="step-badge" :class="{ 'is-done': step2Done }">
+            {{ step2Done ? '已完成' : '未完成' }}
+          </text>
+        </view>
+        <view class="step-actions">
+          <button
+            class="btn btn-primary"
+            :disabled="!step1Done || step2Done"
+            @click="onBind"
+          >
+            {{
+              step2Done
+                ? '已绑定'
+                : step1Done
+                  ? '去绑定'
+                  : '请先完成步骤 1'
+            }}
+          </button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import {
+  fetchMpBindStatus,
+  getUser,
+  forceRelogin,
+  getToken
+} from '../../../utils/auth.js'
+import {
+  isFollowConfirmed,
+  setFollowConfirmed,
+  resolveStepDone
+} from '../../../utils/mp-follow-confirm.js'
+import { MP_GH_USERNAME, MP_QRCODE_URL } from '../../../utils/config.js'
+
+function currentUserId() {
+  const u = getUser() || {}
+  return u.id || u.userId || u.username || ''
+}
+
+export default {
+  data() {
+    return {
+      bound: false,
+      followConfirmed: false,
+      showQr: false,
+      mpQrcodeUrl: MP_QRCODE_URL,
+      step1Done: false,
+      step2Done: false
+    }
+  },
+  onShow() {
+    if (!getToken()) {
+      forceRelogin('请先登录')
+      return
+    }
+    this.followConfirmed = isFollowConfirmed(currentUserId())
+    this.refreshBind()
+  },
+  methods: {
+    applySteps() {
+      const r = resolveStepDone({
+        bound: this.bound,
+        followConfirmed: this.followConfirmed
+      })
+      this.step1Done = r.step1Done
+      this.step2Done = r.step2Done
+    },
+    async refreshBind() {
+      try {
+        const res = await fetchMpBindStatus()
+        this.bound = !!(res && res.bound)
+      } catch (_) {
+        this.bound = false
+      }
+      this.applySteps()
+    },
+    onFollow() {
+      const username = (MP_GH_USERNAME || '').trim()
+      const openProfile =
+        typeof wx !== 'undefined' && typeof wx.openOfficialAccountProfile === 'function'
+      if (openProfile && username) {
+        wx.openOfficialAccountProfile({
+          username,
+          fail: () => {
+            this.showQr = true
+          }
+        })
+        return
+      }
+      this.showQr = true
+    },
+    onConfirmFollowed() {
+      setFollowConfirmed(currentUserId(), true)
+      this.followConfirmed = true
+      this.showQr = false
+      this.applySteps()
+      uni.showToast({ title: '已标记关注', icon: 'none' })
+    },
+    onBind() {
+      if (!this.step1Done || this.step2Done) return
+      uni.navigateTo({ url: '/pages/oa/mp-bind/index' })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.notify-page {
+  min-height: 100vh;
+  background: #f5f8f7;
+  box-sizing: border-box;
+}
+
+.notify-body {
+  padding: 28rpx 24rpx 56rpx;
+}
+
+.step-card {
+  background: #fff;
+  border: 1px solid #dcebe4;
+  border-radius: 28rpx;
+  padding: 28rpx;
+  margin-bottom: 20rpx;
+
+  &.is-done {
+    border-color: #b7ebcf;
+    background: linear-gradient(180deg, #f6fffb 0%, #fff 60%);
+  }
+}
+
+.step-hd {
+  display: flex;
+  align-items: flex-start;
+  gap: 20rpx;
+}
+
+.step-num {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 50%;
+  background: #0a9f6d;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  font-weight: 700;
+  flex-shrink: 0;
+}
+
+.step-card.is-done .step-num {
+  background: #189b65;
+}
+
+.step-main {
+  flex: 1;
+  min-width: 0;
+}
+
+.step-title {
+  font-size: 30rpx;
+  font-weight: 700;
+  line-height: 1.35;
+  color: #1f2a26;
+}
+
+.step-desc {
+  margin-top: 8rpx;
+  font-size: 24rpx;
+  color: #6b7b74;
+  line-height: 1.5;
+}
+
+.step-badge {
+  margin-left: auto;
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 999rpx;
+  background: #eef2f0;
+  color: #6b7b74;
+  flex-shrink: 0;
+  line-height: 1.4;
+
+  &.is-done {
+    background: #e9f9f2;
+    color: #087f57;
+  }
+}
+
+.step-actions {
+  margin-top: 24rpx;
+  padding-left: 76rpx;
+}
+
+.btn {
+  appearance: none;
+  border: 0;
+  border-radius: 20rpx;
+  padding: 20rpx 28rpx;
+  font-size: 28rpx;
+  font-weight: 600;
+  min-height: 80rpx;
+  line-height: 1.3;
+  box-sizing: border-box;
+
+  &::after {
+    border: none;
+  }
+}
+
+.btn-primary {
+  background: #0a9f6d;
+  color: #fff;
+  width: 100%;
+
+  &[disabled] {
+    background: #a7d7c4;
+    color: #fff;
+  }
+}
+
+.btn-ghost {
+  margin-top: 20rpx;
+  background: #fff;
+  color: #087f57;
+  border: 1px solid #dcebe4;
+  width: 100%;
+}
+
+.qr-box {
+  margin-top: 20rpx;
+  padding: 24rpx;
+  border-radius: 24rpx;
+  border: 1px dashed #a7f3d0;
+  background: #f8fffb;
+  text-align: center;
+}
+
+.qr-img {
+  display: block;
+  width: 280rpx;
+  height: 280rpx;
+  margin: 0 auto;
+  border-radius: 16rpx;
+  background: #fff;
+}
+
+.qr-tip {
+  display: block;
+  margin-top: 16rpx;
+  font-size: 24rpx;
+  color: #6b7b74;
+}
+</style>