wwf
12 小时以前 9a6cd220224fd3a9a6c84b5bb37c6410a470969f
考点核验
17个文件已修改
2个文件已添加
894 ■■■■■ 已修改文件
package-lock.json 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/h5/router.js 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/wxjssdk.js 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/faceAuth/components/auditDialog.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/faceAuth/components/camera.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/faceAuth/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/index.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/login/bind.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/login/index.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/login/redirect.vue 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/signup/index.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/verify/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/h5/verify/index.vue 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/main/components/UploadIdCard.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json
@@ -1,11 +1,11 @@
{
  "name": "app-web-examination-platform",
  "name": "app-web-examination-user",
  "version": "0.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "app-web-examination-platform",
      "name": "app-web-examination-user",
      "version": "0.0.0",
      "dependencies": {
        "@element-plus/icons-vue": "^2.3.2",
@@ -22,6 +22,7 @@
        "vconsole": "^3.15.1",
        "vue": "^3.5.22",
        "vue-router": "^4.6.3",
        "weixin-js-sdk": "^1.6.5",
        "xlsx": "^0.18.5",
        "xlsx-js-style": "^1.2.0"
      },
@@ -5712,6 +5713,12 @@
      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
      "license": "MIT"
    },
    "node_modules/weixin-js-sdk": {
      "version": "1.6.5",
      "resolved": "https://registry.npmmirror.com/weixin-js-sdk/-/weixin-js-sdk-1.6.5.tgz",
      "integrity": "sha512-Gph1WAWB2YN/lMOFB/ymb+hbU/wYazzJgu6PMMktCy9cSCeW5wA6Zwt0dpahJbJ+RJEwtTv2x9iIu0U4enuVSQ==",
      "license": "MIT"
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
package.json
@@ -28,6 +28,7 @@
    "vconsole": "^3.15.1",
    "vue": "^3.5.22",
    "vue-router": "^4.6.3",
    "weixin-js-sdk": "^1.6.5",
    "xlsx": "^0.18.5",
    "xlsx-js-style": "^1.2.0"
  },
src/App.vue
@@ -4,10 +4,23 @@
  </div>
