|
|
@@ -0,0 +1,437 @@
|
|
|
+<script setup>
|
|
|
+import { ref } from 'vue'
|
|
|
+import { onLoad } from '@dcloudio/uni-app'
|
|
|
+import { useUserStore } from '@/stores/user'
|
|
|
+import { DEFAULT_PLACEHOLDER_IMAGE_URL } from '@/utils/staticAsset'
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+const sessionId = ref('')
|
|
|
+const done = ref(false)
|
|
|
+const agreeProtocol = ref(false)
|
|
|
+const isLoading = ref(false)
|
|
|
+const protocolDialogVisible = ref(false)
|
|
|
+const protocolDialogForWechat = ref(false)
|
|
|
+
|
|
|
+onLoad((options) => {
|
|
|
+ const opts = options || {}
|
|
|
+ let sid = ''
|
|
|
+ if (opts.scene) {
|
|
|
+ try {
|
|
|
+ sid = decodeURIComponent(String(opts.scene))
|
|
|
+ } catch (e) {
|
|
|
+ sid = String(opts.scene)
|
|
|
+ }
|
|
|
+ } else if (opts.sessionId) {
|
|
|
+ sid = String(opts.sessionId)
|
|
|
+ }
|
|
|
+ sid = (sid || '').trim()
|
|
|
+ sessionId.value = sid
|
|
|
+ if (!sid) return
|
|
|
+ userStore.webQrScanned(sid).catch(() => {
|
|
|
+ /* 软失败:PC 仍可继续轮询或用户可重试授权 */
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+const goUserAgreement = () => {
|
|
|
+ uni.navigateTo({ url: '/pages/login/user-agreement' })
|
|
|
+}
|
|
|
+
|
|
|
+const goPrivacyPolicy = () => {
|
|
|
+ uni.navigateTo({ url: '/pages/login/privacy-policy' })
|
|
|
+}
|
|
|
+
|
|
|
+const toggleProtocolAgreement = () => {
|
|
|
+ agreeProtocol.value = !agreeProtocol.value
|
|
|
+}
|
|
|
+
|
|
|
+const resolveProtocolDialog = (confirmed) => {
|
|
|
+ protocolDialogVisible.value = false
|
|
|
+ protocolDialogForWechat.value = false
|
|
|
+ if (confirmed) {
|
|
|
+ agreeProtocol.value = true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onWechatGate = () => {
|
|
|
+ if (isLoading.value || done.value) return
|
|
|
+ protocolDialogForWechat.value = true
|
|
|
+ protocolDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const onAgreeGetPhoneNumber = (e) => {
|
|
|
+ agreeProtocol.value = true
|
|
|
+ protocolDialogVisible.value = false
|
|
|
+ protocolDialogForWechat.value = false
|
|
|
+ onGetPhoneNumber(e)
|
|
|
+}
|
|
|
+
|
|
|
+const onGetPhoneNumber = async (e) => {
|
|
|
+ if (isLoading.value || done.value || !sessionId.value) return
|
|
|
+ if (!agreeProtocol.value) {
|
|
|
+ onWechatGate()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const detail = (e && e.detail) || {}
|
|
|
+ const errMsg = detail.errMsg || ''
|
|
|
+ if ((errMsg && errMsg.indexOf('ok') === -1) || !detail.code) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '需要授权手机号才能完成扫码登录',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ isLoading.value = true
|
|
|
+ try {
|
|
|
+ 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 result = await userStore.webQrConfirm({
|
|
|
+ sessionId: sessionId.value,
|
|
|
+ loginCode,
|
|
|
+ phoneCode: detail.code
|
|
|
+ })
|
|
|
+ if (result && result.ok) {
|
|
|
+ done.value = true
|
|
|
+ uni.showToast({
|
|
|
+ title: result.message || '电脑端即将登录,可关闭本页',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.showToast({
|
|
|
+ title: (result && result.message) || '授权失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ } catch (err) {
|
|
|
+ uni.showToast({
|
|
|
+ title: (err && err.message) || '授权失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <view class="web-qr-page">
|
|
|
+ <view class="web-qr-brand">
|
|
|
+ <image class="web-qr-brand__logo" :src="DEFAULT_PLACEHOLDER_IMAGE_URL" mode="widthFix" />
|
|
|
+ <view class="web-qr-brand__title">网页登录授权</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="!sessionId" class="web-qr-card web-qr-card--error">
|
|
|
+ <text class="web-qr-msg">请从电脑登录页重新扫码</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else-if="done" class="web-qr-card web-qr-card--ok">
|
|
|
+ <text class="web-qr-msg web-qr-msg--ok">电脑端即将登录,可关闭本页</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else class="web-qr-card">
|
|
|
+ <text class="web-qr-desc">正在登录电脑端管理后台,请授权手机号完成验证</text>
|
|
|
+
|
|
|
+ <view class="footer-line">
|
|
|
+ <view class="agree-check" @tap="toggleProtocolAgreement">
|
|
|
+ <view class="agree-check__icon" :class="{ 'agree-check__icon--checked': agreeProtocol }">
|
|
|
+ <text class="agree-check__tick">✓</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text class="footer-text">授权即表示同意</text>
|
|
|
+ <text class="link" @tap="goUserAgreement">《用户服务协议》</text>
|
|
|
+ <text class="footer-text">和</text>
|
|
|
+ <text class="link" @tap="goPrivacyPolicy">《隐私政策》</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <button
|
|
|
+ v-if="agreeProtocol"
|
|
|
+ class="web-qr-btn"
|
|
|
+ :disabled="isLoading"
|
|
|
+ open-type="getPhoneNumber"
|
|
|
+ @getphonenumber="onGetPhoneNumber"
|
|
|
+ >
|
|
|
+ <text class="web-qr-btn__label">{{ isLoading ? '授权中...' : '授权手机号并登录电脑端' }}</text>
|
|
|
+ </button>
|
|
|
+ <view
|
|
|
+ v-else
|
|
|
+ class="web-qr-btn"
|
|
|
+ :class="{ 'is-disabled': isLoading }"
|
|
|
+ @tap="onWechatGate"
|
|
|
+ >
|
|
|
+ <text class="web-qr-btn__label">授权手机号并登录电脑端</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view
|
|
|
+ v-if="protocolDialogVisible"
|
|
|
+ class="protocol-dialog-mask"
|
|
|
+ @tap="resolveProtocolDialog(false)"
|
|
|
+ >
|
|
|
+ <view class="protocol-dialog" @tap.stop>
|
|
|
+ <text class="protocol-dialog__title">协议确认</text>
|
|
|
+ <text class="protocol-dialog__desc">授权前请先同意《用户服务协议》和《隐私政策》</text>
|
|
|
+ <view class="protocol-dialog__actions">
|
|
|
+ <view
|
|
|
+ class="protocol-dialog__btn protocol-dialog__btn--cancel"
|
|
|
+ @tap="resolveProtocolDialog(false)"
|
|
|
+ >
|
|
|
+ 否
|
|
|
+ </view>
|
|
|
+ <button
|
|
|
+ v-if="protocolDialogForWechat"
|
|
|
+ class="protocol-dialog__btn protocol-dialog__btn--confirm"
|
|
|
+ open-type="getPhoneNumber"
|
|
|
+ @getphonenumber="onAgreeGetPhoneNumber"
|
|
|
+ >
|
|
|
+ 同意并授权
|
|
|
+ </button>
|
|
|
+ <view
|
|
|
+ v-else
|
|
|
+ class="protocol-dialog__btn protocol-dialog__btn--confirm"
|
|
|
+ @tap="resolveProtocolDialog(true)"
|
|
|
+ >
|
|
|
+ 是,继续
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.web-qr-page {
|
|
|
+ min-height: 100vh;
|
|
|
+ padding: 120rpx 48rpx 64rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-brand {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 48rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-brand__logo {
|
|
|
+ display: block;
|
|
|
+ width: 320rpx;
|
|
|
+ margin: 0 auto;
|
|
|
+ border-radius: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-brand__title {
|
|
|
+ margin-top: 24rpx;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0a9f6d;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-card {
|
|
|
+ background: #f7faf8;
|
|
|
+ border: 1px solid #e2ebe6;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 40rpx 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-card--error .web-qr-msg {
|
|
|
+ color: #c45656;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-card--ok {
|
|
|
+ background: #e9f9f2;
|
|
|
+ border-color: #a7f3d0;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-desc {
|
|
|
+ display: block;
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: #334155;
|
|
|
+ margin-bottom: 36rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-msg {
|
|
|
+ display: block;
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 1.6;
|
|
|
+ text-align: center;
|
|
|
+ color: #334155;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-msg--ok {
|
|
|
+ color: #087f57;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.footer-line {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ column-gap: 4rpx;
|
|
|
+ row-gap: 8rpx;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.agree-check {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 8rpx;
|
|
|
+ margin-right: 4rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.agree-check__icon {
|
|
|
+ width: 30rpx;
|
|
|
+ height: 30rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 2rpx solid #c0c4cc;
|
|
|
+ box-sizing: border-box;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.agree-check__tick {
|
|
|
+ font-size: 18rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 1;
|
|
|
+ color: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.agree-check__icon--checked {
|
|
|
+ border-color: #0a9f6d;
|
|
|
+ background: #0a9f6d;
|
|
|
+}
|
|
|
+
|
|
|
+.agree-check__icon--checked .agree-check__tick {
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.footer-text,
|
|
|
+.link {
|
|
|
+ font-size: 22rpx;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
+.footer-text {
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.link {
|
|
|
+ color: #0a9f6d;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-btn {
|
|
|
+ margin-top: 32rpx;
|
|
|
+ height: 88rpx;
|
|
|
+ border-radius: 44rpx;
|
|
|
+ background: #0a9f6d;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: none;
|
|
|
+ padding: 0;
|
|
|
+ line-height: 1;
|
|
|
+ width: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-btn.is-disabled {
|
|
|
+ opacity: 0.75;
|
|
|
+}
|
|
|
+
|
|
|
+.web-qr-btn__label {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+button.web-qr-btn::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.protocol-dialog-mask {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.42);
|
|
|
+ z-index: 99;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 40rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.protocol-dialog {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 620rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 36rpx 30rpx 28rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ box-shadow: 0 14rpx 48rpx rgba(0, 0, 0, 0.16);
|
|
|
+}
|
|
|
+
|
|
|
+.protocol-dialog__title {
|
|
|
+ display: block;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.protocol-dialog__desc {
|
|
|
+ display: block;
|
|
|
+ margin-top: 16rpx;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 26rpx;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.protocol-dialog__actions {
|
|
|
+ margin-top: 30rpx;
|
|
|
+ display: flex;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.protocol-dialog__btn {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 78rpx;
|
|
|
+ border-radius: 999rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.protocol-dialog__btn--cancel {
|
|
|
+ background: #f4f7f6;
|
|
|
+ color: #5b6b64;
|
|
|
+ border: 1px solid #ddebe5;
|
|
|
+}
|
|
|
+
|
|
|
+.protocol-dialog__btn--confirm {
|
|
|
+ background: #0a9f6d;
|
|
|
+ color: #fff;
|
|
|
+ border: none;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ line-height: normal;
|
|
|
+}
|
|
|
+
|
|
|
+button.protocol-dialog__btn--confirm::after {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+</style>
|