This commit is contained in:
2026-08-05 14:23:29 +08:00
parent 74c24d084e
commit f1487e60bb

View File

@@ -1,6 +1,5 @@
<template> <template>
<view data-component-name="diy-recommend-code"> <view data-component-name="diy-recommend-code">
<!-- ============ 未登录:占位引导卡片(保持原有风格与逻辑一致) ============ --> <!-- ============ 未登录:占位引导卡片(保持原有风格与逻辑一致) ============ -->
<view class="invite-login-tip" v-if="!storeToken" @tap="goLogin" <view class="invite-login-tip" v-if="!storeToken" @tap="goLogin"
:style="loginWrapStyle"> :style="loginWrapStyle">
@@ -12,10 +11,8 @@
{{ value.loginBtnText || '去登录' }} {{ value.loginBtnText || '去登录' }}
</text> </text>
</view> </view>
<!-- ============ 已登录:推荐码卡片(后端驱动,3 套预设 + consent 遮罩) ============ --> <!-- ============ 已登录:推荐码卡片(后端驱动,3 套预设 + consent 遮罩) ============ -->
<view v-else> <view v-else>
<!-- ============ Consent 同意遮罩(requireConsent 开启 + 后端返回 consent_required=true 才显示) ============ --> <!-- ============ Consent 同意遮罩(requireConsent 开启 + 后端返回 consent_required=true 才显示) ============ -->
<view v-if="showConsent" class="consent-overlay" :style="cardBaseStyle"> <view v-if="showConsent" class="consent-overlay" :style="cardBaseStyle">
<view class="consent-header"> <view class="consent-header">
@@ -289,17 +286,16 @@ export default {
shareLink: '', shareLink: '',
qrcodeUrl: '', qrcodeUrl: '',
/* 状态机 */ /* 状态机 */
showConsent: false, // 是否弹同意遮罩 showConsent: false,
loadingCode: false, // 接口 loading 中 loadingCode: false,
codeLoaded: false, // 是否至少成功调过一次 getMyCode(防止登录后重复接口) codeLoaded: false,
/* 内部缓存(防止 watch / 登录成功重复刷接口时丢失) */
__lastConsentRequired: false, __lastConsentRequired: false,
/* 渲染调试用:组件实例唯一ID,多个组件同屏时能区分日志 */
__cid: 'rc_' + Date.now() + '_' + Math.floor(Math.random() * 10000) __cid: 'rc_' + Date.now() + '_' + Math.floor(Math.random() * 10000)
}; };
}, },
/* ✅ 关键修复:props.value 为空对象 {} 时,Vue 的 props default 不会生效(只有 value === undefined 才用 default)
这里做一次深 merge:把 props.value 里非空字段覆盖到默认 defaultValue 上,保证商家后台没配置的字段也能拿到默认值,否则所有 style 都是 undefined 导致模板样式全空,组件透明不可见 */
computed: { computed: {
nc() { nc() {
const raw = (this.value && typeof this.value === 'object') ? this.value : {}; const raw = (this.value && typeof this.value === 'object') ? this.value : {};
@@ -385,7 +381,7 @@ export default {
bookGoldStyle() { bookGoldStyle() {
const v = this.nc; const v = this.nc;
return this.cardBaseStyle + [ return this.cardBaseStyle + [
`background:#FFFFFF;`, // 原来渐变改成纯白 `background:#FFFFFF;`,
'box-shadow:0 12rpx 36rpx rgba(166,139,106,0.15);', 'box-shadow:0 12rpx 36rpx rgba(166,139,106,0.15);',
'border-radius:24rpx;' 'border-radius:24rpx;'
].join(''); ].join('');
@@ -401,7 +397,7 @@ export default {
bookGoldCopyBtnStyle() { bookGoldCopyBtnStyle() {
const v = this.nc; const v = this.nc;
return [ return [
`background:#B8A08A;`, // 灰褐金按钮 `background:#B8A08A;`,
`color:#ffffff;`, `color:#ffffff;`,
`border-radius:40rpx;`, `border-radius:40rpx;`,
`font-size:24rpx;padding:10rpx 24rpx;` `font-size:24rpx;padding:10rpx 24rpx;`
@@ -420,7 +416,6 @@ export default {
`border:2rpx dashed ${v.codeColor || '#6B4423'};`, `border:2rpx dashed ${v.codeColor || '#6B4423'};`,
`border-radius:${this.px2rpx(Math.max(4, (Number(v.borderRadius) || 8) - 4))}rpx;`, `border-radius:${this.px2rpx(Math.max(4, (Number(v.borderRadius) || 8) - 4))}rpx;`,
'margin-top:28rpx;', 'margin-top:28rpx;',
// 新增布局行内样式,强制横向
'display:flex;flex-direction:row;align-items:center;flex-wrap:nowrap;' 'display:flex;flex-direction:row;align-items:center;flex-wrap:nowrap;'
].join(''); ].join('');
}, },
@@ -495,8 +490,6 @@ export default {
}, },
}, },
watch: { watch: {
/* 登录态切换:guest → login → 自动调 getMyCode;logout → 清空状态
全部 try-catch 包裹:任何 watcher 错误不会冒泡到父级,避免整个 diy-group 崩掉 */
'storeToken'(nv) { 'storeToken'(nv) {
try { try {
if (nv) { if (nv) {
@@ -522,12 +515,6 @@ export default {
} }
} }
}, },
/* 页面/组件 created 生命周期:首次登录好的直接拉码 */
/**
* 关键修复:捕获子组件/本组件任何渲染错误,避免因为 ns-login / $refs / Vuex commit 等小问题
* 导致整个 diy-group 渲染中断、页面底部导航点了也不切换(BUG1 的连锁问题)
* Vue 的 errorCaptured 返回 false 时阻止错误继续向上冒泡
*/
errorCaptured(err, vm, info) { errorCaptured(err, vm, info) {
try { console.error('[RecommendDiy][' + this.__cid + '][errorCaptured] 组件内部错误(已拦截):', err && err.message ? err.message : err, 'info=', info); } catch (e) {} try { console.error('[RecommendDiy][' + this.__cid + '][errorCaptured] 组件内部错误(已拦截):', err && err.message ? err.message : err, 'info=', info); } catch (e) {}
return false; return false;
@@ -589,17 +576,7 @@ export default {
}; };
}, },
methods: { methods: {
/* ======================== 工具:万能响应解包(兼容后端各种嵌套结构) ======================== */
/**
* 万能解包:后端返回结构可能是:
* 标准 {code:0, msg:'ok', data:{code:'...', share_link:'...'}}
* 嵌套 {data:{code:0, data:{...}}}
* 双data {code:0, data:{data:{...}}}
* TP风格 {status:200, info:'ok', data:{...}}
* 裸返回 {code:'...', share_link:'...'} (没包 code/msg 外层)
* 统一返回 { ok: bool, businessCode: number, msg: string, data: object, raw: any }
*/
mainBtnBgStyle(isCopyMini) { mainBtnBgStyle(isCopyMini) {
const v = this.nc; const v = this.nc;
const sp = this.stylePreset; const sp = this.stylePreset;
@@ -651,14 +628,13 @@ export default {
var codeVal = Number(obj[codeKey]) || 0; var codeVal = Number(obj[codeKey]) || 0;
out.businessCode = codeVal; out.businessCode = codeVal;
if (messageKey) out.msg = String(obj[messageKey]); if (messageKey) out.msg = String(obj[messageKey]);
// 业务码为 0 / 200 / '0' / '200' → 成功
if (codeVal === 0 || codeVal === 200) { if (codeVal === 0 || codeVal === 200) {
out.ok = true; out.ok = true;
// data 优先级:obj.data > obj.result > 整个 obj(防止把 {code,msg,data} 里的 data 当外层当 data 用) // data 优先级:obj.data > obj.result > 整个 obj(防止把 {code,msg,data} 里的 data 当外层当 data 用)
var d = null; var d = null;
if (obj.data && typeof obj.data === 'object') d = obj.data; if (obj.data && typeof obj.data === 'object') d = obj.data;
else if (obj.result && typeof obj.result === 'object') d = obj.result; else if (obj.result && typeof obj.result === 'object') d = obj.result;
else d = obj; // 兜底:裸对象,code/share_link 在最外层 else d = obj;
out.data = d && typeof d === 'object' ? d : {}; out.data = d && typeof d === 'object' ? d : {};
// 特殊处理:如果 data 里又有 data(后端双 data 包裹),再解一层 // 特殊处理:如果 data 里又有 data(后端双 data 包裹),再解一层
if (out.data.data && typeof out.data.data === 'object' && !out.data.code && !out.data.share_link) { if (out.data.data && typeof out.data.data === 'object' && !out.data.code && !out.data.share_link) {
@@ -668,7 +644,6 @@ export default {
return out; return out;
} }
} else if (typeof obj.code === 'string' || typeof obj.share_link === 'string') { } else if (typeof obj.code === 'string' || typeof obj.share_link === 'string') {
// 裸对象:外层没有 code/status 业务码,但内层直接有 code/share_link → 当成成功处理
out.ok = true; out.ok = true;
out.businessCode = 0; out.businessCode = 0;
out.msg = 'ok'; out.msg = 'ok';
@@ -677,7 +652,7 @@ export default {
return out; return out;
} }
} }
// 没解出来:打印完整 res 给用户看
try { console.warn('[RecommendDiy][' + cid + '][解包] ⚠️ 未能识别后端响应结构,raw=', JSON.stringify(res).slice(0, 500)); } catch(e){} try { console.warn('[RecommendDiy][' + cid + '][解包] ⚠️ 未能识别后端响应结构,raw=', JSON.stringify(res).slice(0, 500)); } catch(e){}
} catch (e) { } catch (e) {
try { console.error('[RecommendDiy][' + cid + '][解包] 异常:', e && e.message ? e.message : e); } catch(_){} try { console.error('[RecommendDiy][' + cid + '][解包] 异常:', e && e.message ? e.message : e); } catch(_){}
@@ -735,7 +710,7 @@ export default {
// 本次尝试业务失败(可能是 method 不对,或者 token 不对,或者 code!=0) // 本次尝试业务失败(可能是 method 不对,或者 token 不对,或者 code!=0)
lastMsg = unpacked.msg ? String(unpacked.msg) : ('推荐码获取失败(业务码=' + unpacked.businessCode + ')'); lastMsg = unpacked.msg ? String(unpacked.msg) : ('推荐码获取失败(业务码=' + unpacked.businessCode + ')');
try { console.warn('[RecommendDiy][' + cid + '][loadCode][' + meta.label + '] 业务失败:businessCode=' + unpacked.businessCode + ',msg=' + unpacked.msg + ',准备尝试下一个 method'); } catch(e){} try { console.warn('[RecommendDiy][' + cid + '][loadCode][' + meta.label + '] 业务失败:businessCode=' + unpacked.businessCode + ',msg=' + unpacked.msg + ',准备尝试下一个 method'); } catch(e){}
// 第一个 method 失败 → 尝试下一个(GET→POST);最后一个失败就停
if (attemptIdx < attempts.length) { if (attemptIdx < attempts.length) {
doOneAttempt(); doOneAttempt();
} else { } else {
@@ -796,15 +771,12 @@ export default {
} }
doOneAttempt(); doOneAttempt();
}, },
/** 用户点击同意按钮 → consent=1 再调一次 getMyCode 拿真码 */ /** 用户点击同意按钮 → consent=1 再调一次 getMyCode 拿真码 */
confirmConsent() { confirmConsent() {
this.loadCode(true); this.loadCode(true);
}, },
/** /**
* 接口 2:POST /recommend/api/recommend/regenerateCode * 接口 2:POST /recommend/api/recommend/regenerateCode
* 换新码:旧码立即 status=0 失效,后端返回新 code + share_link + old_code + regenerated_at
*/ */
regenerate() { regenerate() {
const vm = this; const vm = this;
@@ -858,7 +830,6 @@ export default {
} }
}); });
}, },
/** 【接口 5:占位】调 getMyQrcode(可选兜底,失败不影响 showQR,本组件会 fallback 到 qrserver) */ /** 【接口 5:占位】调 getMyQrcode(可选兜底,失败不影响 showQR,本组件会 fallback 到 qrserver) */
loadQrcodeIfNeeded() { loadQrcodeIfNeeded() {
const vm = this; const vm = this;
@@ -946,21 +917,11 @@ onShareClick() {
}); });
}); });
}, },
/**
* 从后端返回的 share_link 中提取小程序相对 path(给 onShareAppMessage 用)
* - 相对路径:直接返回
* - 绝对 URL / H5 URL:提取 recommend_code 参数,拼成小程序标准落地页(/pages_tool/form/formdata?recommend_code=xxx)
*/
extractMiniProgramPath() { extractMiniProgramPath() {
// 直接生成落地页路径,不需要解析后端废弃shareLink // 直接生成落地页路径,不需要解析后端废弃shareLink
if (!this.code) return '/pages_tool/form/formdata'; if (!this.code) return '/pages_tool/form/formdata';
return `/pages_tool/form/formdata?recommend_code=${encodeURIComponent(this.code)}`; return `/pages_tool/form/formdata?recommend_code=${encodeURIComponent(this.code)}`;
}, },
_persistRecommendInfo() { _persistRecommendInfo() {
try { try {
const sInfo = (this.$store && this.$store.state && this.$store.state.memberInfo) || {}; const sInfo = (this.$store && this.$store.state && this.$store.state.memberInfo) || {};
@@ -1231,12 +1192,11 @@ onShareClick() {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
.rc-code-minimal { .rc-code-minimal {
/* 强制覆盖父级.rc-code-box纵向布局 */
display: flex !important; display: flex !important;
flex-direction: row !important; flex-direction: row !important;
align-items: center !important; align-items: center !important;
justify-content: space-between !important; justify-content: space-between !important;
flex-wrap: nowrap !important; /* 禁止换行 */ flex-wrap: nowrap !important;
background: transparent; background: transparent;
border-style: dashed; border-style: dashed;
padding: 22rpx 26rpx; padding: 22rpx 26rpx;
@@ -1246,8 +1206,8 @@ onShareClick() {
text-align: left; text-align: left;
letter-spacing: 4rpx; letter-spacing: 4rpx;
line-height: 1; line-height: 1;
flex: 1; /* 文字自适应填充剩余宽度 */ flex: 1;
white-space: nowrap; /* 文字不换行挤压按钮 */ white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@@ -1257,7 +1217,7 @@ onShareClick() {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0 !important; /* 按钮禁止被压缩 */ flex-shrink: 0 !important;
white-space: nowrap; white-space: nowrap;
} }
} }
@@ -1296,7 +1256,7 @@ onShareClick() {
width: 100%; width: 100%;
.rc-header-bookgold { .rc-header-bookgold {
.rc-title { .rc-title {
color: #333333; // 白色背景改用深色标题 color: #333333;
} }
} }
.rc-code-bookgold { .rc-code-bookgold {