</template>
<script setup>
// import { useWindowSize } from '@/utils/hook.js'
// const { width, height } = useWindowSize()
<script>
import { isWeixin } from '@/utils/UA.js'
import { getWxSignature } from '@/utils/wxjssdk.js'
export default {
  data() {
    return {
    }
  },
  watch: {
    '$route.path': function(){
      if (isWeixin) {
        getWxSignature(this.$route)
      }
    },
  }
}
</script>
<style scoped>
src/main.js
@@ -29,9 +29,9 @@
  app.component(key, component)
}
// import('vconsole').then((module) => {
//   new module.default()
// })
import('vconsole').then((module) => {
  new module.default()
})
app.config.globalProperties.$rules = ruleGenerator
app.config.globalProperties.$property = property
src/router/h5/router.js
@@ -20,11 +20,6 @@
        component: () => import('@/views/h5/verify/noAccess.vue'),
      },
      {
        path: 'login',
        name: '身份验证登录',
        component: () => import('@/views/h5/login/index.vue'),
      },
      {
        path: 'signup',
        name: '签到',
        component: () => import('@/views/h5/signup/index.vue'),
@@ -36,5 +31,20 @@
      },
    ],
  },
  {
    path: '/h5/login',
    name: '身份验证登录',
    component: () => import('@/views/h5/login/index.vue'),
  },
  {
    path: '/h5/redirect',
    name: '重定向',
    component: () => import('@/views/h5/login/redirect.vue'),
  },
  {
    path: '/h5/bind',
    name: '绑定手机号',
    component: () => import('@/views/h5/login/bind.vue'),
  },
]
export default router
src/router/index.js
@@ -3,7 +3,6 @@
import errorPage from '@/router/error/index.js'
import mainPage from '@/router/main/index.js'
import h5 from '@/router/h5/router.js'
import { useLoginStore } from '@/stores/login.js'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
@@ -11,7 +10,6 @@
})
router.beforeEach((to, from, next) => {
  const { setLastRouteInfo } = useLoginStore()
  if (!to.matched.length) {
    if (to.path === '/') {
      next({ path: '/main/home' })
@@ -19,9 +17,6 @@
      next({ path: '/error/404', query: { errorUrl: to.path } })
    }
  } else {
    if (from.name) {
      setLastRouteInfo(from)
    }
    next()
  }
})
src/utils/wxjssdk.js
@@ -1,16 +1,14 @@
import wx from 'weixin-js-sdk'
import axios from './axios'
import store from '../store.js'
import $qxueyou from '@/config/qxueyou.js'
import utilsUA from '@/plugins/utilsUA'
import { getUUID, qxyResImg } from '@/plugins/utils'
import { isWeixin, isMobile } from '@/utils/UA.js'
let newFeature = false
let oldShare = ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareQZone']
let newShare = ['updateTimelineShareData', 'updateAppMessageShareData']
let weixinFlag = utilsUA.isWeixin
let mobileFlag = utilsUA.isMobile
let weixinFlag = isWeixin
let mobileFlag = isMobile
let channel = weixinFlag && mobileFlag ? 'wx_pub' : 'wx_pub_qr'
let isWxpub = 'wx_pub'.includes(channel)
@@ -21,7 +19,7 @@
function getWxSignature(toRoute) { 
  if (!weixinFlag) { return false }
  axios.get('/wx/js/signature', {
  axios.get('/system/wx/js/signature', {
    params: { url: location.href } 
  }).then(res => {
    if (!res || !res.data) {
@@ -36,193 +34,19 @@
      signature: result.signature,
      jsApiList: [
        ...(newFeature ? newShare : oldShare),
        'chooseWXPay',
        // 'chooseWXPay',
        'chooseImage',
        'getLocalImgData'
      ],
      openTagList: ['wx-open-launch-app']
    })
    wx.ready(function () {
      initShareOption(toRoute)
    })
    // wx.ready(function () {
    //   initShareOption(toRoute)
    // })
    wx.error(function (res) {
      console.log(res)
    })
  })
}
/**
 * 初始化分享数据
 * @param {*} toRoute
 */
function initShareOption(toRoute) {
  if (!weixinFlag) { return false }
  let uuid = getUUID()
  let shareOptions = getShareOptions(uuid,toRoute)
  shareOptions.success = function () {
    shareSuccess(uuid,toRoute)
  }
  shareOptions.cancel = function () {
    console.log('取消分享')
  }
  shareOptions.trigger = function () {
    console.log('用户点击发送给朋友')
  }
  // 向下兼容旧版分享接口
  if (!newFeature) {
    wx.onMenuShareTimeline(shareOptions)
    wx.onMenuShareAppMessage(shareOptions)
  } else {
    wx.updateAppMessageShareData(shareOptions)
    wx.updateTimelineShareData(shareOptions)
  }
}
/**
 * 获取分享数据
 * @param {*} list
 */
function getShareOptions(uuid,toRoute) {
  let result = {}
  let customShare = store.state.share.custom
  if (customShare.title) {
    result.title = customShare.title
    result.desc = customShare.desc
  } else {
    result.title = toRoute.name
    result.desc = toRoute.name
  }
  if (customShare.imgUrl) {
    if (customShare.imgUrl.includes('http')) {
      result.imgUrl = customShare.imgUrl
    } else {
      result.imgUrl = qxyResImg(customShare.imgUrl)
    }
  }
  if (customShare.uuidLink) {
    let pageUrl = customShare.pageUrl || toRoute.fullPath
    let encode = encodeURI(`${uuid},${$qxueyou.htmlRoot + pageUrl}`)
    result.link = customShare.uuidLink + btoa(encode)
    // result.link = customShare.uuidLink + uuid
  } else if (customShare.link) {
    result.link = customShare.link
  } else {
    result.link = location.href
  }
  return result
}
/**
 * 分享成功回调
 */
function shareSuccess(uuid,toRoute) {
  let customShare = store.state.share.custom
  if (customShare.targetId) {
    let pageUrl = customShare.pageUrl || toRoute.fullPath
    axios.post('/wx/share/callback', {
      urlId: uuid,
      pageUrl: $qxueyou.htmlRoot + pageUrl,
      targetId: customShare.targetId,
      planIds: customShare.planIds
    }).then(() => {
      initShareOption(toRoute)
    })
  }
  // 当分享有方案Id,触发是否关注公众号
  if (customShare.planIds) {
    store.commit('wxh5/subscribe', '及时获取奖励提醒')
  }
  let mask = store.state.share.mask
  if (mask.show) {
    let text = mask.type === 'plan' ? '分享成功,继续助力' : '邀请成功,继续邀请'
    let newMask = { show: true, type: mask.type, text: text, codeText: mask.codeText }
    store.commit("share/maskText", newMask)
  }
}
/**
 * 统一支付接口处理
 * @param {*} orderId
 * @param {*} successCallback // 支付成功回调
 * @param {*} showCodeCallback // 显示二维码回调
 */
function unipayPay(orderId, successCallback, showCodeCallback) {
  axios.post('/wx/pay/createOrder', {
    orderId: orderId,
    channel: channel,
    redirectUrl: weixinFlag ? store.state.order.paySuccessUrl : undefined,
  }).then(res => {
    if (!res.data.data) { return false }
    let params = res.data.data.param
    if (isWxpub) { // 公众号支付
      chooseWXPay(params, successCallback)
    } else { // 扫码支付
      store.commit('timer/paying', true)
      showCodeCallback && showCodeCallback(params.codeUrl)
      checkIsPay(orderId, successCallback)
    }
  })
}
function chooseWXPay(result, successCallback) {
  //调用微信支付接口
  wx.chooseWXPay({
    timestamp: result.timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
    nonceStr: result.nonceStr, // 支付签名随机串,不长于 32 位
    package: result.packageValue, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
    signType: result.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
    paySign: result.paySign, // 支付签名
    success: () => {
      successCallback && successCallback()
    },
    fail: (e) => {
      store.commit('snack/error', '请联系技术客服解决' + e.errMsg)
    }
  })
}
/**
 * 扫码支付成功的回调
 * @param {} orderId
 * @returns {}
 */
function checkIsPay(orderId, successCallback) {
  if (!store.state.timer.paying) {
    return false
  }
  setTimeout(function () {
    axios.get('/transact/order/payResult', {
      params: { orderId: orderId }
    }).then(res => {
      if (res.data.success) {
        successCallback && successCallback()
        store.commit('timer/paying', false)
      } else {
        checkIsPay(orderId, successCallback)
      }
    })
  }, 2000)
}
/**
 * 获取定位
 * @param {*} locationCallback
 */
function getPosition(locationCallback){
  wx.getLocation({
    success: function (res) {
      locationCallback(res)
    }
  })
}
function chooseImage(){
@@ -246,6 +70,7 @@
    })
  })
}
function getLocalImgData(localId){
  return new Promise((resolve) => {
    wx.getLocalImgData({
@@ -260,75 +85,7 @@
  })
}
/**
 * 预览图片
 * @param {*} url
 * @param {*} urlList
 */
function previewImage(current, urls){
  wx.previewImage({
    current: current, // 当前显示图片的http链接
    urls: urls ? urls : [current] // 需要预览的图片http链接列表
  })
}
/**
 * 返回小程序页面
 * @param {*} mpRouter // 小程序的路由
 * @param {*} otherCallback // 其他回调操作
 */
function redirectToMp(mpRouter) {
  return new Promise(resolve => {
    // 非微信 或 人社局活动入口
    if (!weixinFlag || store.state.course.isRsjActivity) {
      resolve(true)
      return false
    }
    wx.miniProgram.getEnv(function(res) {
      if (res.miniprogram) { // 小程序
        wx.miniProgram.redirectTo({
          url: mpRouter,
          success: function(){
            console.log('跳转成功')
            setTimeout(() => { // 处理其他机构的小程序跳转场景
              resolve(true);
            }, 3000)
          },
          fail: function(){
            resolve(true)
          }
        })
      } else { // 非小程序
        resolve(true)
      }
    })
  })
}
/**
 * 向小程序发送消息
 */
function postMessage(data) {
  // 非微信
  if (!weixinFlag) { return false }
  wx.miniProgram.getEnv(function(res) {
    if (res.miniprogram) { // 小程序
      wx.miniProgram.postMessage({
        data: data
      })
    }
  })
}
export {
  getWxSignature,
  initShareOption,
  unipayPay,
  getPosition,
  previewImage,
  chooseImage,
  redirectToMp,
  postMessage
  chooseImage
}
src/views/h5/faceAuth/components/auditDialog.vue
@@ -66,7 +66,8 @@
  data() {
    return {
      dialogFlag: false,
      status: ''
      status: '',
      url: ''
    }
  },
  props: {
@@ -99,16 +100,15 @@
  methods: {
    async submitAudit() {
      this.status = 'auditing'
      const url = await uploadByBase64(this.base64 ,'人脸照片')
      if (!url) {
      this.url = await uploadByBase64(this.base64 ,'人脸照片')
      if (!this.url) {
        this.status = 'fail'
        return
      }
      const params = { faceImgPath: url  }
      const params = { faceImgPath: this.url  }
      this.$axios.get('/system/auth/staff/checkin/face-match', { params }).then(res => {
        if (res.data.code == 0) {
          // this.status = res.data.data ? 'success' : 'fail'
          this.status = 'success'
          this.status = res.data.data ? 'success' : 'fail'
        } else {
          this.status = 'fail'
          this.$message.error(res.data.msg || "人脸比对失败")
@@ -119,7 +119,7 @@
    },
    handlerSuccess() {
      this.dialogFlag = false
      this.$emit('handlerSuccess')
      this.$emit('handlerSuccess', this.url)
    }
  }
}
src/views/h5/faceAuth/components/camera.vue
@@ -15,7 +15,6 @@
</template>
<script>
import { uploadByBase64 } from '@/utils/tool.js'
export default {
  data () {
    return {
@@ -197,7 +196,6 @@
    },
    async uploadBase64(){ // 上传图片
      let base64 = this.$refs.canvasEl.toDataURL("image/png", 1);
      // const url = await uploadByBase64(base64, '核验照片')
      if (base64) {
        this.$emit('handlerSuccess', base64)
        this.closeCamera()
src/views/h5/faceAuth/index.vue
@@ -51,6 +51,7 @@
import auditDialog from '@/views/h5/faceAuth/components/auditDialog.vue';
import { useSessionStore } from '@/stores/session.js'
import { storeToRefs } from 'pinia';
import { chooseImage } from '@/utils/wxjssdk.js'
export default {
  components: {
    camera,
@@ -63,10 +64,10 @@
  data() {
    return {
      tipItems: [
        { label: '标准拍摄', isCheck: true },
        { label: '标准拍摄', isCheck: true },
        { label: '标准拍摄', isCheck: true },
        { label: '标准拍摄', isCheck: true },
        { label: '标准拍摄' },
        { label: '遮挡脸部' },
        { label: '拍摄不全' },
        { label: '光线不足' },
      ],
      openCameraFlag: false,
      base64: '',
@@ -86,6 +87,9 @@
          'border-color': '#f8f8f8'
        }
      }
    },
    appId() {
      return this.$route.query.appId
    }
  },
  async mounted() {
@@ -95,9 +99,10 @@
    getUserPositionStatus(evt) {
      this.userPositionStatus = evt
    },
    startCapture() {
    async startCapture() {
      if (isWeixin) {
        console.log('')
        const photo = await chooseImage()
        this.shootSuccess('data:image/jpg;base64,' + photo)
      } else {
        this.openCameraFlag = true
      }
@@ -108,17 +113,9 @@
        this.auditDialogFlag = true
      }
    },
    auditSuccess() {
      localStorage.setItem('isFace', true)
      if (!this.getIsSignup()) {
        this.$router.replace({ path: '/h5/signup', query: { appId: this.appId } })
      } else {
        this.$router.replace({ path: '/h5/verForm', query: { appId: this.appId }})
      }
    auditSuccess(evt) {
      this.$router.replace({ path: '/h5/signup', query: { appId: this.appId, url: evt } })
    },
    getIsSignup() {
      return Boolean(localStorage.getItem('isSignup'))
    }
  }
}
</script>
src/views/h5/index.vue
@@ -1,33 +1,32 @@
<template>
  <div>
  <div v-if="userInfo.id">
    <router-view></router-view>
  </div>
</template>
<script>
import { useSessionStore } from '@/stores/session.js'
import { storeToRefs } from 'pinia';
export default {
  setup() {
    const { setUserInfo } = useSessionStore()
    return { setUserInfo }
    const { userInfo } = storeToRefs(useSessionStore())
    return { setUserInfo, userInfo }
  },
  data() {
    return {}
  },
  async created() {
    await this.getUserInfo()
  created() {
    if (this.$route.path == '/h5/verify' && this.$route.query.appId) {
      localStorage.setItem('verify_url', this.$route.fullPath)
    }
    this.getUserInfo()
  },
  methods: {
    getUserInfo() {
      return new Promise((resolve) => {
        this.$axios.get('/system/auth/staff/profile').then(res => {
          if (res.data.code == 0) {
            this.setUserInfo(res.data.data || {})
          } else {
            this.$message.error(res.data.msg || '获取用户信息失败')
          }
        }).finally(() => {
          resolve()
        })
      this.$axios.get('/system/auth/staff/profile').then(res => {
        if (res.data.code == 0) {
          this.setUserInfo(res.data.data || {})
        }
      })
    },
  }
src/views/h5/login/bind.vue
New file
@@ -0,0 +1,140 @@
<template>
  <div class="login">
    <el-form ref="form" :model="form">
      <el-form-item :rules="[$rules.required('请输入绑定手机号') , $rules.phone()]" prop="mobile">
        <el-input v-model="form.mobile" placeholder="请输入绑定手机号" style="width: 100%" size="large" />
      </el-form-item>
      <el-form-item prop="code" :rules="[$rules.required('请输入验证码'), $rules.code()]">
        <el-input
          v-model="form.code"
          placeholder="请输入验证码"
          style="width: 100%" size="large"
        >
          <template #append>
            <el-row style="width: 70px;justify-content: center;">
              <el-button
                v-if="countdown == 180"
                style="color: var(--el-color-primary);"
                class="cursor-p"
                :loading="sendCodeLoading"
                @click="sendCode()"
              >
                获取验证码
              </el-button>
              <el-text v-else>{{ countdown }}s</el-text>
            </el-row>
          </template>
        </el-input>
      </el-form-item>
    </el-form>
    <el-button @click="login()" :loading="loginLoading" type="primary" size="large" class="mt-2" style="width: 100%">绑定并登录</el-button>
  </div>
</template>
<script>
import { tokenUtils } from '@/utils/axios.js';
import { useLoginStore } from '@/stores/login.js'
import { storeToRefs } from 'pinia';
export default {
  setup() {
    const { lastRouteInfo } = storeToRefs(useLoginStore())
    return { lastRouteInfo }
  },
  data() {
    return {
      form: {
        mobile: '',
        code: '',
      },
      countdown: 180,
      sendCodeLoading: false,
      loginLoading: false,
      countdownInterval: null
    }
  },
  created() {
  },
  computed: {
    state() {
      return this.$route.query.state
    },
    openid() {
      return this.$route.query.openid
    },
    wxCode() {
      return this.$route.query.code
    }
  },
  mounted() {
    document.title = this.$route.name
  },
  methods: {
    startCountdownInterval() {
      this.clearCountdownInterval()
      this.countdown--
      this.countdownInterval = setInterval(() => {
        if (this.countdown > 0) {
          this.countdown--
        } else {
          this.countdown = 180
          this.clearCountdownInterval()
        }
      }, 1000)
    },
    clearCountdownInterval() {
      clearInterval(this.countdownInterval)
      this.countdownInterval = null
    },
    async sendCode() {
      const validate = await this.$refs.form.validateField('mobile')
      if (validate) {
        const data = {
          captchaVerification: '',
          mobile: this.form.mobile,
          scene: 21,
        }
        this.sendCodeLoading = true
        this.$axios.post('/system/auth/send-sms-code', data).then(res => {
          if (res.data.code == 0) {
            this.startCountdownInterval()
            this.$message.success('已发送验证码,请注意查收')
          } else {
            this.$message.error(res.data.msg || '获取验证码失败')
          }
        }).finally(() => {
          this.sendCodeLoading = false
        })
      }
    },
    login() {
      const data = {
        mobile: this.form.mobile,
        code: this.form.code,
        state: this.state,
        openid: this.openid,
        wxCode: this.wxCode
      }
      this.loginLoading = true
      this.$axios.post('/system/auth/staff/checkin/bind', data).then(async res => {
        if (res.data.code == 0) {
          const resData = res.data.data
          tokenUtils.setTokens(resData.accessToken, resData.refreshToken)
          this.$message.success('绑定成功')
          const path = localStorage.getItem('verify_url')
          if (path) {
            this.$router.replace(path)
          }
        } else {
          this.$message.error(res.data.msg || '登录失败')
        }
      }).finally(() => {
        this.loginLoading = false
      })
    },
  }
}
</script>
<style scoped>
.login {
  padding: 40px 20px 20px;
}
</style>
src/views/h5/login/index.vue
@@ -33,15 +33,16 @@
<script>
import { tokenUtils } from '@/utils/axios.js';
import { useLoginStore } from '@/stores/login.js'
import { storeToRefs } from 'pinia';
import { isWeixin } from '@/utils/UA.js'
export default {
  setup() {
    const { lastRouteInfo } = useLoginStore()
    const { lastRouteInfo } = storeToRefs(useLoginStore())
    return { lastRouteInfo }
  },
  data() {
    return {
      loginType: '', //mobile、weixin
      loginType: '', //mobilePhone、weixin
      form: {
        mobile: '',
        code: '',
@@ -55,7 +56,10 @@
  created() {
    tokenUtils.clearTokens()
    this.loginType = isWeixin ? 'weixin' : 'mobilePhone'
    this.loginType = 'mobile'
    if (isWeixin) {
      this.loginType = 'weixin'
      this.$router.replace({ path: '/h5/redirect' })
    }
  },
  computed: {
    appId() {
@@ -114,8 +118,9 @@
          const resData = res.data.data
          tokenUtils.setTokens(resData.accessToken, resData.refreshToken)
          this.$message.success('登录成功')
          if (this.lastRouteInfo.name) {
            this.$router.replace(this.lastRouteInfo)
          const path = localStorage.getItem('verify_url')
          if (path) {
            this.$router.replace(path)
          }
        } else {
          this.$message.error(res.data.msg || '登录失败')
@@ -124,12 +129,6 @@
        this.loginLoading = false
      })
    },
    verify() {
      this.$router.push('/h5/verify')
    },
    signup() {
      this.$router.push('/h5/signup')
    }
  }
}
</script>
src/views/h5/login/redirect.vue
New file
@@ -0,0 +1,232 @@
<template>
  <div class="wx-login-container">
    <div class="loading-wrapper">
      <div class="wechat-icon">
        <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
          <path d="M717.5 595.8c-15.7 8.3-32.6 13.9-50.4 15.7-12.6 1.3-25.3-0.4-37.6-2.9-19-3.8-36.9-10.8-53.5-20.7-3.6-2.2-7.2-4.4-10.7-6.8-19.5-13.6-36.5-29.9-50.8-49-6.5-8.6-12.1-17.9-16.9-27.5-4.8-9.7-8.3-19.8-10.6-30.3-2.2-10.1-3.2-20.3-3-30.6 0.1-5.3 0.4-10.5 1.1-15.8 1.7-12.6 5.1-24.7 10.1-36.3 6.1-14.1 14.2-27 24.4-38.5 13.9-15.6 30.4-28.1 49.3-37.2 12.9-6.3 26.5-10.6 40.6-12.8 14.1-2.2 28.4-2.2 42.6 0.1 14.2 2.3 27.8 6.7 40.7 13.1 18.9 9.2 35.4 21.7 49.3 37.3 10.2 11.5 18.3 24.5 24.4 38.5 5 11.6 8.4 23.8 10.1 36.3 0.7 5.3 1 10.5 1.1 15.8 0.2 10.3-0.8 20.5-3 30.6-2.3 10.5-5.8 20.6-10.6 30.3-4.8 9.6-10.4 18.9-16.9 27.5-14.3 19.1-31.3 35.4-50.8 49-3.5 2.4-7.1 4.6-10.7 6.8-16.6 9.9-34.5 16.9-53.5 20.7-12.3 2.5-25 4.2-37.6 2.9z" fill="#07C160"/>
          <path d="M395.6 380.4c0 54.7 26.5 103.5 67.6 134.4-3.6 9.8-7.9 19.2-12.8 28.1-4.9 8.9-10.4 17.4-16.6 25.4-6.2 8-13.1 15.4-20.6 22.2-7.5 6.8-15.7 12.8-24.4 17.9-8.7 5.1-18 9.3-27.6 12.5-9.6 3.2-19.6 5.3-29.8 6.3-10.2 1-20.5 0.9-30.6-0.4-10.1-1.3-20-3.8-29.4-7.4-9.4-3.6-18.3-8.3-26.6-14-8.3-5.7-15.9-12.2-22.7-19.6-6.8-7.4-12.7-15.5-17.6-24.3-4.9-8.8-8.8-18.2-11.5-28-2.7-9.8-4.2-19.9-4.5-30.2-0.3-10.3 0.8-20.5 3.2-30.5 2.4-10 6.1-19.6 10.9-28.7 4.8-9.1 10.7-17.5 17.5-25.2 6.8-7.7 14.5-14.5 22.9-20.4 8.4-5.9 17.5-10.8 27.1-14.6 9.6-3.8 19.7-6.4 30-7.9 10.3-1.5 20.8-1.8 31.2-0.7 10.4 1.1 20.6 3.6 30.3 7.4 9.7 3.8 18.8 8.8 27.1 15 8.3 6.2 15.8 13.4 22.4 21.5 6.6 8.1 12.2 16.9 16.6 26.3 4.4 9.4 7.6 19.3 9.5 29.6z" fill="#07C160"/>
        </svg>
      </div>
      <div class="sub-text">
        微信授权中
        <span class="dots">
          <span class="dot"></span>
          <span class="dot"></span>
          <span class="dot"></span>
        </span>
      </div>
    </div>
  </div>
</template>
<script>
import { tokenUtils } from '@/utils/axios.js';
import { useLoginStore } from '@/stores/login.js'
import { storeToRefs } from 'pinia';
export default {
  setup() {
    const { lastRouteInfo } = storeToRefs(useLoginStore())
    return { lastRouteInfo }
  },
  data() {
    return {
    }
  },
  watch() {
  },
  computed: {
    code() {
      return this.$route.query.code
    },
    state() {
      return this.$route.query.state
    }
  },
  created() {
    if (this.code && this.state) {
      this.authSuccess()
    } else {
      this.redirectUrl()
    }
  },
  methods: {
    redirectUrl() {
      const params = {
        targetId: '',
        redirectUrl: encodeURIComponent(window.location.href),
      }
      this.$axios.get('/system/auth/staff/checkin/wx-auth-redirect', { params }).then(res => {
        if (res.data.code == 0) {
          window.location.replace(res.data.data)
        } else {
          this.$message.error(res.data.msg)
        }
      })
    },
    authSuccess() {
      const params = {
        targetId: '',
        code: this.code,
        state: this.state
      }
      this.$axios.get('/system/auth/staff/checkin/auth-success/null', { params }).then(res => {
        if (res.data.code == 0) {
          const resData = res.data.data || {}
          if (resData.userId) {
            tokenUtils.setTokens(resData.accessToken, resData.refreshToken)
            const path = localStorage.getItem('verify_url')
            if (path) {
              this.$router.replace(path)
            }
          } else {
            this.$router.replace({
              path: '/h5/bind',
              query: {
                state: resData.state,
                openid: resData.openid,
                code: resData.code
              }
            })
          }
        } else {
          this.$message.error(res.data.msg)
        }
      })
    }
  }
}
</script>
<style scoped>
.wx-login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.loading-wrapper {
  text-align: center;
  padding: 60px 40px;
}
.wechat-icon {
  width: 100px;
  height: 100px;
  margin: 0 auto 30px;
  animation: iconPulse 2s ease-in-out infinite;
}
.wechat-icon svg {
  width: 100%;
  height: 100%;
  filter: drop-shadow(0 8px 16px rgba(7, 193, 96, 0.3));
}
@keyframes iconPulse {
  0%, 100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.1);
  }
}
.loading-text {
  font-size: 28px;
  font-weight: 600;
  color: #333;
  margin-bottom: 16px;
  display: inline-block;
}
.text-item {
  display: inline-block;
  opacity: 0;
  animation: textFadeIn 0.5s ease-out forwards;
}
.text-item:nth-child(1) {
  animation-delay: 0.2s;
}
.text-item:nth-child(2) {
  animation-delay: 0.4s;
}
.text-item:nth-child(3) {
  animation-delay: 0.6s;
}
.text-item:nth-child(4) {
  animation-delay: 0.8s;
}
@keyframes textFadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
.dots {
  display: inline-block;
  margin-left: 8px;
  vertical-align: middle;
}
.dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  background: #07C160;
  border-radius: 50%;
  margin: 0 3px;
  animation: dotBounce 1.4s ease-in-out infinite both;
}
.dot:nth-child(1) {
  animation-delay: -0.32s;
}
.dot:nth-child(2) {
  animation-delay: -0.16s;
}
.dot:nth-child(3) {
  animation-delay: 0;
}
@keyframes dotBounce {
  0%, 80%, 100% {
    transform: scale(0);
    opacity: 0.5;
  }
  40% {
    transform: scale(1);
    opacity: 1;
  }
}
.sub-text {
  font-size: 18px;
  color: #999;
  animation: fadeInOut 2s ease-in-out infinite;
}
@keyframes fadeInOut {
  0%, 100% {
    opacity: 0.5;
  }
  50% {
    opacity: 1;
  }
}
</style>
src/views/h5/signup/index.vue
@@ -77,11 +77,11 @@
      distance: null,
      positionError: false,
      positionName: '',
      positionAddress: '南山区打石二路南118号',
      positionAddress: '',
      currentTimeText: '',
      centerPoint: {
        lat: 22.580372,
        lng: 113.946530
        lat: 23.135618,
        lng: 113.27077
      }
    }
  },
@@ -104,6 +104,9 @@
    },
    appId() {
      return this.$route.query.appId
    },
    url() {
      return this.$route.query.url
    }
  },
  created() {
@@ -118,10 +121,12 @@
      this.$axios.get('/exam/verify-record/get-by-application-id', { params }).then(res => {
        if (res.data.code == 0) {
          const resData = res.data.data || {}
          // this.centerPoint = {
          //   lat: resData.examSite?.locationLat,
          //   lng: resData.examSite?.locationLng
          // }
          if (resData.examSite?.locationLat && resData.examSite?.locationLng) {
            this.centerPoint = {
              lat: resData.examSite?.locationLat,
              lng: resData.examSite?.locationLng
            }
          }
          this.positionAddress = resData.examSite?.address
        } else {
          this.$message.error(res.data.msg)
@@ -140,22 +145,29 @@
      }
    },
    signinConfirm() {
      if (!this.canSignup) {
      if (!this.canSignup || this.confirmLoading) {
        return
      }
      this.$message.success('签到成功')
      localStorage.setItem('isSignup', true)
      setTimeout(() => {
        if (this.getIsFace()) {
          this.$router.replace({ path: '/h5/face', query: { appId: this.appId }})
      const data = {
        targetId: this.appId,
        targetType: 2,
        url: this.url,
        type: 0
      }
      this.confirmLoading = true
      this.$axios.post('/exam/staff/checkin', data).then(res => {
        if (res.data.code == 0) {
          this.$message.success('签到成功')
          setTimeout(() => {
            this.$router.replace({ path: '/h5/verForm', query: { appId: this.appId }})
          }, 500)
        } else {
          this.$router.replace({ path: '/h5/verForm', query: { appId: this.appId }})
          this.$message.error(res.data.msg)
        }
      }, 500)
      }).finally(() => {
        this.confirmLoading = false
      })
    },
    getIsFace() {
      return Boolean(localStorage.getItem('isFace'))
    }
  }
}
</script>
src/views/h5/verify/form.vue
@@ -1,5 +1,5 @@
<template>
  <div v-if="pdfUrl">
  <div>
    <el-row class="p-3 m-0" justify="space-between" align="middle">
      <el-col :span="4"></el-col>
      <el-col :span="16">
@@ -28,6 +28,7 @@
      <div v-if="pdfUrl" :style="{width: '100%', height: `${mainHeight - 100}px`}">
        <PdfPreview v-if="pdfUrl" :url="pdfUrl"></PdfPreview>
      </div>
      <el-text class="ml-2 text-info">考点申报文件加载失败...</el-text>
      <div class="p-2 my-4">
        <el-form ref="verifyForm" :model="form">
          <el-form-item label="*以上申报内容是否属实" prop="isVerified">
@@ -125,7 +126,7 @@
      this.$axios.get('/exam/verify-record/get-by-application-id', { params }).then(res => {
        if (res.data.code == 0) {
          const resData = res.data.data || {}
          this.pdfUrl = this.$qxueyou.qxyRes + resData.examSiteVerifyFile
          this.pdfUrl = resData.examSiteVerifyFile ? this.$qxueyou.qxyRes + resData.examSiteVerifyFile : ''
          this.title = resData.organizationName + '-' + resData.examSite.siteName + '考点核验'
          if (resData.id) {
            this.form.isContentTrue  = resData.isContentTrue
src/views/h5/verify/index.vue
@@ -2,33 +2,27 @@
  <div></div>
</template>
<script>
import { tokenUtils } from '@/utils/axios.js';
export default {
  components: {},
  data() {
    return {}
  },
  computed: {
    query() {
      return this.$route.query
    },
    appId() {
      return this.query.appId
      return this.$route.query.appId
    }
  },
  async created() {
    const canVerify = await this.getCanVerify()
    if (canVerify) {
      if (!this.getIsFace()) {
        this.$router.replace({ path: '/h5/face', query: { appId: this.appId }})
      } else if (!this.getIsSignup()) {
        this.$router.replace({ path: '/h5/signup', query: { appId: this.appId } })
      } else {
        this.$router.replace({ path: '/h5/verForm', query: { appId: this.appId }})
      }
    } else {
    if (!canVerify) {
      this.$router.replace('/h5/noVerAccess')
      return
    }
    const checkinExist = await this.getCheckinExist()
    if (checkinExist) {
      this.$router.replace({ path: '/h5/verForm', query: { appId: this.appId }})
    } else {
      this.$router.replace({ path: '/h5/face', query: { appId: this.appId }})
    }
  },
  mounted() {
@@ -38,7 +32,7 @@
    getCanVerify() {
      return new Promise((resolve) => {
        const params = {
          applicationId: this.$route.query.appId
          applicationId: this.appId
        }
        this.$axios.get('/exam/verify-record/can-verify', { params }).then(res => {
          if (res.data.code == 0) {
@@ -51,12 +45,22 @@
        })
      })
    },
    getIsFace() {
      return Boolean(localStorage.getItem('isFace'))
    getCheckinExist() {
      return new Promise((resolve) => {
        const params = {
          targetId: this.appId
        }
        this.$axios.get('/exam/staff/checkin/exist', { params }).then(res => {
          if (res.data.code == 0) {
            resolve(res.data.data)
          } else {
            resolve(false)
          }
        }, () => {
          resolve(false)
        })
      })
    },
    getIsSignup() {
      return Boolean(localStorage.getItem('isSignup'))
    }
  }
}
</script>
src/views/main/components/UploadIdCard.vue
@@ -151,7 +151,7 @@
        file: UploadRequestOptions.file,
        directory: ''
      }
      this.$axios.post('/infra/file/upload', data, {
      this.$axios.post('/infra/file/exam/upload', data, {
        headers: { 'Content-Type': "multipart/form-data" }
      }).then(res => {
        let index = this.list.findIndex(ele => ele.uid == data.file.uid)
vite.config.js
@@ -14,6 +14,7 @@
    },
  },
  server: {
    allowedHosts: ['dev.qxueyou.com'],
    host: '0.0.0.0',
    proxy: {
      '/app-api': {