Files
crmeb-mp/pages/annex/auth/index.vue
Karlcarlo c216865852 feat(user-center): 授权页 submitAuth 带 unionid(用户中心身份键,预留功能)
- 新增 getUnionid()(store/Cache 'UNIONID')
- POST /auth/profile 增加 unionid 参数:box_app 授权成功自动推送用户中心并回写 openid→unionid 映射
- 没拿到 unionid 也不影响授权(后端映射兜底)
- 配套:box_app 1269058(已部署);需重新发版生效
2026-09-19 17:41:02 +08:00

231 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="auth-page">
<!-- 顶部说明 -->
<view class="auth-head">
<text class="auth-title">授权完善资料</text>
<text class="auth-sub">用于在应用里展示你的昵称 / 头像 / 手机号</text>
</view>
<!-- 授权表单卡片 -->
<view class="auth-card">
<!-- 头像:圆形选择区,点击选择;图片填满圆并裁剪,文字放圆外 -->
<view class="auth-avatar-wrap">
<button class="auth-avatar" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image v-if="authAvatar" class="auth-ava" :src="authAvatar" mode="aspectFill" />
<view v-else class="auth-ava-plus">+</view>
</button>
<text class="auth-ava-label">{{ authAvatar ? '点击更换头像' : '点击选择头像' }}</text>
</view>
<view class="auth-field">
<input class="auth-input" type="nickname" v-model="authNickname" placeholder="输入昵称" maxlength="16" placeholder-class="auth-ph" />
</view>
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhone">{{ authPhone ? '✅ 已授权手机号' : '获取手机号' }}</button>
<view class="auth-actions">
<button class="auth-cancel" @tap="cancel">取消</button>
<button class="auth-ok" @tap="submitAuth">保存并返回</button>
</view>
</view>
<text class="auth-tip">微信授权仅用于在应用内展示你的资料,不会泄露给第三方。</text>
</view>
</template>
<script>
import store from '@/store';
import { HTTP_REQUEST_URL } from '@/config/app';
import { LOGIN_STATUS } from '@/config/cache';
function base64Decode(str) {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
var output = '';
var i = 0;
str = String(str).replace(/[^A-Za-z0-9+/=]/g, '');
while (i < str.length) {
var b1 = chars.indexOf(str.charAt(i++));
var b2 = chars.indexOf(str.charAt(i++));
var b3 = chars.indexOf(str.charAt(i++));
var b4 = chars.indexOf(str.charAt(i++));
output += String.fromCharCode((b1 << 2) | (b2 >> 4));
if (b3 !== 64) output += String.fromCharCode(((b2 & 15) << 4) | (b3 >> 2));
if (b4 !== 64) output += String.fromCharCode(((b3 & 3) << 6) | b4);
}
return output;
}
function getOpenid() {
let oid = store.state.app.openid;
if (oid) return oid;
try { oid = uni.getStorageSync('OPENID'); } catch (e) {}
if (oid) return oid;
try {
const token = store.state.app.token || uni.getStorageSync(LOGIN_STATUS);
if (token) {
let parts = token.split('.');
if (parts.length === 3) {
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
while (payload.length % 4 !== 0) payload += '=';
const data = JSON.parse(base64Decode(payload));
oid = data.openid || '';
if (oid) { store.commit('SET_OPENID', oid); return oid; }
}
}
} catch (e) {
console.warn('[auth] JWT decode openid failed:', e.message);
}
return '';
}
/* unionid:用户中心身份键(预留功能)。小程序登录态静默缓存(store/Cache 'UNIONID');
带上后 box_app 授权成功自动推送用户中心并回写 openid→unionid 映射。 */
function getUnionid() {
let uid = store.state.app.unionid;
if (uid) return uid;
try { uid = uni.getStorageSync('UNIONID'); } catch (e) {}
return uid || '';
}
export default {
data() {
return {
authNickname: '',
authAvatar: '',
authPhone: false, // 手机号是否已授权(UI 反馈)
_authCode: '',
openid: ''
}
},
onLoad(options) {
this.openid = getOpenid();
/* H5 经 navigateTo query 带来的「发起授权的当前地址」(backUrl,已 encode):
成功时写进 __AUTH_RESULT__,宿主 onShow 优先按它回传 —— 从哪个 H5 触发的
授权就回到哪个 H5(宫格 location.href 换应用后,shareHref 可能还是旧应用的)。 */
let backUrl = (options && options.backUrl) || '';
if (backUrl) {
try { backUrl = decodeURIComponent(backUrl); } catch (e) { /* 已是原文 */ }
}
this._backUrl = backUrl;
if (!this.openid) uni.showToast({ title: '未获得 openid,请先登录', icon: 'none' });
},
methods: {
onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl;
if (!avatarUrl) return uni.showToast({ title: '未获取到头像', icon: 'none' });
/* 上传到 box_app 公开接口(无鉴权):定制授权页设计为匿名可用,
不依赖 CRMEB 登录态 —— 用 CRMEB 的 uploadImgs 在退出登录后会 401 弹「请登录」。 */
uni.showLoading({ title: '上传头像中…' });
uni.uploadFile({
url: 'https://mp.yizhezhe.com/api/upload',
filePath: avatarUrl,
name: 'file',
success: (res) => {
try {
const d = JSON.parse(res.data);
if (d.code === 200 && d.result && d.result.url) {
this.authAvatar = d.result.url;
} else {
uni.showToast({ title: '头像上传失败', icon: 'none' });
}
} catch (err) {
uni.showToast({ title: '头像上传失败', icon: 'none' });
}
},
fail: () => uni.showToast({ title: '头像上传失败,请检查网络', icon: 'none' }),
complete: () => uni.hideLoading()
});
},
onGetPhone(e) {
if (e.detail.errMsg === 'getPhoneNumber:ok' && e.detail.code) {
this._authCode = e.detail.code;
this.authPhone = true;
} else {
uni.showToast({ title: '未授权手机号', icon: 'none' });
}
},
submitAuth() {
if (!this.authNickname && !this.authAvatar && !this._authCode) {
return uni.showToast({ title: '请至少授权一项', icon: 'none' });
}
const openid = getOpenid() || this.openid;
if (!openid) return uni.showToast({ title: '未获得 openid,请先登录', icon: 'none' });
/* unionid:用户中心身份键(预留)。带上后 box_app 授权成功自动推送用户中心
并回写 openid→unionid 映射;没拿到也不影响授权(后端可映射兜底)。 */
const unionid = getUnionid();
const api = 'https://mp.yizhezhe.com/api/mp/public/auth/profile?openid=' + encodeURIComponent(openid) +
(unionid ? '&unionid=' + encodeURIComponent(unionid) : '') +
(this._authCode ? '&code=' + encodeURIComponent(this._authCode) : '') +
(this.authNickname ? '&nickname=' + encodeURIComponent(this.authNickname) : '') +
(this.authAvatar ? '&avatar=' + encodeURIComponent(this.authAvatar) : '');
uni.request({
url: api, method: 'POST', timeout: 10000,
success: (r) => {
const d = r.data || {};
const res = d.result || {};
// ⚠️ 成功判据必须核对「手机号真的拿到了」:提交了 code 时,
// 后端解密失败(如 access_token 被商城后端挤掉)会如实返回
// code!=200;旧版后端也可能 200 但 result.phone 为空。
// 两种情况都不能写 __AUTH_RESULT__、不能提示成功 —— 否则 H5
// 收到空 phone 的回传,闸门拦截,用户会陷入「授权成功却又
// 要求授权」的死循环(2026-09-18 孕检日历实测)。
const phoneMissing = !!this._authCode && !res.phone;
if (d.code === 200 && !phoneMissing) {
uni.setStorageSync('__AUTH_RESULT__', {
openid: openid,
phone: res.phone || '',
nickname: res.userName || this.authNickname || '',
avatar: res.avatar || this.authAvatar || '',
backUrl: this._backUrl || ''
});
uni.showToast({ title: '授权成功', icon: 'success' });
setTimeout(() => {
uni.navigateBack();
}, 600);
} else {
uni.showToast({ title: (d.message || '授权保存失败,请稍后重试'), icon: 'none', duration: 3000 });
}
},
fail: () => uni.showToast({ title: '授权请求失败,请检查网络', icon: 'none' })
});
},
cancel() { uni.navigateBack(); }
}
}
</script>
<style scoped>
/* 清 button 默认边框/padding(避免撑出卡片 + 消除默认灰底边框)。
不改 margin —— 统配 margin:0 会压掉各按钮自己的 margin-top,导致元素紧贴。 */
.auth-page button::after { border: none; }
.auth-page button { padding: 0; line-height: normal; box-sizing: border-box; border: none; -webkit-appearance: none; }
.auth-page input { box-sizing: border-box; margin: 0; }
.auth-page { min-height: 100vh; background: #F8F3EF; padding: 24px 20px 40px; display: flex; flex-direction: column; align-items: center; }
.auth-head { text-align: center; margin-top: 16px; }
.auth-title { font-size: 20px; font-weight: 800; color: #2B2320; display: block; }
.auth-sub { font-size: 13px; color: #A79C95; margin-top: 8px; display: block; }
.auth-card { width: 100%; max-width: 340px; background: #fff; border-radius: 18px; padding: 28px 20px 20px; margin-top: 28px; box-shadow: 0 6px 24px rgba(0,0,0,.05); display: flex; flex-direction: column; align-items: center; }
/* 头部:圆形头像选择区(图片填满圆并裁剪),说明文字放圆外下方 */
.auth-avatar-wrap { display: flex; flex-direction: column; align-items: center; }
.auth-avatar { width: 92px; height: 92px; border-radius: 50%; background: #F3E9E3; display: flex; align-items: center; justify-content: center; overflow: hidden; border: 1px dashed #D9C4B8; flex: 0 0 auto; }
.auth-ava { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.auth-ava-plus { font-size: 30px; color: #C9B7AC; line-height: 1; }
.auth-ava-label { font-size: 11px; color: #A79C95; margin-top: 8px; }
/* 昵称输入 */
.auth-field { width: 100%; margin-top: 22px; }
.auth-input { width: 100%; height: 48px; border-radius: 12px; background: #F8F3EF; font-size: 14px; color: #2B2320; padding: 0 14px; box-sizing: border-box; }
.auth-ph { color: #C9B7AC; }
/* 获取手机号按钮 */
.auth-btn { width: 100%; height: 48px; border-radius: 12px; background: #F8F3EF; color: #2B2320; font-size: 14px; margin-top: 14px; display: flex; align-items: center; justify-content: center; }
.auth-actions { display: flex; gap: 12px; margin-top: 24px; width: 100%; }
.auth-cancel, .auth-ok { flex: 1; height: 46px; border-radius: 12px; font-size: 14px; font-weight: 600; display: flex; align-items: center; justify-content: center; }
.auth-cancel { background: #F1E6E0; color: #6B615B; }
.auth-ok { background: #F4593C; color: #fff; }
.auth-tip { font-size: 11px; color: #A79C95; margin-top: 20px; text-align: center; line-height: 1.5; }
</style>