chore: 入库生产基线(2026-10-26 工作树快照)
- 含首页壳/容器页/授权页/分享图/首页配置等此前未入库的在途改动 - 新增未跟踪的 pages/index/mall.vue(第 5 个 tabBar 页,pages.json 已引用) - 排除本地备份文件(*.bak* / *.origin-bak) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,43 +1,142 @@
|
||||
<template>
|
||||
<view class="auth-page">
|
||||
<!-- 顶部说明 -->
|
||||
<view class="auth-head">
|
||||
<text class="auth-title">授权完善资料</text>
|
||||
<text class="auth-sub">用于在应用里展示你的昵称 / 头像 / 手机号</text>
|
||||
<!-- ============ 可滚动内容区 ============ -->
|
||||
<view class="home">
|
||||
<!-- 未授权:价值主张卡(先说清"有什么好处",再要授权) -->
|
||||
<view v-if="!phoneBound" class="pitch">
|
||||
<view class="pbadge">全部免费 · 无需付费</view>
|
||||
<view class="pttl">{{ pitchTitle }}</view>
|
||||
<view class="pttl pttl2 em">授权后全部免费用</view>
|
||||
<view class="psub">胎动、体重、产检、血糖、待产清单……</view>
|
||||
<view class="psub">从孕期到产后,一个地方全记下来</view>
|
||||
<view class="social">
|
||||
<view class="dots">
|
||||
<image v-for="(d, i) in SOCIAL_AVATARS" :key="i" class="dot" :src="d" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="social-txt">已有 {{ SOCIAL_COUNT }} 位妈妈在用</text>
|
||||
</view>
|
||||
<button class="cta" @tap="openPhoneSheet">授权手机号,立即免费体验</button>
|
||||
<view class="ctahint">微信授权 · 10 秒完成 · 仅用于创建你的孕育档案</view>
|
||||
<view class="trust">
|
||||
<text class="trust-i"><text class="trust-tick">✓</text>全部免费</text>
|
||||
<text class="trust-i"><text class="trust-tick">✓</text>无需下载 App</text>
|
||||
<text class="trust-i"><text class="trust-tick">✓</text>换手机不丢</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已授权:收起成一条状态,不再占地方 -->
|
||||
<view v-else class="hero">
|
||||
<view class="ava">
|
||||
<image v-if="heroAvatar" class="ava-img" :src="heroAvatar" mode="aspectFill" />
|
||||
<text v-else class="ava-emoji">📱</text>
|
||||
</view>
|
||||
<view class="who">
|
||||
<view class="nm">{{ heroName }}</view>
|
||||
<view class="st">{{ heroSub }}</view>
|
||||
</view>
|
||||
<view class="ok">已授权</view>
|
||||
</view>
|
||||
|
||||
<template v-if="apps.length">
|
||||
<view class="secthead">
|
||||
<text class="secttl">孕期工具</text>
|
||||
<text class="sectmore">共 {{ apps.length }} 个 ›</text>
|
||||
</view>
|
||||
<view v-if="!phoneBound" class="freebar">
|
||||
<text class="freepill">全部免费</text>
|
||||
<text class="freetxt">{{ apps.length }} 款工具当前未解锁,授权手机号后立即可用</text>
|
||||
</view>
|
||||
<!-- 工具宫格:数据来自 box_app 后台在架应用,**仅展示不可点击**;
|
||||
未授权时右上角显示小锁(授权后解锁,仍不响应点击)。 -->
|
||||
<view class="grid" :class="{ locked: !phoneBound }">
|
||||
<view v-for="a in apps" :key="a.id" class="tile">
|
||||
<view class="ic">
|
||||
<image v-if="a.logo" class="tile-logo" :src="a.logo" mode="aspectFit" />
|
||||
<text v-else class="tile-fb">{{ (a.name || '·').slice(0, 1) }}</text>
|
||||
</view>
|
||||
<text class="tn">{{ a.name }}</text>
|
||||
<view class="lock">
|
||||
<image class="lock-ic" :src="LOCK_ICON" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</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>
|
||||
<!-- 常驻底部行动条:用户滚到哪一屏都能点 -->
|
||||
<view v-if="!phoneBound" class="stickycta">
|
||||
<button class="cta cta2" @tap="openPhoneSheet">{{ stickyText }}</button>
|
||||
</view>
|
||||
|
||||
<!-- 遮罩(演示里点遮罩不关闭,必须走两个按钮之一) -->
|
||||
<view class="mask" :class="{ show: !!sheet }" @tap="noop"></view>
|
||||
|
||||
<!-- ① 手机号半弹窗:「是」按钮本身必须是 open-type="getPhoneNumber",
|
||||
手机号没有可编程调起的 API;带右上角「原生」角标的才调微信接口。 -->
|
||||
<view class="sheet" :class="{ show: sheet === 'phone' }">
|
||||
<view class="grab"></view>
|
||||
<view class="sheet-h3">是否授权手机号进行体验?</view>
|
||||
<view class="sheet-p">用于创建你的孕育档案,让胎动、体重、产检记录在换设备后也能找回。</view>
|
||||
<view class="btnrow">
|
||||
<button class="bsec" @tap="denyPhone">否</button>
|
||||
<button class="bmain bwx" open-type="getPhoneNumber" @getphonenumber="onGetPhone">
|
||||
是,授权手机号
|
||||
</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>
|
||||
<!-- ④-A 老用户(库里已有昵称/头像):是否复用。普通按钮 —— 复用走库里资料,不调微信接口 -->
|
||||
<view class="sheet" :class="{ show: sheet === 'reuse' }">
|
||||
<view class="grab"></view>
|
||||
<view class="sheet-h3">是否复用你的微信昵称和头像?</view>
|
||||
<view class="avarow">
|
||||
<view class="avarow-big">
|
||||
<image v-if="libAvatar" class="avarow-img" :src="libAvatar" mode="aspectFill" />
|
||||
<text v-else class="avarow-emoji">🐣</text>
|
||||
</view>
|
||||
<view class="avarow-txt">
|
||||
<view class="t1">{{ libNickname || '微信用户' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btnrow">
|
||||
<button class="bsec" @tap="reuseNo">否,稍后再说</button>
|
||||
<button class="bmain" @tap="reuseYes">是,直接用</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ④-B 库里没有昵称/头像:不再追问、不再给表单,直接收尾返回(2026-09-22 定案) -->
|
||||
<!-- 说明:本页只负责「手机号」这一件事 + 可选复用库里已有资料。
|
||||
昵称/头像不由本页采集(getUserProfile 已废弃,且产品定案不在此追问),
|
||||
它们由小程序其它流程/上游用户中心/商城同步维护,见 AGENTS §4.4。 -->
|
||||
|
||||
<!-- 页内 toast -->
|
||||
<view class="toast" :class="{ show: toast.show }">
|
||||
<text v-if="toast.tick" class="toast-tick">✓</text>
|
||||
<text class="toast-msg">{{ toast.msg }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store';
|
||||
import { HTTP_REQUEST_URL } from '@/config/app';
|
||||
import { LOGIN_STATUS } from '@/config/cache';
|
||||
|
||||
/* 共享授权接口(user_auth_router:全 H5 应用共用,openid 单键,不区分应用) */
|
||||
const AUTH_API = 'https://mp.yizhezhe.com/api/mp/public/auth/profile';
|
||||
/* box_app 公开应用列表:宫格按后台登记的在架应用渲染(样式照 design/授权流程演示) */
|
||||
const APPS_API = 'https://mp.yizhezhe.com/api/mp/apps';
|
||||
|
||||
/* ⚠️ 演示页占位数据:上线前必须核实真实数字,否则涉嫌虚假宣传。
|
||||
三个头像是内联 SVG 示意插画(为保持单文件离线可用),不是真实用户头像。 */
|
||||
const SOCIAL_COUNT = '36.5万+';
|
||||
const SOCIAL_AVATARS = [
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCc+PGNpcmNsZSBjeD0nMTInIGN5PScxMicgcj0nMTInIGZpbGw9JyNGN0U0RUEnLz48cGF0aCBkPSdNMi42IDI0Yy43LTQuMyA0LjYtNi45IDkuNC02LjlzOC43IDIuNiA5LjQgNi45eicgZmlsbD0nI0M5N0I5MicvPjxjaXJjbGUgY3g9JzEyJyBjeT0nMTAuNicgcj0nNC4xNScgZmlsbD0nI0YyQzZBQycvPjxlbGxpcHNlIGN4PSc3LjknIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjM0IyQTI0Jy8+PGVsbGlwc2UgY3g9JzE2LjEnIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjM0IyQTI0Jy8+PHBhdGggZD0nTTcuOCAxMC42QzcuOCA2LjUgMTYuMiA2LjUgMTYuMiAxMC42Yy0uOS0yLjEtNy41LTIuMS04LjQgMHonIGZpbGw9JyMzQjJBMjQnLz48L3N2Zz4=',
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCc+PGNpcmNsZSBjeD0nMTInIGN5PScxMicgcj0nMTInIGZpbGw9JyNFRkVBRkEnLz48cGF0aCBkPSdNMi42IDI0Yy43LTQuMyA0LjYtNi45IDkuNC02LjlzOC43IDIuNiA5LjQgNi45eicgZmlsbD0nIzlCODZDOScvPjxjaXJjbGUgY3g9JzEyJyBjeT0nMTAuNicgcj0nNC4xNScgZmlsbD0nI0Y2RDNCQycvPjxlbGxpcHNlIGN4PSc3LjknIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjNUEzQjJFJy8+PGVsbGlwc2UgY3g9JzE2LjEnIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjNUEzQjJFJy8+PHBhdGggZD0nTTcuOCAxMC42QzcuOCA2LjUgMTYuMiA2LjUgMTYuMiAxMC42Yy0uOS0yLjEtNy41LTIuMS04LjQgMHonIGZpbGw9JyM1QTNCMkUnLz48L3N2Zz4=',
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCc+PGNpcmNsZSBjeD0nMTInIGN5PScxMicgcj0nMTInIGZpbGw9JyNFNEYyRUMnLz48cGF0aCBkPSdNMi42IDI0Yy43LTQuMyA0LjYtNi45IDkuNC02LjlzOC43IDIuNiA5LjQgNi45eicgZmlsbD0nIzVGQTk4QycvPjxjaXJjbGUgY3g9JzEyJyBjeT0nMTAuNicgcj0nNC4xNScgZmlsbD0nI0VGQzBBMCcvPjxlbGxpcHNlIGN4PSc3LjknIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjMjQxQzE5Jy8+PGVsbGlwc2UgY3g9JzE2LjEnIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjMjQxQzE5Jy8+PHBhdGggZD0nTTcuOCAxMC42QzcuOCA2LjUgMTYuMiA2LjUgMTYuMiAxMC42Yy0uOS0yLjEtNy41LTIuMS04LjQgMHonIGZpbGw9JyMyNDFDMTknLz48L3N2Zz4='
|
||||
];
|
||||
/* 宫格右上角小锁(未授权时显示;base64 保证 WXSS/小程序端可直接用) */
|
||||
const LOCK_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjOEM3QTZGJyBzdHJva2Utd2lkdGg9JzIuMic+PHJlY3QgeD0nNC41JyB5PScxMC41JyB3aWR0aD0nMTUnIGhlaWdodD0nMTAnIHJ4PScyLjUnLz48cGF0aCBkPSdNOCAxMC41VjcuNWE0IDQgMCAwIDEgOCAwdjMnLz48L3N2Zz4=';
|
||||
|
||||
function base64Decode(str) {
|
||||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
var output = '';
|
||||
@@ -90,141 +189,365 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
authNickname: '',
|
||||
authAvatar: '',
|
||||
authPhone: false, // 手机号是否已授权(UI 反馈)
|
||||
_authCode: '',
|
||||
openid: ''
|
||||
openid: '',
|
||||
/* 当前半弹窗:'' | 'phone' | 'reuse' */
|
||||
sheet: '',
|
||||
phoneBound: false, // 手机号是否已落库(= 价值卡收起 / 宫格解锁)
|
||||
phone: '',
|
||||
libNickname: '', // 共享库里已有的昵称(判据的取值来源)
|
||||
libAvatar: '', // 共享库里已有的头像
|
||||
apps: [],
|
||||
busy: false,
|
||||
toast: { show: false, msg: '', tick: false },
|
||||
SOCIAL_COUNT: SOCIAL_COUNT,
|
||||
SOCIAL_AVATARS: SOCIAL_AVATARS,
|
||||
LOCK_ICON: LOCK_ICON
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pitchTitle() {
|
||||
return (this.apps.length ? this.apps.length + ' 款' : '') + '孕育必备工具';
|
||||
},
|
||||
heroName() {
|
||||
return this.libNickname || this.phone || '已授权';
|
||||
},
|
||||
heroSub() {
|
||||
return this.libNickname ? '资料已复用 · 手机号已绑定' : '手机号已绑定 · 未设置昵称';
|
||||
},
|
||||
heroAvatar() {
|
||||
return this.libAvatar;
|
||||
},
|
||||
/* 应用列表拉取失败时不要显示「解锁 0 款工具」 */
|
||||
stickyText() {
|
||||
return this.apps.length ? '立即免费体验 · 解锁 ' + this.apps.length + ' 款工具' : '立即免费体验';
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.openid = getOpenid();
|
||||
/* H5 经 navigateTo query 带来的「发起授权的当前地址」(backUrl,已 encode):
|
||||
成功时写进 __AUTH_RESULT__,宿主 onShow 优先按它回传 —— 从哪个 H5 触发的
|
||||
结束后写进 __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' });
|
||||
|
||||
/* 宫格数据:失败不影响授权主流程(旁路能力绝不阻塞) */
|
||||
this.fetchApps();
|
||||
|
||||
if (!this.openid) {
|
||||
this.showToast('未获得 openid,请先登录', false, 1500);
|
||||
setTimeout(() => { uni.navigateBack(); }, 1500);
|
||||
return;
|
||||
}
|
||||
/* 进页即问手机号:用户是从 H5「授权完善资料」进来的,意图已经明确,少一次点击 */
|
||||
this.sheet = 'phone';
|
||||
},
|
||||
onUnload() {
|
||||
if (this._toastTimer) clearTimeout(this._toastTimer);
|
||||
},
|
||||
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',
|
||||
/* ---------- 工具宫格:box_app 在架应用(仅展示,不响应点击) ---------- */
|
||||
fetchApps() {
|
||||
uni.request({
|
||||
url: APPS_API,
|
||||
data: { isPublished: 1, pageSize: 200 },
|
||||
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' });
|
||||
const d = res.data || {};
|
||||
const items = (d.result && d.result.items) || d.data || [];
|
||||
if (Array.isArray(items)) {
|
||||
this.apps = items.filter((a) => a && a.name);
|
||||
}
|
||||
},
|
||||
fail: () => uni.showToast({ title: '头像上传失败,请检查网络', icon: 'none' }),
|
||||
complete: () => uni.hideLoading()
|
||||
fail: () => {
|
||||
console.warn('[auth] 应用列表拉取失败,宫格留空(不影响授权)');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* ---------- ① 手机号 ---------- */
|
||||
openPhoneSheet() {
|
||||
if (!this.openid) return this.showToast('未获得 openid,请先登录');
|
||||
this.sheet = 'phone';
|
||||
},
|
||||
denyPhone() {
|
||||
/* 手机号都不授权 = 本次授权放弃:直接回 H5(不写 __AUTH_RESULT__,
|
||||
宿主按现有逻辑重载当前 H5,不会有假成功回流)。 */
|
||||
uni.navigateBack();
|
||||
},
|
||||
onGetPhone(e) {
|
||||
if (e.detail.errMsg === 'getPhoneNumber:ok' && e.detail.code) {
|
||||
this._authCode = e.detail.code;
|
||||
this.authPhone = true;
|
||||
const detail = (e && e.detail) || {};
|
||||
if (detail.errMsg === 'getPhoneNumber:ok' && detail.code) {
|
||||
this.submitPhone(detail.code);
|
||||
} else {
|
||||
uni.showToast({ title: '未授权手机号', icon: 'none' });
|
||||
this.showToast('未授权手机号', false, 1800);
|
||||
}
|
||||
},
|
||||
submitAuth() {
|
||||
if (!this.authNickname && !this.authAvatar && !this._authCode) {
|
||||
return uni.showToast({ title: '请至少授权一项', icon: 'none' });
|
||||
async submitPhone(code) {
|
||||
if (this.busy) return;
|
||||
this.busy = true;
|
||||
uni.showLoading({ title: '授权中…', mask: true });
|
||||
const r = await this.postAuth({ code: code });
|
||||
uni.hideLoading();
|
||||
this.busy = false;
|
||||
|
||||
const phone = (r.res && r.res.phone) || '';
|
||||
/* ⚠️ 成功判据必须核对「手机号真的拿到了」:后端解密失败(如 access_token 被
|
||||
商城后端挤掉)会如实返回 code!=200;旧版后端也可能 200 但 result.phone 为空。
|
||||
两种情况都不能往下走 —— 否则 H5 收到空 phone 的回传,闸门拦截,用户会陷入
|
||||
「授权成功却又要求授权」的死循环(2026-09-18 孕检日历实测)。 */
|
||||
if (!r.ok || !phone) {
|
||||
return this.showToast(r.message || '手机号获取失败,请重试', false, 3000);
|
||||
}
|
||||
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' })
|
||||
|
||||
this.phone = phone;
|
||||
this.libNickname = (r.res && r.res.userName) || '';
|
||||
this.libAvatar = (r.res && r.res.avatar) || '';
|
||||
this.phoneBound = true;
|
||||
this.sheet = '';
|
||||
this.showToast('授权成功', true, 1100);
|
||||
|
||||
/* ④ 查共享库(**必须走 GET,不能只看上面 POST 的返回**):
|
||||
本页**不采集**昵称/头像(getUserProfile 已废弃,昵称/头像只能由用户手势触发
|
||||
input type=nickname / chooseAvatar;且产品定案此页不再追问),展示的永远是
|
||||
「库里已经存过的值」—— 可能来自上一次授权、上游用户中心、或 CRMEB 商城用户表同步。
|
||||
而 GET /auth/profile 才带「先取后授权」兜底回填(用户中心 → CRMEB 同步),
|
||||
POST(apply_auth) 不做回填 —— 只在商城有昵称/头像的用户,只有 GET 能带出来。
|
||||
判据用 !!(userName || avatar),**不能**用「记录是否存在」(接口对任何 openid
|
||||
都返回 200 并现场建一条空记录)。 */
|
||||
const startedAt = Date.now();
|
||||
const lib = await this.fetchLibraryProfile();
|
||||
if (lib) {
|
||||
this.libNickname = lib.userName || '';
|
||||
this.libAvatar = lib.avatar || '';
|
||||
}
|
||||
/* 让「授权成功」toast 至少停 1.1s,查库太快也不闪 */
|
||||
const wait = Math.max(0, 1100 - (Date.now() - startedAt));
|
||||
setTimeout(() => {
|
||||
if (this.libNickname || this.libAvatar) {
|
||||
/* 库里有昵称/头像 → 提示是否复用 */
|
||||
this.sheet = 'reuse';
|
||||
} else {
|
||||
/* 库里没有 → **不追问头像昵称**(本页不采集这两项),直接收尾返回 */
|
||||
this.finish('', '', '授权成功');
|
||||
}
|
||||
}, wait);
|
||||
},
|
||||
|
||||
/* ---------- ④-A 复用库里已有资料 ---------- */
|
||||
async reuseYes() {
|
||||
if (this.busy) return;
|
||||
const extra = {};
|
||||
if (this.libNickname) extra.nickname = this.libNickname;
|
||||
if (this.libAvatar) extra.avatar = this.libAvatar;
|
||||
/* 库里值可能来自 CRMEB 商城同步(authorized=false):这里再写一次,
|
||||
把「复用」视为用户主动授权,置位 authorized 并回写用户中心。 */
|
||||
if (extra.nickname || extra.avatar) {
|
||||
this.busy = true;
|
||||
uni.showLoading({ title: '处理中…', mask: true });
|
||||
const r = await this.postAuth(extra);
|
||||
uni.hideLoading();
|
||||
this.busy = false;
|
||||
if (!r.ok) return this.showToast(r.message, false, 3000);
|
||||
this.libNickname = (r.res && r.res.userName) || this.libNickname;
|
||||
this.libAvatar = (r.res && r.res.avatar) || this.libAvatar;
|
||||
}
|
||||
this.finish(this.libNickname, this.libAvatar, '已复用微信资料');
|
||||
},
|
||||
reuseNo() {
|
||||
/* 不同步:库里资料原样保留,照样把真实值回传给 H5,然后直接返回 */
|
||||
this.finish(this.libNickname, this.libAvatar, '已保留手机号,资料未改动');
|
||||
},
|
||||
|
||||
/* ---------- 收尾:写 __AUTH_RESULT__ → 宿主 onShow 回传 H5 hash ---------- */
|
||||
finish(nickname, avatar, msg) {
|
||||
const openid = this.openid || getOpenid();
|
||||
if (!openid) return this.showToast('未获得 openid,请先登录');
|
||||
try {
|
||||
uni.setStorageSync('__AUTH_RESULT__', {
|
||||
openid: openid,
|
||||
phone: this.phone || '',
|
||||
nickname: nickname || '',
|
||||
avatar: avatar || '',
|
||||
backUrl: this._backUrl || ''
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[auth] 写 __AUTH_RESULT__ 失败:', e && e.message);
|
||||
}
|
||||
this.sheet = '';
|
||||
this.showToast(msg || '授权成功', true, 1000);
|
||||
setTimeout(() => { uni.navigateBack(); }, 700);
|
||||
},
|
||||
|
||||
/* ---------- ④ 查共享库(GET)----------
|
||||
唯一带「先取后授权」兜底回填的入口(本地 → 上游用户中心 → CRMEB 商城同步),
|
||||
所以判「库里是否已有昵称/头像」必须走它,不能拿 POST 的返回凑。
|
||||
超时/失败一律 resolve(null)(fail-soft):退回用 POST 带回的值判分支,
|
||||
后续任意一次 GET 仍会把兜底数据补上,不会丢状态。 */
|
||||
fetchLibraryProfile() {
|
||||
return new Promise((resolve) => {
|
||||
const openid = this.openid || getOpenid();
|
||||
if (!openid) return resolve(null);
|
||||
const unionid = getUnionid();
|
||||
let url = AUTH_API + '?openid=' + encodeURIComponent(openid);
|
||||
if (unionid) url += '&unionid=' + encodeURIComponent(unionid);
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
timeout: 2500,
|
||||
success: (r) => {
|
||||
const d = r.data || {};
|
||||
resolve(d.code === 200 ? (d.result || {}) : null);
|
||||
},
|
||||
fail: () => resolve(null)
|
||||
});
|
||||
});
|
||||
},
|
||||
cancel() { uni.navigateBack(); }
|
||||
|
||||
/* ---------- 共享授权接口 POST(code / nickname / avatar 三选若干) ---------- */
|
||||
postAuth(extra) {
|
||||
return new Promise((resolve) => {
|
||||
const openid = this.openid || getOpenid();
|
||||
if (!openid) return resolve({ ok: false, res: {}, message: '未获得 openid,请先登录' });
|
||||
/* unionid:用户中心身份键(预留)。没拿到也不影响授权(后端可映射兜底)。 */
|
||||
const unionid = getUnionid();
|
||||
let url = AUTH_API + '?openid=' + encodeURIComponent(openid);
|
||||
if (unionid) url += '&unionid=' + encodeURIComponent(unionid);
|
||||
const ex = extra || {};
|
||||
['code', 'nickname', 'avatar'].forEach((k) => {
|
||||
if (ex[k]) url += '&' + k + '=' + encodeURIComponent(ex[k]);
|
||||
});
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
timeout: 10000,
|
||||
success: (r) => {
|
||||
const d = r.data || {};
|
||||
if (d.code === 200) {
|
||||
resolve({ ok: true, res: d.result || {}, message: d.message || '' });
|
||||
} else {
|
||||
resolve({ ok: false, res: d.result || {}, message: d.message || '授权保存失败,请稍后重试' });
|
||||
}
|
||||
},
|
||||
fail: () => resolve({ ok: false, res: {}, message: '授权请求失败,请检查网络' })
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
showToast(msg, tick, ms) {
|
||||
if (this._toastTimer) clearTimeout(this._toastTimer);
|
||||
this.toast.msg = msg || '';
|
||||
this.toast.tick = !!tick;
|
||||
this.toast.show = true;
|
||||
this._toastTimer = setTimeout(() => { this.toast.show = false; }, ms || 1500);
|
||||
},
|
||||
noop() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 清 button 默认边框/padding(避免撑出卡片 + 消除默认灰底边框)。
|
||||
不改 margin —— 统配 margin:0 会压掉各按钮自己的 margin-top,导致元素紧贴。 */
|
||||
⚠️ 两条纪律,踩过:
|
||||
① 这里不设 background —— 否则会盖掉 .cta/.bmain 自己的底色;
|
||||
② `.auth-page button` 的特异性是 (0,1,1)(scoped 编译后约 (0,2,1)),**高于**单类选择器
|
||||
`.bsec`/`.bmain`/`.cta`/(0,1,0)。所以下面所有按钮自己的 padding / border 规则
|
||||
都必须带上 `.auth-page ` 前缀提权,否则会被这条 reset 清成 0 ——
|
||||
表现是「否」没有边框只剩文字、按钮高度塌成一行字高(2026-09-22 真机截图)。 */
|
||||
.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-page { min-height: 100vh; background: #F5EFE7; color: #4A3B33; font-size: 15px; line-height: 1.6; }
|
||||
|
||||
/* 头部:圆形头像选择区(图片填满圆并裁剪),说明文字放圆外下方 */
|
||||
.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; }
|
||||
/* ---------- 内容区 ---------- */
|
||||
.home { padding: 14px 14px 0; }
|
||||
|
||||
/* 昵称输入 */
|
||||
.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; }
|
||||
/* ---------- 价值主张卡(未授权时的主转化区) ---------- */
|
||||
.pitch { background-image: linear-gradient(160deg, #FFFFFF 0%, #FFFCF8 46%, #FDF6EF 100%); border: 1px solid #EDE2D7; border-radius: 16px; padding: 17px 15px 15px; box-shadow: 0 6px 18px -8px rgba(122,88,66,.18), 0 1px 3px rgba(122,88,66,.06); text-align: center; }
|
||||
.pbadge { display: inline-block; background-image: linear-gradient(90deg, #F7E4EB, #FBF0E7); color: #A9506C; font-size: 10.5px; font-weight: 700; border-radius: 20px; padding: 4px 11px; }
|
||||
.pttl { font-size: 18.5px; line-height: 1.45; margin-top: 11px; font-weight: 700; color: #4A3B33; }
|
||||
.pttl2 { margin-top: 0; }
|
||||
.pttl.em { color: #A9506C; }
|
||||
.psub { font-size: 11.5px; color: #7D6A5E; line-height: 1.8; }
|
||||
.social { display: flex; align-items: center; justify-content: center; margin-top: 12px; }
|
||||
.dots { display: flex; align-items: center; }
|
||||
.dot { width: 19px; height: 19px; border-radius: 50%; border: 1.5px solid #fff; box-shadow: 0 1px 2px rgba(60,42,36,.12); }
|
||||
.dot + .dot { margin-left: -7px; }
|
||||
.social-txt { font-size: 11px; color: #A9506C; font-weight: 600; margin-left: 7px; }
|
||||
.auth-page .cta { display: block; width: 100%; margin-top: 14px; border-radius: 13px; padding: 14px 0; font-size: 15.5px; font-weight: 700; color: #fff; background-image: linear-gradient(100deg, #B2546D 0%, #CE7089 100%); animation: ctapulse 2.6s ease-in-out infinite; }
|
||||
.auth-page .cta:active { opacity: .88; }
|
||||
@keyframes ctapulse {
|
||||
0%, 100% { box-shadow: 0 8px 18px -9px rgba(169,80,108,.55); }
|
||||
50% { box-shadow: 0 10px 26px -7px rgba(169,80,108,.85); }
|
||||
}
|
||||
.ctahint { font-size: 10.5px; color: #97867C; margin-top: 9px; }
|
||||
.trust { display: flex; justify-content: center; margin-top: 13px; padding-top: 12px; border-top: 1px dashed #EDE2D7; }
|
||||
.trust-i { font-size: 10.5px; color: #7D6A5E; margin: 0 5px; }
|
||||
.trust-tick { color: #12A67F; font-size: 11px; margin-right: 3px; }
|
||||
|
||||
/* 获取手机号按钮 */
|
||||
.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; }
|
||||
/* ---------- 已授权:收起成一条状态 ---------- */
|
||||
.hero { background-image: linear-gradient(160deg, #FFFFFF 0%, #FFFCF8 46%, #FDF6EF 100%); border: 1px solid #EDE2D7; border-radius: 15px; padding: 14px; box-shadow: 0 6px 18px -8px rgba(122,88,66,.18), 0 1px 3px rgba(122,88,66,.06); display: flex; align-items: center; }
|
||||
.hero .ava { width: 44px; height: 44px; border-radius: 50%; background: #F7EFE6; flex: 0 0 44px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
.ava-img { width: 100%; height: 100%; border-radius: 50%; }
|
||||
.ava-emoji { font-size: 19px; }
|
||||
.hero .who { flex: 1; min-width: 0; margin-left: 12px; }
|
||||
.hero .nm { font-size: 14.5px; font-weight: 600; }
|
||||
.hero .st { font-size: 11.5px; color: #7D6A5E; margin-top: 2px; }
|
||||
.hero .ok { font-size: 11.5px; color: #12A67F; font-weight: 700; background: #E4F5EF; border-radius: 20px; padding: 5px 11px; }
|
||||
|
||||
.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; }
|
||||
/* ---------- 工具区 ---------- */
|
||||
.secthead { display: flex; align-items: baseline; justify-content: space-between; margin: 16px 2px 9px; }
|
||||
.secttl { font-size: 12px; color: #7D6A5E; font-weight: 600; }
|
||||
.sectmore { font-size: 11px; color: #97867C; }
|
||||
.freebar { display: flex; align-items: center; margin: 0 2px 11px; padding: 9px 11px; background-image: linear-gradient(120deg, #FDF1F4 0%, #FCF6F1 100%); border: 1px solid #F1DDE3; border-radius: 11px; }
|
||||
.freepill { flex: 0 0 auto; background: #A9506C; color: #fff; font-size: 10px; font-weight: 700; border-radius: 20px; padding: 3px 8px; }
|
||||
.freetxt { font-size: 11px; color: #8E4960; line-height: 1.5; margin-left: 8px; }
|
||||
|
||||
/* 底部留白要够,否则最后一行被常驻行动条压住 */
|
||||
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; padding-bottom: 96px; }
|
||||
.tile { background: #fff; border: 1px solid #EDE2D7; border-radius: 13px; padding: 11px 4px 9px; text-align: center; box-shadow: 0 6px 18px -8px rgba(122,88,66,.18), 0 1px 3px rgba(122,88,66,.06); position: relative; }
|
||||
.tile .ic { height: 34px; display: flex; align-items: center; justify-content: center; }
|
||||
.tile-logo { width: 34px; height: 34px; }
|
||||
.tile-fb { font-size: 17px; color: #C4A18C; font-weight: 700; }
|
||||
/* 工具名最长的要能折两行且不挤掉图标 */
|
||||
.tile .tn { font-size: 9.5px; color: #5C4A41; margin-top: 6px; line-height: 1.3; min-height: 25px; }
|
||||
/* 未解锁标记:制造"授权即可打开"的预期(仅展示,不响应点击) */
|
||||
.lock { position: absolute; top: 6px; right: 6px; display: none; line-height: 0; }
|
||||
.grid.locked .lock { display: block; }
|
||||
.lock-ic { width: 12px; height: 12px; opacity: .5; }
|
||||
|
||||
/* ---------- 常驻底部行动条:上缘渐变淡出,避免硬生生切断内容 ---------- */
|
||||
.stickycta { position: fixed; left: 0; right: 0; bottom: 0; padding: 14px 14px calc(14px + env(safe-area-inset-bottom)); z-index: 10; background-image: linear-gradient(180deg, rgba(245,239,231,0) 0%, rgba(245,239,231,.97) 38%); }
|
||||
.auth-page .cta2 { margin-top: 0; font-size: 14.5px; padding: 13px 0; }
|
||||
|
||||
/* ---------- 遮罩 + 底部半弹窗 ---------- */
|
||||
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(66,50,42,.42); opacity: 0; pointer-events: none; transition: opacity .26s; z-index: 20; }
|
||||
.mask.show { opacity: 1; pointer-events: auto; }
|
||||
.sheet { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; border-radius: 20px 20px 0 0; padding: 20px 20px calc(24px + env(safe-area-inset-bottom)); z-index: 30; transform: translateY(102%); transition: transform .3s cubic-bezier(.22,.9,.28,1); box-shadow: 0 -8px 30px -10px rgba(96,70,54,.28); }
|
||||
.sheet.show { transform: translateY(0); }
|
||||
.sheet .grab { width: 34px; height: 4px; border-radius: 4px; background: #EBDCCF; margin: 0 auto 15px; }
|
||||
.sheet-h3 { font-size: 16.5px; font-weight: 600; color: #4A3B33; margin-bottom: 7px; }
|
||||
.sheet-p { font-size: 12.5px; color: #7D6A5E; line-height: 1.75; }
|
||||
.btnrow { display: flex; gap: 10px; margin-top: 18px; }
|
||||
.auth-page .bsec { flex: 1; border: 1px solid #EBDCCF; background: #fff; color: #A9506C; border-radius: 12px; padding: 12px 0; font-size: 14.5px; }
|
||||
.auth-page .bsec:active { background: #FDF8F3; }
|
||||
.auth-page .bmain { flex: 1.55; border-radius: 12px; padding: 12px 0; font-size: 14.5px; font-weight: 600; color: #fff; background: #A9506C; position: relative; }
|
||||
.auth-page .bmain:active { opacity: .88; }
|
||||
.auth-page .bmain.bwx { background: #07C160; }
|
||||
|
||||
/* ---------- ④-A 复用:库里的头像 + 昵称 ---------- */
|
||||
.avarow { display: flex; align-items: center; margin-top: 18px; background: #FDF8F3; border: 1px solid #EBDCCF; border-radius: 12px; padding: 12px; }
|
||||
.avarow-big { width: 52px; height: 52px; border-radius: 50%; flex: 0 0 52px; overflow: hidden; background: #F7EFE6; display: flex; align-items: center; justify-content: center; }
|
||||
.avarow-img { width: 100%; height: 100%; border-radius: 50%; }
|
||||
.avarow-emoji { font-size: 23px; }
|
||||
.avarow-txt { min-width: 0; margin-left: 13px; }
|
||||
.avarow-txt .t1 { font-size: 14px; font-weight: 600; color: #4A3B33; }
|
||||
|
||||
/* ---------- 页内 toast ---------- */
|
||||
.toast { position: fixed; left: 50%; top: 44%; transform: translate(-50%,-50%) scale(.9); background: rgba(68,52,44,.92); color: #fff; border-radius: 12px; padding: 16px 22px; font-size: 14px; z-index: 60; opacity: 0; pointer-events: none; transition: .24s; text-align: center; min-width: 130px; }
|
||||
.toast.show { opacity: 1; transform: translate(-50%,-50%) scale(1); }
|
||||
.toast-tick { display: block; font-size: 26px; margin-bottom: 5px; color: #6EE7B7; }
|
||||
.toast-msg { color: #fff; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user