1174 lines
48 KiB
Vue
1174 lines
48 KiB
Vue
<template>
|
||
<view data-component-name="diy-recommend-code">
|
||
|
||
<!-- ============ 未登录:占位引导卡片(保持原有风格与逻辑一致) ============ -->
|
||
<view class="invite-login-tip" v-if="!storeToken" @tap="goLogin"
|
||
:style="loginWrapStyle">
|
||
<text class="tip-icon">{{ value.loginIcon || '🔑' }}</text>
|
||
<text class="tip-text" :style="{ color: value.loginTipColor || '#666666' }">
|
||
{{ value.loginTipText || '登录后查看我的推荐码' }}
|
||
</text>
|
||
<text class="tip-btn" :style="loginBtnStyle">
|
||
{{ value.loginBtnText || '去登录' }}
|
||
</text>
|
||
</view>
|
||
|
||
<!-- ============ 已登录:推荐码卡片(后端驱动,3 套预设 + consent 遮罩) ============ -->
|
||
<view v-else>
|
||
|
||
<!-- ============ Consent 同意遮罩(requireConsent 开启 + 后端返回 consent_required=true 才显示) ============ -->
|
||
<view v-if="showConsent" class="consent-overlay" :style="cardBaseStyle">
|
||
<view class="consent-header">
|
||
<text class="consent-title" :style="{ color: value.titleColor || value.textColor || '#333333' }">
|
||
{{ value.title || '我的推荐码' }}
|
||
</text>
|
||
</view>
|
||
<view class="consent-content">
|
||
<text class="consent-text" :style="{ color: value.textColor || '#666666' }">
|
||
{{ value.consentText || '您的推荐码将用于企业入驻来源追踪,我们会记录每一次通过您分享链接完成的入驻申请,用于您的推广收益结算。开通后可随时在页面停止使用。' }}
|
||
</text>
|
||
</view>
|
||
<view class="consent-action">
|
||
<view class="consent-btn" :style="consentBtnStyle" @tap="confirmConsent" :class="{ 'btn-loading': loadingCode }">
|
||
<text>{{ loadingCode ? '提交中...' : (value.consentButtonText || '同意并获取推荐码') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ============ 加载中占位(首次加载 / consent 提交中 / regenerate 中) ============ -->
|
||
<view v-else-if="loadingCode && !code" class="loading-wrap" :style="cardBaseStyle">
|
||
<text class="loading-text" :style="{ color: value.titleColor || '#999999' }">推荐码加载中...</text>
|
||
</view>
|
||
|
||
<!-- ============ 预设 1:Gradient 渐变风格(默认) ============ -->
|
||
<view v-else-if="stylePreset === 'gradient'" class="style-gradient" :style="gradientStyle">
|
||
<view class="rc-header">
|
||
<text class="rc-title" :style="{ color: value.titleColor || '#ffffff' }">{{ value.title || '我的推荐码' }}</text>
|
||
</view>
|
||
<view class="rc-code-box" @tap="copyRecommendCode">
|
||
<text class="rc-prefix" :style="{ color: 'rgba(255,255,255,0.7)' }">推荐码</text>
|
||
<text class="rc-code" :style="codeTextStyle">{{ code || '暂无' }}</text>
|
||
<text class="rc-copy" :style="mainBtnBgStyle(true)">复制</text>
|
||
</view>
|
||
<view class="rc-action-bar">
|
||
<view v-if="isFeatureOn(value.showButton)" class="rc-btn rc-btn-share"
|
||
:style="mainBtnBgStyle()" @tap="onShareClick" open-type="share">
|
||
<text>{{ value.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
<view class="rc-btn rc-btn-copy" :style="secondaryBtnStyle" @tap="copyLink">
|
||
<text>{{ value.placeholderCopyText || '复制链接' }}</text>
|
||
</view>
|
||
<view v-if="isFeatureOn(value.allowRegenerate)" class="rc-btn rc-btn-regenerate"
|
||
:style="secondaryBtnStyle()" @tap="regenerate">
|
||
<text>{{ value.placeholderRegenerateText || '换一个新码' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- showLink:是否显示分享链接文字 -->
|
||
<view v-if="isFeatureOn(value.showLink)" class="rc-link-row">
|
||
<text class="rc-link-text" @tap="copyLink">{{ shareLink }}</text>
|
||
</view>
|
||
<!-- showQR:是否显示二维码 -->
|
||
<view v-if="isFeatureOn(value.showQR)" class="rc-qr-row">
|
||
<image :src="qrcodeRenderUrl" mode="aspectFit" class="rc-qr-img" />
|
||
<text class="rc-qr-tip" :style="{ color: 'rgba(255,255,255,0.8)' }">
|
||
{{ value.posterQrTip || '扫码进入并填写入驻信息' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ============ 预设 2:Minimal 极简风格(纯色 + 边框) ============ -->
|
||
<view v-else-if="stylePreset === 'minimal'" class="style-minimal" :style="minimalStyle">
|
||
<view class="rc-header">
|
||
<text class="rc-title" :style="{ color: value.titleColor || '#333333' }">{{ value.title || '我的推荐码' }}</text>
|
||
</view>
|
||
<view class="rc-code-box rc-code-minimal" @tap="copyRecommendCode" :style="minimalCodeBoxStyle">
|
||
<text class="rc-code rc-code-minimal-text" :style="codeTextStyle">{{ code || '暂无' }}</text>
|
||
<text class="rc-copy rc-copy-minimal" :style="minimalCopyBtnStyle">{{ value.placeholderCopyText || '复制链接' }}</text>
|
||
</view>
|
||
<view class="rc-action-bar rc-action-minimal">
|
||
<button v-if="isFeatureOn(value.showButton)" class="rc-btn rc-btn-share rc-btn-share-minimal"
|
||
:style="mainBtnBgStyle()" @tap="onShareClick" open-type="share">
|
||
<text>{{ value.buttonText || '立即分享' }}</text>
|
||
</button>
|
||
<view v-if="isFeatureOn(value.allowRegenerate)" class="rc-btn rc-btn-regenerate"
|
||
:style="secondaryBtnStyle()" @tap="regenerate">
|
||
<text>{{ value.placeholderRegenerateText || '换一个新码' }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-if="isFeatureOn(value.showLink)" class="rc-link-row">
|
||
<text class="rc-link-text rc-link-minimal" @tap="copyLink">{{ shareLink }}</text>
|
||
</view>
|
||
<view v-if="isFeatureOn(value.showQR)" class="rc-qr-row rc-qr-minimal">
|
||
<image :src="qrcodeRenderUrl" mode="aspectFit" class="rc-qr-img rc-qr-minimal-img" />
|
||
<text class="rc-qr-tip rc-qr-tip-minimal" :style="{ color: '#999999' }">
|
||
{{ value.posterQrTip || '扫码进入并填写入驻信息' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ============ 预设 3:Card 金色边框卡片风格 ============ -->
|
||
<view v-else class="style-card" :style="cardStyleBox">
|
||
<view class="rc-accent-bar" :style="{ background: value.accentColor || '#FFD700' }"></view>
|
||
<view class="rc-header rc-header-card">
|
||
<text class="rc-title" :style="{ color: value.titleColor || '#333333' }">{{ value.title || '我的推荐码' }}</text>
|
||
</view>
|
||
<view class="rc-code-box rc-code-card" @tap="copyRecommendCode">
|
||
<text class="rc-code" :style="codeTextStyle">{{ code || '暂无' }}</text>
|
||
<view class="rc-copy rc-copy-card" :style="{ background: value.accentColor || '#FFD700', color: '#4b3600' }">
|
||
<text>复制</text>
|
||
</view>
|
||
</view>
|
||
<view class="rc-action-bar rc-action-card">
|
||
<button v-if="isFeatureOn(value.showButton)" class="rc-btn rc-btn-share"
|
||
:style="mainBtnBgStyle()" @tap="onShareClick" open-type="share">
|
||
<text>{{ value.buttonText || '立即分享' }}</text>
|
||
</button>
|
||
<view class="rc-btn rc-btn-copy" :style="secondaryBtnStyle" @tap="copyLink">
|
||
<text>{{ value.placeholderCopyText || '复制链接' }}</text>
|
||
</view>
|
||
<view v-if="isFeatureOn(value.allowRegenerate)" class="rc-btn rc-btn-regenerate"
|
||
:style="secondaryBtnStyle()" @tap="regenerate">
|
||
<text>{{ value.placeholderRegenerateText || '换一个新码' }}</text>
|
||
</view>
|
||
</view>
|
||
<view v-if="isFeatureOn(value.showLink)" class="rc-link-row rc-link-card">
|
||
<text class="rc-link-text" @tap="copyLink">{{ shareLink }}</text>
|
||
</view>
|
||
<view v-if="isFeatureOn(value.showQR)" class="rc-qr-row rc-qr-card">
|
||
<view class="rc-qr-wrap-card" :style="{ borderColor: value.accentColor || '#FFD700' }">
|
||
<image :src="qrcodeRenderUrl" mode="aspectFit" class="rc-qr-img rc-qr-card-img" />
|
||
</view>
|
||
<text class="rc-qr-tip rc-qr-tip-card" :style="{ color: '#666666' }">
|
||
{{ value.posterQrTip || '扫码进入并填写入驻信息' }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 登录组件占位(ns-login 全局 mixin 已注册) -->
|
||
<ns-login ref="nsLoginBox" :popup="true" @getUserInfo="onLoginSuccess"></ns-login>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
/* =========================== 剪贴板双端工具(和之前保持一致,改名为 rc 前缀) =========================== */
|
||
function rcCopyToClipboard(vm, text, cb) {
|
||
if (!text) { cb && cb(false, '内容为空'); return; }
|
||
// #ifdef H5
|
||
try {
|
||
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(
|
||
() => cb && cb(true, null),
|
||
() => rcFallbackCopy(text, cb)
|
||
);
|
||
return;
|
||
}
|
||
} catch (e) {}
|
||
rcFallbackCopy(text, cb);
|
||
return;
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
uni.setClipboardData({
|
||
data: text,
|
||
showToast: false,
|
||
success: () => cb && cb(true, null),
|
||
fail: () => cb && cb(false, '复制失败')
|
||
});
|
||
// #endif
|
||
}
|
||
function rcFallbackCopy(text, cb) {
|
||
try {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.readOnly = true;
|
||
ta.style.position = 'absolute';
|
||
ta.style.left = '-9999px';
|
||
ta.style.top = '-9999px';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
ta.setSelectionRange(0, text.length);
|
||
const ok = document.execCommand && document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
cb && cb(ok ? true : false, ok ? null : '复制失败');
|
||
} catch (e) {
|
||
cb && cb(false, '复制失败');
|
||
}
|
||
}
|
||
|
||
/* =========================== 组件本体 =========================== */
|
||
import DiyMinx from './minx.js';
|
||
import golbalConfig from '@/common/js/golbalConfig.js';
|
||
export default {
|
||
name: 'DiyRecommendCode',
|
||
mixins: [DiyMinx, golbalConfig],
|
||
props: {
|
||
value: {
|
||
type: Object,
|
||
default: () => ({
|
||
/* ============== 内容配置,diy_view.php 默认值对齐) ============== */
|
||
title: '我的推荐码',
|
||
buttonText: '立即分享',
|
||
showButton: true,
|
||
allowRegenerate: true,
|
||
placeholderRegenerateText: '换一个新码',
|
||
placeholderCopyText: '复制链接',
|
||
placeholderShareText: '分享',
|
||
requireConsent: true,
|
||
consentText: '您的推荐码将用于企业入驻来源追踪,我们会记录每一次通过您分享链接完成的入驻申请,用于您的推广收益结算。',
|
||
consentButtonText: '同意并获取推荐码',
|
||
|
||
/* ============== 风格与样式 ============== */
|
||
stylePreset: 'gradient', // gradient / minimal / card
|
||
bgColorStart: '#3B82F6',
|
||
bgColorEnd: '#1E40AF',
|
||
bgColor: '#FFFFFF',
|
||
borderColor: '#E5E7EB',
|
||
accentColor: '#FFD700',
|
||
textColor: '#FFFFFF',
|
||
titleColor: '#333333',
|
||
codeColor: '#E4393C',
|
||
buttonBgColor: '#E4393C',
|
||
buttonTextColor: '#FFFFFF',
|
||
shareButtonBgColor: '#67C23A',
|
||
titleFontSize: 14, // px,前端转 rpx(×2)
|
||
codeFontSize: 28, // px,前端转 rpx(×2)
|
||
borderRadius: 8, // px → rpx(×2)
|
||
padding: 16, // px → rpx(×2)
|
||
showQR: false,
|
||
showLink: false,
|
||
|
||
/* ============== 兼容旧字段(不破坏之前商家已经配置过的卡片) ============== */
|
||
// 外间距
|
||
marginTop: 20, marginBottom: 20,
|
||
// 未登录引导
|
||
loginIcon: '🔑', loginTipText: '登录后查看我的推荐码', loginTipColor: '#666666',
|
||
loginBtnText: '去登录', loginBtnBg: '#E4393C', loginBtnTextColor: '#FFFFFF',
|
||
loginBg: '#FFF5F5', loginRadius: 20, loginPaddingY: 40, loginPaddingX: 30,
|
||
posterQrTip: '扫码进入并填写入驻信息'
|
||
})
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
/* 接口返回的核心数据(完全后端驱动,不自己生成) */
|
||
code: '',
|
||
shareLink: '',
|
||
qrcodeUrl: '',
|
||
/* 状态机 */
|
||
showConsent: false, // 是否弹同意遮罩
|
||
loadingCode: false, // 接口 loading 中
|
||
codeLoaded: false, // 是否至少成功调过一次 getMyCode(防止登录后重复接口)
|
||
/* 内部缓存(防止 watch / 登录成功重复刷接口时丢失) */
|
||
__lastConsentRequired: false,
|
||
/* 渲染调试用:组件实例唯一ID,多个组件同屏时能区分日志 */
|
||
__cid: 'rc_' + Date.now() + '_' + Math.floor(Math.random() * 10000)
|
||
};
|
||
},
|
||
/* ✅ 关键修复:props.value 为空对象 {} 时,Vue 的 props default 不会生效(只有 value === undefined 才用 default)
|
||
这里做一次深 merge:把 props.value 里非空字段覆盖到默认 defaultValue 上,保证商家后台没配置的字段也能拿到默认值,否则所有 style 都是 undefined 导致模板样式全空,组件透明不可见 */
|
||
computed: {
|
||
nc() {
|
||
const raw = (this.value && typeof this.value === 'object') ? this.value : {};
|
||
const def = this.$options.props.value.default.call(this);
|
||
// 深合并:遍历 default 的所有 key,raw 里有就用 raw 的,否则用 default 的
|
||
var merged = {};
|
||
var allKeys = Object.keys(def || {});
|
||
// 同时把 raw 里新增的字段(后端可能新增配置)也加进去
|
||
Object.keys(raw || {}).forEach(function(k){ if (allKeys.indexOf(k) === -1) allKeys.push(k); });
|
||
allKeys.forEach(function(k){
|
||
if (raw && raw[k] !== undefined && raw[k] !== null && raw[k] !== '') {
|
||
merged[k] = raw[k];
|
||
} else {
|
||
merged[k] = def && def[k] !== undefined ? def[k] : raw[k];
|
||
}
|
||
});
|
||
return merged;
|
||
},
|
||
stylePreset() {
|
||
const s = String(this.nc.stylePreset || 'gradient').toLowerCase();
|
||
if (['minimal', 'card'].includes(s)) return s;
|
||
return 'gradient';
|
||
},
|
||
requireConsentComputed() {
|
||
return this.isFeatureOn(this.nc.requireConsent);
|
||
},
|
||
|
||
wrapStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
'width:100%;box-sizing:border-box;',
|
||
v.marginTop != null ? `margin-top:${this.px2rpx(v.marginTop)}rpx;` : '',
|
||
v.marginBottom != null ? `margin-bottom:${this.px2rpx(v.marginBottom)}rpx;` : ''
|
||
].join('');
|
||
},
|
||
cardBaseStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`padding:${this.px2rpx(v.padding)}rpx;`,
|
||
`border-radius:${this.px2rpx(v.borderRadius)}rpx;`,
|
||
'box-sizing:border-box;'
|
||
].join('');
|
||
},
|
||
loginWrapStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
v.loginBg ? `background:${v.loginBg};` : '',
|
||
v.loginRadius != null ? `border-radius:${this.px2rpx(v.loginRadius)}rpx;` : '',
|
||
v.loginPaddingY != null ? `padding-top:${this.px2rpx(v.loginPaddingY)}rpx;padding-bottom:${this.px2rpx(v.loginPaddingY)}rpx;` : '',
|
||
v.loginPaddingX != null ? `padding-left:${this.px2rpx(v.loginPaddingX)}rpx;padding-right:${this.px2rpx(v.loginPaddingX)}rpx;` : '',
|
||
'width:100%;display:flex;align-items:center;justify-content:center;flex-wrap:nowrap;box-sizing:border-box;'
|
||
].join('');
|
||
},
|
||
loginBtnStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
v.loginBtnBg ? `background:${v.loginBtnBg};` : '',
|
||
v.loginBtnTextColor ? `color:${v.loginBtnTextColor};` : '',
|
||
'padding:12rpx 28rpx;border-radius:40rpx;font-size:26rpx;margin-left:20rpx;'
|
||
].join('');
|
||
},
|
||
/* ---------- 三套容器样式 ---------- */
|
||
gradientStyle() {
|
||
const v = this.nc;
|
||
const start = v.bgColorStart || '#3B82F6';
|
||
const end = v.bgColorEnd || '#1E40AF';
|
||
return this.cardBaseStyle + [
|
||
`background:linear-gradient(135deg, ${start} 0%, ${end} 100%);`,
|
||
'box-shadow:0 12rpx 36rpx rgba(0,0,0,0.12);'
|
||
].join('');
|
||
},
|
||
minimalStyle() {
|
||
const v = this.nc;
|
||
return this.cardBaseStyle + [
|
||
`background:${v.bgColor || '#FFFFFF'};`,
|
||
`border:2rpx solid ${v.borderColor || '#E5E7EB'};`
|
||
].join('');
|
||
},
|
||
minimalCodeBoxStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`border:2rpx dashed ${v.codeColor || '#E4393C'};`,
|
||
`border-radius:${this.px2rpx(Math.max(4, (Number(v.borderRadius) || 8) - 4))}rpx;`,
|
||
'margin-top:28rpx;'
|
||
].join('');
|
||
},
|
||
minimalCopyBtnStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`background:${v.buttonBgColor || '#E4393C'};`,
|
||
`color:${v.buttonTextColor || '#FFFFFF'};`,
|
||
`border-radius:${this.px2rpx(v.borderRadius)}rpx;`,
|
||
`font-size:24rpx;padding:10rpx 24rpx;`
|
||
].join('');
|
||
},
|
||
cardStyleBox() {
|
||
const v = this.nc;
|
||
return this.cardBaseStyle + [
|
||
`background:${v.bgColor || '#FFFFFF'};`,
|
||
`border:2rpx solid ${v.accentColor || '#FFD700'};`,
|
||
'box-shadow:0 10rpx 30rpx rgba(0,0,0,0.08);',
|
||
'position:relative;overflow:hidden;'
|
||
].join('');
|
||
},
|
||
/* ---------- 推荐码文字样式 ---------- */
|
||
codeTextStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`color:${v.codeColor || '#E4393C'};`,
|
||
`font-size:${this.px2rpx(v.codeFontSize)}rpx;`,
|
||
`letter-spacing:${this.px2rpx(1)}rpx;`,
|
||
'font-weight:800;',
|
||
"font-family:'Courier New', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;"
|
||
].join('');
|
||
},
|
||
/* ---------- 按钮样式 ---------- */
|
||
|
||
secondaryBtnStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
'background:rgba(0,0,0,0.06);',
|
||
`color:${this.stylePreset === 'gradient' ? '#ffffff' : (v.titleColor || '#333333')};`,
|
||
`border-radius:${this.px2rpx(v.borderRadius)}rpx;`,
|
||
`padding:${this.px2rpx(Math.round((Number(v.padding) || 16) * 0.55))}rpx ${this.px2rpx(Math.round((Number(v.padding) || 16) * 0.9))}rpx;`,
|
||
'font-size:24rpx;font-weight:600;'
|
||
].join('');
|
||
},
|
||
consentBtnStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`background:linear-gradient(135deg, ${v.bgColorStart || '#3B82F6'} 0%, ${v.bgColorEnd || '#1E40AF'} 100%);`,
|
||
`color:${v.buttonTextColor || '#FFFFFF'};`,
|
||
`border-radius:${this.px2rpx(Math.max(12, Number(v.borderRadius) || 8))}rpx;`,
|
||
`padding:${this.px2rpx(Math.round((Number(v.padding) || 16) * 0.9))}rpx ${this.px2rpx(Math.round((Number(v.padding) || 16) * 2))}rpx;`,
|
||
'font-size:28rpx;font-weight:800;'
|
||
].join('');
|
||
},
|
||
/* ---------- 二维码渲染 URL ---------- */
|
||
qrcodeRenderUrl() {
|
||
// 优先用后端占位接口返回的真实 qrcode_url;没返回就 fallback 用公共 api 合成 share_link 的二维码
|
||
if (this.qrcodeUrl && String(this.qrcodeUrl).trim()) return this.qrcodeUrl;
|
||
const url = encodeURIComponent(this.shareLink || '');
|
||
if (!url) return '';
|
||
return 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&margin=0&data=' + url;
|
||
}
|
||
},
|
||
watch: {
|
||
/* 登录态切换:guest → login → 自动调 getMyCode;logout → 清空状态
|
||
全部 try-catch 包裹:任何 watcher 错误不会冒泡到父级,避免整个 diy-group 崩掉 */
|
||
'storeToken'(nv) {
|
||
try {
|
||
if (nv) {
|
||
this._resetAll();
|
||
var vm = this;
|
||
this.$nextTick(function(){ try { vm.loadCode(false); } catch(e){} });
|
||
} else {
|
||
this._resetAll();
|
||
}
|
||
} catch (e) {
|
||
try { console.error('[RecommendDiy][watch.storeToken] 错误(已拦截):', e && e.message ? e.message : e); } catch(_) {}
|
||
}
|
||
},
|
||
'memberInfo.member_id'(nv) {
|
||
try {
|
||
if (nv) {
|
||
this._resetAll();
|
||
var vm2 = this;
|
||
this.$nextTick(function(){ try { vm2.loadCode(false); } catch(e){} });
|
||
}
|
||
} catch (e) {
|
||
try { console.error('[RecommendDiy][watch.memberInfo.member_id] 错误(已拦截):', e && e.message ? e.message : e); } catch(_) {}
|
||
}
|
||
}
|
||
},
|
||
/* 页面/组件 created 生命周期:首次登录好的直接拉码 */
|
||
/**
|
||
* 关键修复:捕获子组件/本组件任何渲染错误,避免因为 ns-login / $refs / Vuex commit 等小问题
|
||
* 导致整个 diy-group 渲染中断、页面底部导航点了也不切换(BUG1 的连锁问题)
|
||
* Vue 的 errorCaptured 返回 false 时阻止错误继续向上冒泡
|
||
*/
|
||
errorCaptured(err, vm, info) {
|
||
try { console.error('[RecommendDiy][' + this.__cid + '][errorCaptured] 组件内部错误(已拦截):', err && err.message ? err.message : err, 'info=', info); } catch (e) {}
|
||
return false;
|
||
},
|
||
created() {
|
||
try {
|
||
if (this.storeToken) {
|
||
this._resetAll();
|
||
var vm = this;
|
||
this.$nextTick(function(){ try { vm.loadCode(false); } catch(e){} });
|
||
}
|
||
} catch (e) {
|
||
try { console.error('[RecommendDiy][' + this.__cid + '][created] 初始化失败(已拦截):', e && e.message ? e.message : e); } catch(_) {}
|
||
}
|
||
},
|
||
mounted() {
|
||
// ✅ 关键调试日志:页面打开后直接去 F12 搜 "[RecommendDiy][mounted 就能看到组件是否真的渲染了、value 配置是什么、走 v-if 哪个分支会命中
|
||
try {
|
||
var cid = this.__cid;
|
||
var rawVal = this.value || {};
|
||
var mergedVal = this.nc || {};
|
||
var info = {
|
||
cid: cid,
|
||
storeToken: !!this.storeToken,
|
||
memberId: (this.memberInfo && this.memberInfo.member_id) ? this.memberInfo.member_id : null,
|
||
rawValueKeys: Object.keys(rawVal),
|
||
mergedTitle: mergedVal.title,
|
||
stylePreset: mergedVal.stylePreset,
|
||
componentName: rawVal.componentName,
|
||
componentId: rawVal.id,
|
||
requireConsentComputed: this.requireConsentComputed
|
||
};
|
||
console.log('[RecommendDiy][' + cid + '][mounted] 组件已渲染 ✓ 配置=', JSON.stringify(info));
|
||
if (!this.storeToken) {
|
||
console.log('[RecommendDiy][' + cid + '][mounted] 当前未登录,渲染「登录引导卡片(未登录占位)');
|
||
} else {
|
||
console.log('[RecommendDiy][' + cid + '][mounted] 已登录,等待 loadCode 接口返回后渲染推荐码卡片');
|
||
}
|
||
} catch (e) {
|
||
try { console.error('[RecommendDiy][mounted] 日志输出失败:', e && e.message ? e.message : e); } catch(_) {}
|
||
}
|
||
},
|
||
/* 小程序分享钩子(uni-app 支持组件内定义,作为页面级钩子的补充) */
|
||
onShareAppMessage() {
|
||
const v = this.nc;
|
||
const title = v.shareTitle || v.title || '邀您入驻低空经济产业平台';
|
||
return {
|
||
title,
|
||
path: this.extractMiniProgramPath(),
|
||
imageUrl: v.shareImageUrl || ''
|
||
};
|
||
},
|
||
onShareTimeline() {
|
||
const v = this.nc;
|
||
return {
|
||
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
|
||
query: this.code ? ('recommend_code=' + encodeURIComponent(this.code)) : '',
|
||
imageUrl: v.shareImageUrl || ''
|
||
};
|
||
},
|
||
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) {
|
||
const v = this.nc;
|
||
const bg = isCopyMini ? 'rgba(255,255,255,0.92)' : (v.buttonBgColor || '#E4393C');
|
||
const color = isCopyMini ? (v.buttonBgColor || '#E4393C') : (v.buttonTextColor || '#FFFFFF');
|
||
return [
|
||
`background:${bg};`,
|
||
`color:${color};`,
|
||
`border-radius:${this.px2rpx(v.borderRadius)}rpx;`,
|
||
`padding:${this.px2rpx(Math.round((Number(v.padding) || 16) * 0.6))}rpx ${this.px2rpx(Math.round((Number(v.padding) || 16) * 1.2))}rpx;`,
|
||
'font-weight:700;font-size:26rpx;'
|
||
].join('');
|
||
},
|
||
px2rpx(val) {
|
||
return Number(val || 0) * 2
|
||
},
|
||
isFeatureOn(v) {
|
||
if (v === false || v === 0 || v === '0' || v === 'false' || v === null || v === undefined) return false;
|
||
return !!v;
|
||
},
|
||
_unwrapRecommendResponse(res) {
|
||
const cid = this.__cid;
|
||
var out = { ok: false, businessCode: -1, msg: '', data: {}, raw: res };
|
||
try {
|
||
if (!res) return out;
|
||
var r = res;
|
||
// 支持一层 data 包裹(如外层 {code:0, data:{...}} → 再检查 data 里是不是还有 code/data 结构)
|
||
var attempts = [
|
||
function(){ return r; }, // 最外层:直接用 r
|
||
function(){ return (r && r.data && typeof r.data === 'object') ? r.data : null; }, // 下钻一层:r.data
|
||
function(){ return (r && r.data && r.data.data && typeof r.data.data === 'object') ? r.data.data : null; }, // 下钻两层
|
||
function(){ return (r && r.result && typeof r.result === 'object') ? r.result : null; } // 兼容 .result
|
||
];
|
||
for (var i = 0; i < attempts.length; i++) {
|
||
var obj = attempts[i]();
|
||
if (!obj || typeof obj !== 'object') continue;
|
||
var codeKey = null;
|
||
if (typeof obj.code === 'number' || typeof obj.code === 'string') codeKey = 'code';
|
||
else if (typeof obj.status === 'number' || typeof obj.status === 'string') codeKey = 'status';
|
||
var messageKey = null;
|
||
if (typeof obj.msg === 'string') messageKey = 'msg';
|
||
else if (typeof obj.message === 'string') messageKey = 'message';
|
||
else if (typeof obj.info === 'string') messageKey = 'info';
|
||
|
||
// 规则:如果该层有 code/status 字段(业务码),就算是业务层
|
||
if (codeKey) {
|
||
var codeVal = Number(obj[codeKey]) || 0;
|
||
out.businessCode = codeVal;
|
||
if (messageKey) out.msg = String(obj[messageKey]);
|
||
// 业务码为 0 / 200 / '0' / '200' → 成功
|
||
if (codeVal === 0 || codeVal === 200) {
|
||
out.ok = true;
|
||
// data 优先级:obj.data > obj.result > 整个 obj(防止把 {code,msg,data} 里的 data 当外层当 data 用)
|
||
var d = null;
|
||
if (obj.data && typeof obj.data === 'object') d = obj.data;
|
||
else if (obj.result && typeof obj.result === 'object') d = obj.result;
|
||
else d = obj; // 兜底:裸对象,code/share_link 在最外层
|
||
out.data = d && typeof d === 'object' ? d : {};
|
||
// 特殊处理:如果 data 里又有 data(后端双 data 包裹),再解一层
|
||
if (out.data.data && typeof out.data.data === 'object' && !out.data.code && !out.data.share_link) {
|
||
out.data = out.data.data;
|
||
}
|
||
try { console.log('[RecommendDiy][' + cid + '][解包] 命中第 ' + (i+1) + ' 层结构,businessCode=' + codeVal + ', data keys=', Object.keys(out.data)); } catch(e){}
|
||
return out;
|
||
}
|
||
} else if (typeof obj.code === 'string' || typeof obj.share_link === 'string') {
|
||
// 裸对象:外层没有 code/status 业务码,但内层直接有 code/share_link → 当成成功处理
|
||
out.ok = true;
|
||
out.businessCode = 0;
|
||
out.msg = 'ok';
|
||
out.data = obj;
|
||
try { console.log('[RecommendDiy][' + cid + '][解包] 裸对象结构,直接返回 data keys=', Object.keys(obj)); } catch(e){}
|
||
return out;
|
||
}
|
||
}
|
||
// 没解出来:打印完整 res 给用户看
|
||
try { console.warn('[RecommendDiy][' + cid + '][解包] ⚠️ 未能识别后端响应结构,raw=', JSON.stringify(res).slice(0, 500)); } catch(e){}
|
||
} catch (e) {
|
||
try { console.error('[RecommendDiy][' + cid + '][解包] 异常:', e && e.message ? e.message : e); } catch(_){}
|
||
}
|
||
return out;
|
||
},
|
||
|
||
/* ======================== 接口调用(核心 3 个) ======================== */
|
||
|
||
/**
|
||
* 调接口 1:/recommend/api/recommend/getMyCode
|
||
* HTTP Method 兼容:先按文档调 GET;GET 没拿到业务 code===0 的数据,自动 fallback 用 POST
|
||
* consent:false=首次加载,true=用户点击了同意按钮
|
||
* 后端返回(经万能解包):data.consent_required=true 弹同意遮罩;否则 data.code + data.share_link + data.qrcode_url
|
||
*/
|
||
loadCode(consent) {
|
||
const vm = this;
|
||
const cid = this.__cid;
|
||
if (!this.storeToken) {
|
||
try { console.log('[RecommendDiy][' + cid + '][loadCode] 跳过:未登录'); } catch(e){}
|
||
return;
|
||
}
|
||
if (this.loadingCode) return;
|
||
this.loadingCode = true;
|
||
|
||
var reqData = consent ? { consent: 1 } : {};
|
||
console.log('[RecommendDiy][' + cid + '][loadCode] 开始请求,consent=' + !!consent + ',params=', JSON.stringify(reqData));
|
||
|
||
var attempts = [
|
||
{ method: 'GET', label: 'GET(文档标准)' },
|
||
{ method: 'POST', label: 'POST(兜底兼容)' }
|
||
];
|
||
var attemptIdx = 0;
|
||
var lastMsg = '推荐码获取失败,请稍后重试';
|
||
|
||
function doOneAttempt() {
|
||
if (attemptIdx >= attempts.length) {
|
||
// 所有尝试都失败了
|
||
vm.loadingCode = false;
|
||
uni.showToast && uni.showToast({ title: lastMsg, icon: 'none', duration: 2800 });
|
||
return;
|
||
}
|
||
var meta = attempts[attemptIdx];
|
||
attemptIdx += 1;
|
||
try { console.log('[RecommendDiy][' + cid + '][loadCode] 尝试 ' + attemptIdx + '/' + attempts.length + ':' + meta.label); } catch(e){}
|
||
vm.$api.sendRequest({
|
||
url: '/recommend/api/recommend/getMyCode',
|
||
method: meta.method,
|
||
data: reqData,
|
||
async: vm.$api && vm.$api.async === false ? false : true,
|
||
success: function(res){
|
||
try { console.log('[RecommendDiy][' + cid + '][loadCode][' + meta.label + '] 原始响应=', JSON.stringify(res).slice(0, 1200)); } catch(e){}
|
||
var unpacked = vm._unwrapRecommendResponse(res);
|
||
if (!unpacked.ok) {
|
||
// 本次尝试业务失败(可能是 method 不对,或者 token 不对,或者 code!=0)
|
||
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){}
|
||
// 第一个 method 失败 → 尝试下一个(GET→POST);最后一个失败就停
|
||
if (attemptIdx < attempts.length) {
|
||
doOneAttempt();
|
||
} else {
|
||
vm.loadingCode = false;
|
||
uni.showToast && uni.showToast({ title: lastMsg, icon: 'none', duration: 2800 });
|
||
}
|
||
return;
|
||
}
|
||
// ✅ 业务成功
|
||
var d = unpacked.data || {};
|
||
// Case 1:需要同意 + 组件配置了开启同意机制 + 不是同意按钮触发的(还没同意)
|
||
if (d.consent_required && !consent && vm.requireConsentComputed) {
|
||
try { console.log('[RecommendDiy][' + cid + '][loadCode] 命中:需要同意授权 → 弹 consent 遮罩'); } catch(e){}
|
||
vm.__lastConsentRequired = true;
|
||
vm.showConsent = true;
|
||
vm.code = '';
|
||
vm.shareLink = '';
|
||
vm.qrcodeUrl = '';
|
||
vm.codeLoaded = true;
|
||
vm.loadingCode = false;
|
||
return;
|
||
}
|
||
// Case 2:正常拿到推荐码
|
||
try { console.log('[RecommendDiy][' + cid + '][loadCode] 成功 ✓ code=', d.code, 'share_link=', d.share_link, 'qrcode_url=', d.qrcode_url ? '有' : '无'); } catch(e){}
|
||
vm.__lastConsentRequired = false;
|
||
vm.showConsent = false;
|
||
vm.code = d.code ? String(d.code) : '';
|
||
vm.shareLink = d.share_link ? String(d.share_link) : '';
|
||
vm.qrcodeUrl = d.qrcode_url ? String(d.qrcode_url) : '';
|
||
// 兼容旧字段名:后端可能返回 recommend_code / recommendShareLink 等
|
||
if (!vm.code && d.recommend_code) vm.code = String(d.recommend_code);
|
||
if (!vm.code && d.invite_code) vm.code = String(d.invite_code);
|
||
if (!vm.shareLink && d.share_url) vm.shareLink = String(d.share_url);
|
||
if (!vm.shareLink && d.link) vm.shareLink = String(d.link);
|
||
// 持久化:Vuex + storage,供 util.js 现场 banner 跳转自动拼 recommend_code 参数用
|
||
vm._persistRecommendInfo();
|
||
vm.codeLoaded = true;
|
||
vm.loadingCode = false;
|
||
if (consent) {
|
||
uni.showToast && uni.showToast({ title: '获取成功', icon: 'success' });
|
||
}
|
||
// 拿到码后如果 showQR=true,顺便调 getMyQrcode(占位接口)
|
||
try { vm.loadQrcodeIfNeeded(); } catch(e){}
|
||
},
|
||
fail: function(err){
|
||
var failMsg = (err && err.errMsg) ? String(err.errMsg) : '';
|
||
try { console.error('[RecommendDiy][' + cid + '][loadCode][' + meta.label + '] 网络失败:', failMsg, 'raw err=', err); } catch(e){}
|
||
lastMsg = failMsg ? ('网络异常:' + failMsg.slice(0, 30)) : '网络异常,请稍后重试';
|
||
// GET 网络失败也尝试 POST(有些服务器接口路由不支持 GET,只支持 POST)
|
||
if (attemptIdx < attempts.length) {
|
||
doOneAttempt();
|
||
} else {
|
||
vm.loadingCode = false;
|
||
uni.showToast && uni.showToast({ title: lastMsg, icon: 'none', duration: 2800 });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
doOneAttempt();
|
||
},
|
||
|
||
/** 用户点击同意按钮 → consent=1 再调一次 getMyCode 拿真码 */
|
||
confirmConsent() {
|
||
this.loadCode(true);
|
||
},
|
||
|
||
/**
|
||
* 接口 2:POST /recommend/api/recommend/regenerateCode
|
||
* 换新码:旧码立即 status=0 失效,后端返回新 code + share_link + old_code + regenerated_at
|
||
*/
|
||
regenerate() {
|
||
const vm = this;
|
||
const cid = this.__cid;
|
||
if (!this.code) return;
|
||
uni.showModal({
|
||
title: '换新码',
|
||
content: '换新码后旧码会立即失效,后续所有使用旧分享链接的入驻申请都不会归属到您,确认继续?',
|
||
confirmText: '确认换新',
|
||
cancelText: '取消',
|
||
success: (r) => {
|
||
if (!r.confirm) return;
|
||
if (vm.loadingCode) return;
|
||
vm.loadingCode = true;
|
||
vm.$api.sendRequest({
|
||
url: '/recommend/api/recommend/regenerateCode',
|
||
method: 'POST',
|
||
data: {},
|
||
success: (res) => {
|
||
try { console.log('[RecommendDiy][' + cid + '][regenerate] 原始响应=', JSON.stringify(res).slice(0, 1200)); } catch(e){}
|
||
var unpacked = vm._unwrapRecommendResponse(res);
|
||
if (!unpacked.ok) {
|
||
vm.loadingCode = false;
|
||
uni.showToast && uni.showToast({
|
||
title: unpacked.msg ? String(unpacked.msg) : '换新失败,请稍后重试',
|
||
icon: 'none',
|
||
duration: 2200
|
||
});
|
||
return;
|
||
}
|
||
const d = unpacked.data || {};
|
||
vm.code = d.code ? String(d.code) : (d.recommend_code ? String(d.recommend_code) : (d.invite_code ? String(d.invite_code) : ''));
|
||
vm.shareLink = d.share_link ? String(d.share_link) : (d.share_url ? String(d.share_url) : (d.link ? String(d.link) : ''));
|
||
vm._persistRecommendInfo();
|
||
const old = d.old_code ? String(d.old_code) : '';
|
||
try { console.log('[RecommendDiy][' + cid + '][regenerate] 成功 ✓ 新码=', vm.code, '旧码=', old); } catch(e){}
|
||
uni.showToast && uni.showToast({
|
||
title: old ? ('已换新码,旧码 ' + old + ' 已停用') : '换新成功',
|
||
icon: 'none',
|
||
duration: 2200
|
||
});
|
||
vm.loadingCode = false;
|
||
},
|
||
fail: (err) => {
|
||
var failMsg = (err && err.errMsg) ? String(err.errMsg) : '';
|
||
try { console.error('[RecommendDiy][' + cid + '][regenerate] 网络失败:', failMsg); } catch(e){}
|
||
vm.loadingCode = false;
|
||
uni.showToast && uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
|
||
}
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
/** 【接口 5:占位】调 getMyQrcode(可选兜底,失败不影响 showQR,本组件会 fallback 到 qrserver) */
|
||
loadQrcodeIfNeeded() {
|
||
const vm = this;
|
||
if (!this.isFeatureOn(this.nc.showQR)) return;
|
||
if (this.qrcodeUrl) return;
|
||
if (!this.code) return;
|
||
try {
|
||
this.$api.sendRequest({
|
||
url: '/recommend/api/recommend/getMyQrcode',
|
||
method: 'GET',
|
||
data: {},
|
||
success: (res) => {
|
||
if (res && res.code === 0 && res.data && res.data.qrcode_url) {
|
||
vm.qrcodeUrl = String(res.data.qrcode_url);
|
||
}
|
||
}
|
||
});
|
||
} catch (e) {}
|
||
},
|
||
|
||
/* ======================== 分享 / 复制 ======================== */
|
||
|
||
/** 点击主按钮「立即分享」:小程序(原生 open-type 已处理);H5/APP 走 navigator.share */
|
||
onShareClick() {
|
||
const vm = this;
|
||
const v = this.nc;
|
||
// #ifdef H5
|
||
try {
|
||
if (navigator && navigator.share && typeof navigator.share === 'function' && this.shareLink) {
|
||
navigator.share({
|
||
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
|
||
text: v.shareDesc || '',
|
||
url: this.shareLink
|
||
}).catch(() => vm.copyLink());
|
||
return;
|
||
}
|
||
} catch (e) {}
|
||
this.copyLink();
|
||
return;
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
if (plus && plus.share && this.shareLink) {
|
||
plus.share.sendWithSystem({
|
||
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
|
||
content: v.shareDesc || '',
|
||
href: this.shareLink
|
||
}, function () {}, function () { vm.copyLink(); });
|
||
return;
|
||
}
|
||
} catch (e) {}
|
||
this.copyLink();
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN
|
||
// 小程序:主按钮 open-type=share 原生处理,这里兜底复制链接
|
||
if (this.shareLink) this.copyLink();
|
||
// #endif
|
||
},
|
||
|
||
/** 复制「推荐码」本身(点击推荐码盒子时) */
|
||
copyRecommendCode() {
|
||
if (!this.code) {
|
||
uni.showToast && uni.showToast({ title: '推荐码还未生成,请稍候', icon: 'none' });
|
||
return;
|
||
}
|
||
rcCopyToClipboard(this, this.code, (ok, err) => {
|
||
uni.showToast && uni.showToast({
|
||
title: ok ? '推荐码已复制' : (err || '复制失败'),
|
||
icon: ok ? 'success' : 'none'
|
||
});
|
||
});
|
||
},
|
||
|
||
/** 复制「分享链接」(后端给的 share_link 原样复制) */
|
||
copyLink() {
|
||
if (!this.shareLink) {
|
||
uni.showToast && uni.showToast({ title: '分享链接还未生成,请稍候', icon: 'none' });
|
||
return;
|
||
}
|
||
rcCopyToClipboard(this, this.shareLink, (ok, err) => {
|
||
uni.showToast && uni.showToast({
|
||
title: ok ? '推广链接已复制' : (err || '复制失败'),
|
||
icon: ok ? 'success' : 'none'
|
||
});
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 从后端返回的 share_link 中提取小程序相对 path(给 onShareAppMessage 用)
|
||
* - 相对路径:直接返回
|
||
* - 绝对 URL / H5 URL:提取 recommend_code 参数,拼成小程序标准落地页(/pages_tool/form/formdata?recommend_code=xxx)
|
||
*/
|
||
extractMiniProgramPath() {
|
||
try {
|
||
const u = this.shareLink || '';
|
||
if (!u) return '/pages_tool/form/formdata';
|
||
if (u.startsWith('/')) return u;
|
||
// 绝对 URL:解析 recommend_code query
|
||
const m = u.match(/[?&](recommend_code|invite)=([^&]+)/i);
|
||
if (m && m[2]) return '/pages_tool/form/formdata?recommend_code=' + decodeURIComponent(m[2]);
|
||
return '/pages_tool/form/formdata';
|
||
} catch (e) {
|
||
return '/pages_tool/form/formdata';
|
||
}
|
||
},
|
||
|
||
/* ======================== 工具 / 持久化 ======================== */
|
||
|
||
/** 把 code + share_link 持久化到 Vuex + storage,供 util.js 现场 banner 跳转自动拼 recommend_code */
|
||
_persistRecommendInfo() {
|
||
try {
|
||
const sInfo = (this.$store && this.$store.state && this.$store.state.memberInfo) || {};
|
||
let stored = {};
|
||
try { stored = uni.getStorageSync('memberInfo') || {}; } catch (e) { stored = {}; }
|
||
const merged = Object.assign({}, typeof sInfo === 'object' ? sInfo : {}, typeof stored === 'object' ? stored : {}, {
|
||
recommend_code: this.code || '',
|
||
recommend_share_link: this.shareLink || ''
|
||
});
|
||
if (this.$store && this.$store.commit) {
|
||
try { this.$store.commit('setMemberInfo', merged); } catch (e) {}
|
||
}
|
||
try { uni.setStorageSync('memberInfo', merged); } catch (e) {}
|
||
} catch (e) {}
|
||
},
|
||
|
||
/** 登出 / 换账号时清空所有缓存状态 */
|
||
_resetAll() {
|
||
this.code = '';
|
||
this.shareLink = '';
|
||
this.qrcodeUrl = '';
|
||
this.showConsent = false;
|
||
this.loadingCode = false;
|
||
this.codeLoaded = false;
|
||
this.__lastConsentRequired = false;
|
||
},
|
||
|
||
/** ns-login 登录成功回调 → 拉一次 getMyCode */
|
||
onLoginSuccess() {
|
||
this._resetAll();
|
||
this.$nextTick(() => this.loadCode(false));
|
||
},
|
||
|
||
/** 点击未登录卡片 → 弹出登录 */
|
||
goLogin() {
|
||
const isPage = getCurrentPages().length > 0;
|
||
if (this.$refs && this.$refs.nsLoginBox) {
|
||
try {
|
||
if (typeof this.$refs.nsLoginBox.showLoginBox === 'function') {
|
||
this.$refs.nsLoginBox.showLoginBox();
|
||
return;
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
uni.navigateTo && uni.navigateTo({
|
||
url: '/pages/login/login',
|
||
fail: () => { uni.redirectTo && uni.redirectTo({ url: '/pages/login/login' }); }
|
||
});
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.diy-recommend-code {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
/* ===== 未登录占位卡片 ===== */
|
||
.invite-login-tip {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-wrap: nowrap;
|
||
box-sizing: border-box;
|
||
user-select: none;
|
||
cursor: pointer;
|
||
-webkit-tap-highlight-color: transparent;
|
||
.tip-icon { font-size: 40rpx; margin-right: 16rpx; }
|
||
.tip-text { font-size: 28rpx; flex: 1; }
|
||
.tip-btn { font-weight: 500; }
|
||
}
|
||
|
||
/* ===== 通用:同意遮罩 / loading ===== */
|
||
.consent-overlay {
|
||
width: 100%;
|
||
background: #FFFFFF;
|
||
box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.08);
|
||
.consent-header {
|
||
.consent-title { font-size: 34rpx; font-weight: 700; display: block; margin-bottom: 8rpx; }
|
||
}
|
||
.consent-content {
|
||
margin-top: 28rpx;
|
||
background: rgba(0,0,0,0.04);
|
||
border-radius: 20rpx;
|
||
padding: 28rpx 28rpx;
|
||
.consent-text { font-size: 26rpx; line-height: 1.75; }
|
||
}
|
||
.consent-action {
|
||
margin-top: 40rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
.consent-btn {
|
||
display: inline-flex; justify-content: center; align-items: center;
|
||
user-select: none; cursor: pointer; transition: opacity 0.2s ease, transform 0.15s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
&:active { opacity: 0.92; transform: scale(0.98); }
|
||
&.btn-loading { opacity: 0.7; pointer-events: none; }
|
||
}
|
||
}
|
||
}
|
||
.loading-wrap {
|
||
width: 100%;
|
||
background: #F9FAFB;
|
||
min-height: 240rpx;
|
||
display: flex; align-items: center; justify-content: center;
|
||
.loading-text { font-size: 28rpx; }
|
||
}
|
||
|
||
/* ===== 通用:推荐码卡片各组件 ===== */
|
||
.rc-header {
|
||
width: 100%;
|
||
.rc-title { font-size: 34rpx; font-weight: 800; display: block; }
|
||
}
|
||
.rc-code-box {
|
||
margin-top: 30rpx;
|
||
background: rgba(255,255,255,0.16);
|
||
border-radius: 20rpx;
|
||
padding: 26rpx 30rpx;
|
||
display: flex; align-items: center; justify-content: space-between; flex-wrap: nowrap;
|
||
cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent;
|
||
transition: opacity 0.15s ease;
|
||
&:active { opacity: 0.88; }
|
||
.rc-prefix { font-size: 24rpx; margin-right: 20rpx; }
|
||
.rc-code { flex: 1; word-break: break-all; }
|
||
.rc-copy {
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
padding: 12rpx 26rpx;
|
||
border-radius: 40rpx;
|
||
display: inline-flex; justify-content: center; align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
.rc-action-bar {
|
||
margin-top: 36rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 16rpx;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
.rc-btn {
|
||
box-sizing: border-box;
|
||
display: inline-flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
text-align: center;
|
||
border: none;
|
||
outline: none;
|
||
line-height: 1.35;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
transition: opacity 0.2s ease, transform 0.15s ease;
|
||
&:active { opacity: 0.9; transform: scale(0.98); }
|
||
&.rc-btn-share {
|
||
min-width: 220rpx;
|
||
flex: 0 0 auto;
|
||
}
|
||
}
|
||
button.rc-btn {
|
||
margin: 0 !important;
|
||
padding-left: 0 !important;
|
||
padding-right: 0 !important;
|
||
background: transparent;
|
||
overflow: hidden;
|
||
&::after { border: none; }
|
||
}
|
||
|
||
/* ===== 通用:分享链接 / 二维码 ===== */
|
||
.rc-link-row {
|
||
margin-top: 28rpx;
|
||
width: 100%;
|
||
.rc-link-text {
|
||
display: block;
|
||
width: 100%;
|
||
font-size: 22rpx;
|
||
line-height: 1.5;
|
||
word-break: break-all;
|
||
cursor: pointer;
|
||
opacity: 0.82;
|
||
padding: 18rpx 22rpx;
|
||
border-radius: 16rpx;
|
||
background: rgba(255,255,255,0.08);
|
||
}
|
||
&.rc-link-minimal .rc-link-text,
|
||
&.rc-link-card .rc-link-text {
|
||
color: #666666;
|
||
background: #F3F4F6;
|
||
opacity: 1;
|
||
}
|
||
}
|
||
.rc-qr-row {
|
||
margin-top: 36rpx;
|
||
display: flex; flex-direction: column; align-items: center; gap: 14rpx;
|
||
.rc-qr-img {
|
||
width: 300rpx; height: 300rpx;
|
||
display: block;
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.1);
|
||
}
|
||
.rc-qr-tip {
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
text-align: center;
|
||
max-width: 420rpx;
|
||
}
|
||
&.rc-qr-minimal-img, &.rc-qr-card-img { border: 2rpx solid #eee; }
|
||
&.rc-qr-minimal {
|
||
align-items: flex-start;
|
||
gap: 12rpx;
|
||
.rc-qr-minimal-img { border: 2rpx solid #F3F4F6; }
|
||
.rc-qr-tip-minimal { text-align: left; max-width: 500rpx; }
|
||
}
|
||
&.rc-qr-card {
|
||
gap: 20rpx;
|
||
.rc-qr-wrap-card {
|
||
border-radius: 20rpx;
|
||
border: 4rpx solid #FFD700;
|
||
padding: 16rpx;
|
||
background: #ffffff;
|
||
.rc-qr-card-img { width: 260rpx; height: 260rpx; border-radius: 12rpx; }
|
||
}
|
||
.rc-qr-tip-card { max-width: 480rpx; }
|
||
}
|
||
}
|
||
|
||
/* ===== 预设 1:gradient ===== */
|
||
.style-gradient {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
.rc-header .rc-title { color: #ffffff; }
|
||
.rc-code-box {
|
||
background: rgba(0,0,0,0.16);
|
||
.rc-prefix { color: rgba(255,255,255,0.7); }
|
||
.rc-code { color: #ffffff; }
|
||
}
|
||
}
|
||
|
||
/* ===== 预设 2:minimal ===== */
|
||
.style-minimal {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
.rc-code-minimal {
|
||
background: transparent;
|
||
border-style: dashed;
|
||
padding: 22rpx 26rpx;
|
||
gap: 16rpx;
|
||
.rc-code-minimal-text { text-align: center; letter-spacing: 4rpx; }
|
||
.rc-copy-minimal { font-size: 22rpx; }
|
||
}
|
||
.rc-action-minimal { justify-content: space-between; }
|
||
}
|
||
|
||
/* ===== 预设 3:card 金色边框 ===== */
|
||
.style-card {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
.rc-accent-bar {
|
||
position: absolute;
|
||
left: 0; top: 0; right: 0;
|
||
height: 8rpx;
|
||
}
|
||
.rc-header-card { margin-top: 8rpx; }
|
||
.rc-code-card {
|
||
background: #FAFAFA;
|
||
padding: 28rpx 32rpx;
|
||
gap: 16rpx;
|
||
border-radius: 20rpx;
|
||
.rc-copy-card {
|
||
padding: 14rpx 30rpx;
|
||
font-weight: 700;
|
||
font-size: 24rpx;
|
||
}
|
||
}
|
||
.rc-action-card { justify-content: flex-start; }
|
||
}
|
||
}
|
||
</style>
|
||
|