问题:
- 后端 diyData 数值语义是 px(后端 wap/design.html 都按 px 输出),
但前端 px2rpx() 之前 *2 转 rpx,导致商家设置 8px 实际渲染 16px
- 鸿蒙元服务对动态 style 中的 rpx 解析有问题
方案:
- px2rpx(val) 改为返回原值(当作 px 用)
- 17 处动态 :style="{...}" 全部改成 :style="$util.objToStyle({...})"
用 objToStyle 工具方法生成标准 CSS 字符串,跨平台兼容
- 34 处 px2rpx() + 'rpx' 拼接改成 + 'px'
- 8 处 padding/border 中的 'rpx ' / 'rpx solid' 也同步改成 'px'
静态 'XXrpx' 字符串(写在模板里的)保留,uniapp 静态模板
支持 rpx,不影响。
后端保持原状不动(数值 px 语义,与 wap/design.html 一致)。
这样前后端语义统一,商家在 diyData 改数值时:
- H5/小程序: 静态 rpx 自适应屏幕
- 动态部分: 按 px 渲染,商家设计器里的 px 值精确生效
- 鸿蒙元服务: 全部用 px,无解析问题
1930 lines
92 KiB
Vue
1930 lines
92 KiB
Vue
<template>
|
||
<view data-component-name="diy-recommend-code" style="width:100%;box-sizing:border-box;overflow:hidden;">
|
||
<!-- ============ 未登录:占位引导卡片(全部内联样式,匹配参考图) ============ -->
|
||
<view v-if="!storeToken" @tap="goLogin"
|
||
style="width:100%;box-sizing:border-box;margin-top:24rpx;display:flex;align-items:center;background:linear-gradient(135deg,#FFF5F5 0%,#FFF0F0 35%,#FFEDED 65%,#FFE8E8 100%);border-radius:32rpx;padding:36rpx 28rpx;position:relative;overflow:hidden;box-shadow:0 4rpx 16rpx rgba(255,107,107,0.1);">
|
||
<!-- 背景装饰blob -->
|
||
<view style="position:absolute;top:-60rpx;left:-50rpx;width:240rpx;height:240rpx;border-radius:50%;background:rgba(255,107,107,0.05);z-index:0;"></view>
|
||
<view style="position:absolute;bottom:-80rpx;right:-50rpx;width:260rpx;height:260rpx;border-radius:50%;background:rgba(255,193,7,0.04);z-index:0;"></view>
|
||
<view style="position:absolute;top:30rpx;right:80rpx;width:100rpx;height:100rpx;border-radius:50%;background:rgba(255,152,0,0.05);z-index:0;"></view>
|
||
<!-- 左侧:3D钥匙+礼物装饰区 -->
|
||
<view style="flex-shrink:0;width:200rpx;height:200rpx;position:relative;z-index:1;display:flex;align-items:center;justify-content:center;">
|
||
<!-- 圆形底座 -->
|
||
<view style="position:absolute;bottom:0;width:170rpx;height:170rpx;border-radius:50%;background:radial-gradient(circle at 40% 35%,rgba(255,255,255,0.95) 0%,rgba(255,240,230,0.6) 50%,rgba(255,220,210,0.15) 100%);box-shadow:0 6rpx 24rpx rgba(255,107,107,0.12);"></view>
|
||
<!-- 钥匙+礼物组合 -->
|
||
<view style="position:relative;z-index:2;display:flex;align-items:center;justify-content:center;">
|
||
<view style="transform:rotate(-25deg);margin-right:-6rpx;">
|
||
<text style="font-size:76rpx;line-height:1;">🔑</text>
|
||
</view>
|
||
<view style="margin-left:-6rpx;">
|
||
<text style="font-size:68rpx;line-height:1;">🎁</text>
|
||
</view>
|
||
</view>
|
||
<!-- 闪光装饰 -->
|
||
<text style="position:absolute;top:6rpx;right:14rpx;font-size:26rpx;z-index:3;">✨</text>
|
||
<text style="position:absolute;top:54rpx;left:6rpx;font-size:18rpx;z-index:3;opacity:0.7;">✨</text>
|
||
<text style="position:absolute;bottom:24rpx;right:20rpx;font-size:20rpx;z-index:3;opacity:0.5;">⭐</text>
|
||
</view>
|
||
<!-- 右侧:标题+副标题+特性标签+按钮 -->
|
||
<view style="flex:1;margin-left:20rpx;display:flex;flex-direction:column;position:relative;z-index:1;">
|
||
<!-- 主标题 -->
|
||
<text style="font-size:30rpx;font-weight:700;color:#1A1A1A;line-height:1.4;">{{ value.loginTipText || '登录后查看我的推荐码' }}</text>
|
||
<!-- 特性标签行 -->
|
||
<view style="display:flex;align-items:center;gap:14rpx;margin-top:14rpx;">
|
||
<view style="display:flex;align-items:center;gap:4rpx;">
|
||
<text style="font-size:22rpx;">💎</text>
|
||
<text style="font-size:21rpx;color:#666;">专属推荐码</text>
|
||
</view>
|
||
<view style="display:flex;align-items:center;gap:4rpx;">
|
||
</view>
|
||
</view>
|
||
<!-- 去登录按钮 -->
|
||
<view style="margin-top:18rpx;display:flex;align-items:center;gap:6rpx;width:fit-content;background:linear-gradient(90deg,#FF6B6B 0%,#FF8E8E 100%);border-radius:32rpx;padding:14rpx 36rpx;box-shadow:0 4rpx 12rpx rgba(255,107,107,0.3);">
|
||
<text style="font-size:26rpx;color:#FFFFFF;font-weight:600;">{{ value.loginBtnText || '去登录' }}</text>
|
||
<text style="font-size:24rpx;color:#FFFFFF;">›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- ============ 已登录:推荐码卡片(后端驱动,3 套预设 + consent 遮罩) ============ -->
|
||
<view v-else>
|
||
<!-- ============ Consent 同意遮罩(全部内联样式,匹配样式4参考图) ============ -->
|
||
<view v-if="showConsent" style="width:100%;box-sizing:border-box;margin-top:24rpx;background:#FFFFFF;border-radius:32rpx;padding:28rpx 28rpx;position:relative;overflow:hidden;box-shadow:0 4rpx 16rpx rgba(0,0,0,0.06);">
|
||
<!-- 标题行 + 待获取标签 -->
|
||
<view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;">
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A' })">{{ value.title || '我的推荐码' }}</text>
|
||
<view style="display:inline-flex;align-items:center;background:rgba(255,152,0,0.12);border-radius:16rpx;padding:4rpx 14rpx;gap:4rpx;">
|
||
<text style="font-size:18rpx;color:#FF8F00;">✓</text>
|
||
<text style="font-size:20rpx;color:#FF8F00;font-weight:600;">待获取推荐码</text>
|
||
</view>
|
||
</view>
|
||
<!-- 推荐码占位+按钮区域:浅暖色背景 -->
|
||
<view style="margin-top:24rpx;background:linear-gradient(135deg,#FFF8F0 0%,#FFF5F0 50%,#FFF0EB 100%);border-radius:16rpx;padding:28rpx 24rpx 24rpx;position:relative;overflow:hidden;">
|
||
<!-- 右上角礼物装饰 -->
|
||
<view style="position:absolute;top:-10rpx;right:-6rpx;z-index:2;">
|
||
<text style="font-size:72rpx;">🎁</text>
|
||
<text style="position:absolute;top:-4rpx;left:-10rpx;font-size:14rpx;">✨</text>
|
||
</view>
|
||
<!-- 推荐码占位文字居中 -->
|
||
<view style="text-align:center;position:relative;z-index:1;">
|
||
<text style="font-size:48rpx;font-weight:900;color:#FFCCC0;letter-spacing:2rpx;font-family:'Courier New',monospace;">待获取</text>
|
||
</view>
|
||
<!-- 描述文字 -->
|
||
<view style="margin-top:12rpx;text-align:center;position:relative;z-index:1;">
|
||
<text style="font-size:22rpx;color:#999;line-height:1.5;">{{ value.consentText || '您的推荐码将用于企业入驻时识别推荐人关系,请确认后获取。' }}</text>
|
||
</view>
|
||
<!-- 同意并获取推荐码按钮:红橙渐变,小圆角,全宽 -->
|
||
<view @tap="confirmConsent" :style="loadingCode ? 'margin-top:16rpx;height:76rpx;display:flex;justify-content:center;align-items:center;background:linear-gradient(90deg,#FF6B35 0%,#FF4444 100%);border-radius:16rpx;opacity:0.7;pointer-events:none;position:relative;z-index:1;' : 'margin-top:16rpx;height:76rpx;display:flex;justify-content:center;align-items:center;background:linear-gradient(90deg,#FF6B35 0%,#FF4444 100%);border-radius:16rpx;box-shadow:0 4rpx 12rpx rgba(255,68,68,0.25);position:relative;z-index:1;'">
|
||
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">{{ 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>
|
||
|
||
<!-- ============ 推荐码卡片:仅在非 consent 且非首次加载时渲染 ============ -->
|
||
<block v-if="!showConsent && !(loadingCode && !code)">
|
||
<!-- ============ 预设 1:Gradient 渐变风格(默认)全部内联样式 ============ -->
|
||
<view v-if="stylePreset === 'gradient'" :style="$util.objToStyle({ width: '100%', boxSizing: 'border-box', marginTop: '24rpx', background: '#FFFFFF', borderRadius: px2rpx(value.borderRadius || 8) + 'px', padding: px2rpx(value.padding || 16) + 'px ' + px2rpx(value.padding || 16) + 'px', position: 'relative', overflow: 'hidden', boxShadow: '0 4rpx 16rpx rgba(0,0,0,0.06)' })">
|
||
<!-- 标题行 + 已获得推荐码标签 -->
|
||
<view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;">
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A' })">{{ value.title || '我的推荐码' }}</text>
|
||
<view style="display:inline-flex;align-items:center;background:#52C41A;border-radius:16rpx;padding:4rpx 14rpx;gap:4rpx;">
|
||
<text style="font-size:18rpx;color:#FFFFFF;">✓</text>
|
||
<text style="font-size:20rpx;color:#FFFFFF;font-weight:600;">已获得推荐码</text>
|
||
</view>
|
||
</view>
|
||
<!-- 推荐码+复制按钮区域:浅米黄色背景,圆角紧凑 -->
|
||
<view style="margin-top:24rpx;background:#FFF8F0;border-radius:16rpx;padding:32rpx 20rpx 24rpx;position:relative;z-index:1;">
|
||
<!-- 推荐码居中显示 -->
|
||
<view style="text-align:center;">
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(Math.max(16, Math.min(48, (Number(value.codeFontSize) || 28) * 0.93))) + 'px', fontWeight: '900', color: value.codeColor || '#DC143C', letterSpacing: '2rpx', fontFamily: 'Courier New, monospace' })">{{ code || value.placeholderCode || 'ABCD1234' }}</text>
|
||
</view>
|
||
<!-- 复制推荐码按钮:白色小按钮,红边框,在推荐码下方居中 -->
|
||
<view v-if="value.showCopyButton !== false" style="margin-top:16rpx;text-align:center;">
|
||
<view @tap="copyRecommendCode" :style="$util.objToStyle(copyBtnStyleObj({display:'inline-flex',alignItems:'center',borderRadius:'15px',padding:'0 16px',height:'30px',gap:'4px'}))">
|
||
<text style="font-size:22rpx;">📋</text>
|
||
<text :style="$util.objToStyle(copyBtnStyleObj({}, true))">{{ nc.copyButtonText || '复制推荐码' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 立即分享链接按钮:黄橙渐变,胶囊形圆角,紧凑布局
|
||
固定走 linear-gradient,不读 diyData.buttonBgColor(避免被后端默认色覆盖)
|
||
H5 端用 view 触发(避免 button 默认样式),小程序/APP 端保留 button 以触发 open-type=share -->
|
||
<button v-if="value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:16rpx 0 0 0;width:100%;text-align:left;background:transparent;border:0;padding:0;line-height:normal;display:block;box-sizing:border-box;">
|
||
<view style="display:block;width:100%;height:80rpx;line-height:80rpx;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:40rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);box-sizing:border-box;text-align:center;">
|
||
<text style="font-size:28rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
</button>
|
||
<view v-else-if="value.showShareButton !== false" @tap="onShareClick" style="margin-top:16rpx;width:100%;">
|
||
<view style="display:block;height:80rpx;line-height:80rpx;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:40rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);box-sizing:border-box;text-align:center;">
|
||
<text style="font-size:28rpx;font-weight:700;color:#FFFFFF;">📤 {{ nc.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- showLink -->
|
||
<view v-if="isFeatureOn(value.showLink)" style="margin-top:20rpx;text-align:center;position:relative;z-index:1;">
|
||
<text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text>
|
||
</view>
|
||
<!-- showQR -->
|
||
<view v-if="isFeatureOn(value.showQR)" style="margin-top:20rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;">
|
||
<view :style="$util.objToStyle({ width: px2rpx(nc.qrSize || 200) + 'px', height: px2rpx(nc.qrSize || 200) + 'px', background: nc.qrBoxBackground || 'transparent', border: nc.qrBoxBorderWidth && nc.qrBoxBorderColor ? (px2rpx(nc.qrBoxBorderWidth) + 'px solid ' + nc.qrBoxBorderColor) : 'none', borderRadius: px2rpx(nc.qrBoxBorderRadius || 0) + 'px', padding: px2rpx(nc.qrBoxPadding || 0) + 'px', boxSizing: 'border-box' })">
|
||
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:100%;height:100%;display:block;" />
|
||
</view>
|
||
<text style="font-size:22rpx;color:#999;margin-top:12rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ============ 预设 2:Minimal 极简风格(全部内联样式) ============ -->
|
||
<view v-if="stylePreset === 'minimal'" :style="$util.objToStyle({ width: '100%', boxSizing: 'border-box', marginTop: '24rpx', background: 'linear-gradient(90deg,#FFFFFF 0%,#FFF8F8 30%,#FFF5F5 60%,#FFF0F0 100%)', borderRadius: px2rpx(value.borderRadius || 8) + 'px', padding: px2rpx(value.padding || 16) + 'px ' + px2rpx(value.padding || 16) + 'px', position: 'relative', overflow: 'hidden', boxShadow: '0 4rpx 16rpx rgba(255,107,107,0.08)' })">
|
||
<!-- 背景礼物装饰大图1(右上角,移至顶部) -->
|
||
<view style="position:absolute;top:-20rpx;right:-30rpx;z-index:0;">
|
||
<text style="font-size:200rpx;opacity:0.15;">🎁</text>
|
||
</view>
|
||
<!-- 背景礼物装饰大图2(左下角) -->
|
||
<view style="position:absolute;bottom:-40rpx;left:-30rpx;z-index:0;">
|
||
<text style="font-size:180rpx;opacity:0.12;">🎁</text>
|
||
</view>
|
||
<!-- 背景散落小礼物装饰 -->
|
||
<view style="position:absolute;top:120rpx;left:30rpx;z-index:0;">
|
||
<text style="font-size:60rpx;opacity:0.15;">🎁</text>
|
||
</view>
|
||
<view style="position:absolute;top:200rpx;right:60rpx;z-index:0;">
|
||
<text style="font-size:48rpx;opacity:0.18;">🎀</text>
|
||
</view>
|
||
<view style="position:absolute;bottom:160rpx;right:20rpx;z-index:0;">
|
||
<text style="font-size:52rpx;opacity:0.15;">🎁</text>
|
||
</view>
|
||
<!-- 标题行 + 已获得标签 -->
|
||
<view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;">
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A' })">{{ value.title || '我的推荐码' }}</text>
|
||
<view style="display:inline-flex;align-items:center;background:rgba(255,107,107,0.15);border-radius:16rpx;padding:4rpx 14rpx;">
|
||
<text style="font-size:20rpx;color:#FF6B6B;font-weight:600;">已获得</text>
|
||
</view>
|
||
</view>
|
||
<!-- 描述文字 -->
|
||
<view style="margin-top:8rpx;padding-right:70rpx;position:relative;z-index:1;">
|
||
<text style="font-size:23rpx;color:#888;line-height:1.5;">分享给好友,邀请注册即可获得奖励</text>
|
||
</view>
|
||
<!-- 推荐码红色虚线框:推荐码居中,圆角更圆,更紧凑 -->
|
||
<view style="margin-top:20rpx;background:rgba(255,255,255,0.7);border:2rpx dashed #FF6B6B;border-radius:32rpx;padding:12rpx 16rpx;position:relative;z-index:1;">
|
||
<view style="text-align:center;">
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(Math.max(16, Math.min(48, (Number(value.codeFontSize) || 28) * 0.86))) + 'px', fontWeight: '800', color: value.codeColor || '#FF4444', letterSpacing: '2rpx', fontFamily: 'Courier New, monospace' })">{{ code || value.placeholderCode || 'ABCD1234' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 复制推荐码按钮:白色小按钮,在虚线框下方、立即分享链接上方 -->
|
||
<view v-if="value.showCopyButton !== false" style="margin-top:14rpx;display:flex;justify-content:center;position:relative;z-index:1;">
|
||
<view @tap="copyRecommendCode" :style="$util.objToStyle(copyBtnStyleObj({width:'fit-content',height:'30px',display:'flex',justifyContent:'center',alignItems:'center',borderRadius:'15px',padding:'0 16px',gap:'4px'}))">
|
||
<text style="font-size:22rpx;">📋</text>
|
||
<text :style="$util.objToStyle(copyBtnStyleObj({}, true))">{{ nc.copyButtonText || '复制推荐码' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 立即分享链接按钮:红橙渐变胶囊全宽铺满,触发微信原生分享
|
||
固定走 linear-gradient,不读 diyData.buttonBgColor
|
||
H5 端用 view 触发(避免 button 默认样式),小程序/APP 端保留 button 触发 open-type=share -->
|
||
<button v-if="value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:16rpx 0 0 0;width:100%;text-align:left;background:transparent;border:0;padding:0;line-height:normal;display:block;box-sizing:border-box;">
|
||
<view style="display:block;width:100%;height:76rpx;line-height:76rpx;background:linear-gradient(90deg,#FF6B6B 0%,#FF4444 100%);border-radius:38rpx;box-shadow:0 4rpx 12rpx rgba(255,68,68,0.3);box-sizing:border-box;text-align:center;">
|
||
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
</button>
|
||
<view v-else-if="value.showShareButton !== false" @tap="onShareClick" style="margin-top:16rpx;width:100%;">
|
||
<view style="display:block;width:100%;height:76rpx;line-height:76rpx;background:linear-gradient(90deg,#FF6B6B 0%,#FF4444 100%);border-radius:38rpx;box-shadow:0 4rpx 12rpx rgba(255,68,68,0.3);box-sizing:border-box;text-align:center;">
|
||
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ nc.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- showLink -->
|
||
<view v-if="isFeatureOn(value.showLink)" style="margin-top:16rpx;text-align:center;position:relative;z-index:1;">
|
||
<text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text>
|
||
</view>
|
||
<!-- showQR -->
|
||
<view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;">
|
||
<view :style="$util.objToStyle({ width: px2rpx(nc.qrSize || 200) + 'px', height: px2rpx(nc.qrSize || 200) + 'px', background: nc.qrBoxBackground || 'transparent', border: nc.qrBoxBorderWidth && nc.qrBoxBorderColor ? (px2rpx(nc.qrBoxBorderWidth) + 'px solid ' + nc.qrBoxBorderColor) : 'none', borderRadius: px2rpx(nc.qrBoxBorderRadius || 0) + 'px', padding: px2rpx(nc.qrBoxPadding || 0) + 'px', boxSizing: 'border-box' })">
|
||
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:100%;height:100%;display:block;" />
|
||
</view>
|
||
<text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ============ 预设 3:Card 卡片风格(全部内联样式) ============ -->
|
||
<view v-if="stylePreset === 'card'" :style="$util.objToStyle({ width: '100%', boxSizing: 'border-box', marginTop: '24rpx', background: 'linear-gradient(180deg,#FFF9F5 0%,#FFF5F0 100%)', borderRadius: px2rpx(value.borderRadius || 8) + 'px', padding: px2rpx(value.padding || 16) + 'px ' + px2rpx(value.padding || 16) + 'px', position: 'relative', overflow: 'hidden' })">
|
||
<!-- 右上角礼物装饰(开盖礼物盒) -->
|
||
<view style="position:absolute;top:16rpx;right:20rpx;z-index:2;">
|
||
<text style="font-size:52rpx;">🎁</text>
|
||
</view>
|
||
<!-- 标题行 + 已生效标签 -->
|
||
<view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;">
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A' })">{{ value.title || '我的推荐码' }}</text>
|
||
<view style="display:inline-flex;align-items:center;background:rgba(255,107,107,0.12);border-radius:16rpx;padding:4rpx 14rpx;">
|
||
<text style="font-size:20rpx;color:#FF6B6B;font-weight:600;">✓ 已生效</text>
|
||
</view>
|
||
</view>
|
||
<!-- 描述文字 -->
|
||
<view style="margin-top:8rpx;padding-right:70rpx;position:relative;z-index:1;">
|
||
<text style="font-size:23rpx;color:#999;line-height:1.5;">推荐码已生成,可用于邀请好友或企业入驻绑定推荐关系</text>
|
||
</view>
|
||
<!-- 推荐码红色虚线框:居中,圆角,紧凑 -->
|
||
<view style="margin-top:20rpx;display:flex;justify-content:center;position:relative;z-index:1;">
|
||
<view style="background:rgba(255,255,255,0.7);border:2rpx dashed #FF6B6B;border-radius:32rpx;padding:12rpx 32rpx;text-align:center;">
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(Math.max(16, Math.min(48, (Number(value.codeFontSize) || 28) * 0.71))) + 'px', fontWeight: '800', color: value.codeColor || '#FF4444', letterSpacing: '2rpx', fontFamily: 'Courier New, monospace' })">{{ code || value.placeholderCode || 'ABCD1234' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 复制推荐码按钮:白色小按钮,在虚线框下方、立即分享链接上方,居中 -->
|
||
<view v-if="value.showCopyButton !== false" style="margin-top:14rpx;display:flex;justify-content:center;position:relative;z-index:1;">
|
||
<view @tap="copyRecommendCode" :style="$util.objToStyle(copyBtnStyleObj({width:'fit-content',height:'30px',display:'flex',justifyContent:'center',alignItems:'center',borderRadius:'15px',padding:'0 16px',gap:'4px'}))">
|
||
<text style="font-size:22rpx;">📋</text>
|
||
<text :style="$util.objToStyle(copyBtnStyleObj({}, true))">{{ nc.copyButtonText || '复制推荐码' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 立即分享链接按钮:Card 风格固定「白底红边框」设计,不读 buttonBgColor
|
||
H5 端改用 view 触发(避免 <button> 默认样式影响),小程序/APP 端保留 button 以触发 open-type=share -->
|
||
<button v-if="value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:16rpx 0 0 0;width:100%;text-align:left;background:transparent;border:0;padding:0;line-height:normal;display:block;box-sizing:border-box;">
|
||
<view style="display:block;width:100%;height:76rpx;line-height:76rpx;background:#FFFFFF;border:2rpx solid #FF6B6B;border-radius:38rpx;box-sizing:border-box;text-align:center;">
|
||
<text style="font-size:27rpx;font-weight:600;color:#FF6B6B;">📤 {{ value.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
</button>
|
||
<view v-else-if="value.showShareButton !== false" @tap="onShareClick" style="margin-top:16rpx;width:100%;">
|
||
<view style="display:block;width:100%;height:76rpx;line-height:76rpx;background:#FFFFFF;border:2rpx solid #FF6B6B;border-radius:38rpx;box-sizing:border-box;text-align:center;">
|
||
<text style="font-size:27rpx;font-weight:600;color:#FF6B6B;">📤 {{ nc.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- showLink -->
|
||
<view v-if="isFeatureOn(value.showLink)" style="margin-top:16rpx;text-align:center;position:relative;z-index:1;">
|
||
<text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text>
|
||
</view>
|
||
<!-- showQR -->
|
||
<view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;">
|
||
<view :style="$util.objToStyle({ width: px2rpx(nc.qrSize || 200) + 'px', height: px2rpx(nc.qrSize || 200) + 'px', background: nc.qrBoxBackground || 'transparent', border: nc.qrBoxBorderWidth && nc.qrBoxBorderColor ? (px2rpx(nc.qrBoxBorderWidth) + 'px solid ' + nc.qrBoxBorderColor) : 'none', borderRadius: px2rpx(nc.qrBoxBorderRadius || 0) + 'px', padding: px2rpx(nc.qrBoxPadding || 0) + 'px', boxSizing: 'border-box' })">
|
||
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:100%;height:100%;display:block;" />
|
||
</view>
|
||
<text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ============ 预设 4:Horizontal 横向风格(全部内联样式,匹配参考图) ============ -->
|
||
<view v-if="stylePreset === 'horizontal'" :style="$util.objToStyle({ width: '100%', boxSizing: 'border-box', marginTop: '24rpx', background: '#FFFFFF', borderRadius: px2rpx(value.borderRadius || 8) + 'px', padding: px2rpx(value.padding || 16) + 'px ' + px2rpx(value.padding || 16) + 'px', position: 'relative', overflow: 'hidden', boxShadow: '0 4rpx 16rpx rgba(0,0,0,0.06)' })">
|
||
<!-- 卡片顶部:主标题 -->
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A', display: 'block' })">{{ value.title || '我的推荐码' }}</text>
|
||
<!-- 内容主体框:浅橙细边框,极浅米白底色 -->
|
||
<view style="margin-top:20rpx;background:#FFFCF8;border:2rpx solid #FFE0B2;border-radius:20rpx;padding:24rpx 24rpx 20rpx;position:relative;overflow:hidden;">
|
||
<!-- 2.1 状态标签(左上角) -->
|
||
<view style="display:inline-flex;align-items:center;background:rgba(255,152,0,0.1);border-radius:20rpx;padding:4rpx 14rpx;gap:4rpx;">
|
||
<view style="width:28rpx;height:28rpx;border-radius:50%;background:#FF9800;display:flex;align-items:center;justify-content:center;">
|
||
<text style="font-size:16rpx;color:#FFFFFF;">✓</text>
|
||
</view>
|
||
<text style="font-size:20rpx;color:#FF8F00;font-weight:600;">已获得推荐码</text>
|
||
</view>
|
||
<!-- 2.2 礼品装饰插画(容器右上角) -->
|
||
<view style="position:absolute;top:-8rpx;right:-4rpx;z-index:2;">
|
||
<text style="font-size:64rpx;">🎁</text>
|
||
<!-- 粉色、浅橙色飘散碎片装饰 -->
|
||
<text style="position:absolute;top:-2rpx;left:-12rpx;font-size:12rpx;">✨</text>
|
||
<text style="position:absolute;top:24rpx;left:-18rpx;font-size:10rpx;opacity:0.6;">✨</text>
|
||
<text style="position:absolute;top:-6rpx;right:10rpx;font-size:8rpx;opacity:0.5;">⭐</text>
|
||
</view>
|
||
<!-- 2.3 推荐码核心文本(左对齐) -->
|
||
<view style="margin-top:20rpx;position:relative;z-index:1;">
|
||
<text :style="$util.objToStyle({ fontSize: px2rpx(Math.max(16, Math.min(48, (Number(value.codeFontSize) || 28) * 1.07))) + 'px', fontWeight: '900', color: value.codeColor || '#FF4444', letterSpacing: '2rpx', fontFamily: 'Courier New, monospace' })">{{ code || value.placeholderCode || 'ABCD1234' }}</text>
|
||
</view>
|
||
<!-- 2.4 + 2.5 按钮行:左复制、右分享,按内容自适应宽度,两端对称对齐 -->
|
||
<view style="margin-top:16rpx;display:flex;align-items:center;justify-content:space-between;gap:16rpx;padding:0 8rpx;position:relative;z-index:1;">
|
||
<!-- 左下角:复制推荐码按钮(描边圆角胶囊,按内容自适应) -->
|
||
<view v-if="value.showCopyButton !== false" @tap="copyRecommendCode" :style="$util.objToStyle(copyBtnStyleObj({height:'32px',display:'inline-flex',justifyContent:'center',alignItems:'center',borderRadius:'16px',padding:'0 12px',gap:'3px'}))">
|
||
<text style="font-size:22rpx;">📋</text>
|
||
<text :style="$util.objToStyle(copyBtnStyleObj({}, true))">{{ nc.copyButtonText || '复制推荐码' }}</text>
|
||
</view>
|
||
<!-- 右下角:立即分享链接按钮(黄橙渐变胶囊,按内容自适应,触发微信原生分享)
|
||
H5 端用 view 触发,小程序/APP 端保留 button 以触发 open-type=share -->
|
||
<button v-if="value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:0;padding:0;text-align:center;background:transparent;border:0;line-height:normal;display:inline-block;">
|
||
<view style="display:inline-block;height:72rpx;line-height:72rpx;padding:0 32rpx;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:36rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);box-sizing:border-box;">
|
||
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
</button>
|
||
<view v-else-if="value.showShareButton !== false" @tap="onShareClick" style="height:72rpx;display:inline-flex;justify-content:center;align-items:center;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:36rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);padding:0 32rpx;line-height:1;box-sizing:border-box;">
|
||
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ nc.buttonText || '立即分享' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 换一个新码 -->
|
||
<view v-if="value.allowRegenerate === true" style="margin-top:16rpx;display:flex;justify-content:center;">
|
||
<view @tap="regenerate" style="background:rgba(255,68,68,0.06);border-radius:24rpx;padding:8rpx 24rpx;">
|
||
<text style="font-size:22rpx;color:#FF4444;font-weight:600;">{{ value.placeholderRegenerateText || '换一个新码' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- showLink -->
|
||
<view v-if="isFeatureOn(value.showLink)" style="margin-top:16rpx;text-align:center;">
|
||
<text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text>
|
||
</view>
|
||
<!-- showQR -->
|
||
<view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;">
|
||
<view :style="$util.objToStyle({ width: px2rpx(nc.qrSize || 200) + 'px', height: px2rpx(nc.qrSize || 200) + 'px', background: nc.qrBoxBackground || 'transparent', border: nc.qrBoxBorderWidth && nc.qrBoxBorderColor ? (px2rpx(nc.qrBoxBorderWidth) + 'px solid ' + nc.qrBoxBorderColor) : 'none', borderRadius: px2rpx(nc.qrBoxBorderRadius || 0) + 'px', padding: px2rpx(nc.qrBoxPadding || 0) + 'px', boxSizing: 'border-box' })">
|
||
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:100%;height:100%;display:block;" />
|
||
</view>
|
||
<text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
</view>
|
||
<!-- 登录组件占位(ns-login 全局 mixin 已注册) -->
|
||
<ns-login ref="nsLoginBox" :popup="true" @getUserInfo="onLoginSuccess"></ns-login>
|
||
<!-- ============ 分享海报 canvas(隐藏,仅小程序分享时生成推荐码组件的截图) ============ -->
|
||
<canvas v-if="!isH5Platform" canvas-id="sharePosterCanvas" id="sharePosterCanvas"
|
||
style="position:fixed;left:-9999px;top:-9999px;width:375px;height:480px;z-index:-1;"></canvas>
|
||
</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: '立即分享',
|
||
copyButtonText: '复制推荐码',
|
||
showButton: true,
|
||
showShareButton: true,
|
||
showCopyButton: true,
|
||
allowRegenerate: true,
|
||
placeholderRegenerateText: '换一个新码',
|
||
requireConsent: true,
|
||
consentText: '您的推荐码将用于企业入驻来源追踪,我们会记录每一次通过您分享链接完成的入驻申请,用于您的推广收益结算。',
|
||
consentButtonText: '同意并获取推荐码',
|
||
|
||
/* ============== 风格与样式 ============== */
|
||
stylePreset: 'gradient',
|
||
bgColorStart: '#3B82F6',
|
||
bgColorEnd: '#1E40AF',
|
||
bgColor: '#FFFFFF',
|
||
borderColor: '#E5E7EB',
|
||
accentColor: '#FFD700',
|
||
textColor: '#FFFFFF',
|
||
titleColor: '#1A1A1A',
|
||
codeColor: '#E4393C',
|
||
buttonBgColor: '', // 默认空:让 Card 风格的立即分享按钮走「白底红边框」分支,匹配设计参考图
|
||
buttonTextColor: '#FFFFFF',
|
||
shareButtonBgColor: '#67C23A',
|
||
titleFontSize: 14,
|
||
codeFontSize: 28,
|
||
borderRadius: 8,
|
||
padding: 16,
|
||
showQR: false,
|
||
showLink: false,
|
||
// 二维码容器样式(与后端 diyData / design.html 对齐)
|
||
qrSize: 200, // 二维码图片尺寸(前端 rpx)
|
||
qrBoxBorderColor: '', // 容器边框色;空=不渲染边框
|
||
qrBoxBorderWidth: 0, // 边框宽度(rpx)
|
||
qrBoxBorderRadius: 0, // 容器圆角(rpx)
|
||
qrBoxBackground: '', // 容器背景色;空=透明
|
||
qrBoxPadding: 0, // 容器内边距(rpx)
|
||
|
||
/* ============== 兼容旧字段(不破坏之前商家已经配置过的卡片) ============== */
|
||
// 外间距
|
||
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: '',
|
||
/* H5 平台标识:用于在 H5 端用 view 替代 button,避免浏览器 <button> 默认样式覆盖 */
|
||
isH5Platform: false,
|
||
/* 状态机 */
|
||
showConsent: false,
|
||
loadingCode: false,
|
||
codeLoaded: false,
|
||
|
||
__lastConsentRequired: false,
|
||
|
||
__cid: 'rc_' + Date.now() + '_' + Math.floor(Math.random() * 10000)
|
||
};
|
||
},
|
||
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', 'bookgold', 'horizontal'].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 [
|
||
'width:100%;',
|
||
'margin-top:24rpx;',
|
||
`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;
|
||
// 奶油米色背景
|
||
return this.cardBaseStyle + [
|
||
`background:#FDF6ED;`,
|
||
'box-shadow:0 12rpx 36rpx rgba(92,61,30,0.08);'
|
||
].join('');
|
||
},
|
||
/* 预设1专用:复制链接按钮白色背景样式 */
|
||
gradientCopyLinkStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`background:#FFFFFF;`,
|
||
`color:#5C3D1E;`,
|
||
`border-radius:40rpx;`,
|
||
`padding:14rpx 56rpx;`,
|
||
'font-size:26rpx;font-weight:600;',
|
||
'box-shadow:0 4rpx 12rpx rgba(92,61,30,0.1);'
|
||
].join('');
|
||
},
|
||
bookGoldStyle() {
|
||
const v = this.nc;
|
||
return this.cardBaseStyle + [
|
||
`background:#FFFFFF;`,
|
||
'box-shadow:0 12rpx 36rpx rgba(166,139,106,0.15);',
|
||
'border-radius:24rpx;'
|
||
,`border:3rpx solid #0066FF;`
|
||
].join('');
|
||
},
|
||
bookGoldCodeBoxStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`background:rgba(255,255,255,0.12);`,
|
||
`border-radius:${this.px2rpx(v.borderRadius)}rpx;`,
|
||
'margin-top:28rpx;'
|
||
].join('');
|
||
},
|
||
bookGoldCopyBtnStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`background:#E62E34;`, // 按钮红色背景,匹配图中大红按钮
|
||
`color:#ffffff;`, // 白色文字
|
||
`border-radius:999rpx;`, // 极致胶囊圆角(超大圆角)
|
||
`font-size:24rpx;`,
|
||
`font-weight:bold;`, // 文字加粗,和原图一致
|
||
`padding:22rpx 70rpx;`, // 上下左右内边距撑开按钮大小
|
||
`border:none;` // 清除默认边框
|
||
].join('');
|
||
},
|
||
minimalStyle() {
|
||
const v = this.nc;
|
||
return this.cardBaseStyle + [
|
||
`background:${v.bgColor || '#FDF6ED'};`,
|
||
`border:2rpx solid ${v.borderColor || '#E8D5C0'};`
|
||
].join('');
|
||
},
|
||
minimalCodeBoxStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`border:2rpx dashed ${v.codeColor || '#6B4423'};`,
|
||
`border-radius:${this.px2rpx(Math.max(4, (Number(v.borderRadius) || 8) - 4))}rpx;`,
|
||
'margin-top:28rpx;',
|
||
'display:flex;flex-direction:row;align-items:center;flex-wrap:nowrap;'
|
||
].join('');
|
||
},
|
||
minimalCopyBtnStyle() {
|
||
const v = this.nc;
|
||
return [
|
||
`background:${v.buttonBgColor || '#6B4423'};`,
|
||
`color:${v.buttonTextColor || '#FFFFFF'};`,
|
||
`border-radius:${this.px2rpx(v.borderRadius)}rpx;`,
|
||
`font-size:24rpx;padding:14rpx 24rpx;`
|
||
].join('');
|
||
},
|
||
cardStyleBox() {
|
||
const v = this.nc;
|
||
return this.cardBaseStyle + [
|
||
`background:${v.bgColor || '#FFE4B5'};`,
|
||
`border:2rpx solid ${v.accentColor || '#F0C040'};`,
|
||
'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 || (this.stylePreset === 'horizontal' ? '#FFFFFF' : (this.stylePreset === 'card' ? '#B8860B' : '#5C3D1E'))};`,
|
||
`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;
|
||
const sp = this.stylePreset;
|
||
const secBg = sp === 'gradient' ? 'rgba(92,61,30,0.08)' : (sp === 'bookgold' ? 'rgba(255,255,255,0.12)' : (sp === 'card' ? 'rgba(184,134,11,0.08)' : 'rgba(92,61,30,0.06)'));
|
||
const secColor = sp === 'gradient' ? '#5C3D1E' : (sp === 'bookgold' ? '#FFFFFF' : (sp === 'card' ? '#B8860B' : '#5C3D1E'));
|
||
return [
|
||
`background:${secBg};`,
|
||
`color:${secColor};`,
|
||
`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, #FF7530 0%, #FF2438 100%);',
|
||
'color:#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;box-shadow:0 6rpx 16rpx rgba(255,138,110,0.25);'
|
||
].join('');
|
||
},
|
||
/* ---------- 二维码渲染 URL ---------- */
|
||
qrcodeRenderUrl() {
|
||
// 优先用后端占位接口返回的真实 qrcode_url;没返回就 fallback 用公共 api 合成 share_link 的二维码
|
||
if (this.qrcodeUrl && String(this.qrcodeUrl).trim()) return this.qrcodeUrl;
|
||
const realUrl = this.formatShareLink;
|
||
if (!realUrl) return '';
|
||
const url = encodeURIComponent(realUrl);
|
||
return 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&margin=0&data=' + url;
|
||
},
|
||
formatShareLink() {
|
||
// 读取后台配置完整带{code}的模板
|
||
const template = this.nc.customShareLink || "http://192.168.8.118/hwappx/common/pages_tool/form/formdata?id=13&uniacid=1&recommend_code={code}";
|
||
// 把模板里 {code} 整体替换成 {推荐码}
|
||
return template.replace(/\{code\}/g, this.code);
|
||
},
|
||
},
|
||
watch: {
|
||
'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(_) {}
|
||
}
|
||
}
|
||
},
|
||
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 {
|
||
// H5 端检测:避开 <button> 默认样式影响(h5 端改用 view 触发)
|
||
try {
|
||
// #ifdef H5
|
||
this.isH5Platform = true;
|
||
// #endif
|
||
} catch (e) {}
|
||
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 || '邀您入驻低空经济产业平台';
|
||
// 小程序环境下,生成「仅推荐码组件」的截图海报作为分享图
|
||
// H5 平台不进入 canvas 路径,直接用 diyData.shareImageUrl
|
||
if (!this.isH5Platform) {
|
||
return new Promise((resolve) => {
|
||
// 先生成海报;失败或非小程序时回退到默认 imageUrl
|
||
this.generateSharePoster()
|
||
.then((posterPath) => {
|
||
resolve({
|
||
title,
|
||
path: this.extractMiniProgramPath(),
|
||
imageUrl: posterPath || (v.shareImageUrl || '')
|
||
});
|
||
})
|
||
.catch(() => {
|
||
resolve({
|
||
title,
|
||
path: this.extractMiniProgramPath(),
|
||
imageUrl: v.shareImageUrl || ''
|
||
});
|
||
});
|
||
});
|
||
}
|
||
return {
|
||
title,
|
||
path: this.extractMiniProgramPath(),
|
||
imageUrl: v.shareImageUrl || ''
|
||
};
|
||
},
|
||
/**
|
||
* 小程序分享到朋友圈(uniapp 生命周期钩子,必须在 export default 对象上,不能进 methods)
|
||
*/
|
||
onShareTimeline() {
|
||
const v = this.nc;
|
||
// 从H5分享链接中提取完整查询参数,与分享给好友保持一致
|
||
var query = '';
|
||
try {
|
||
var shareLink = this.formatShareLink;
|
||
var queryIndex = shareLink.indexOf('?');
|
||
if (queryIndex >= 0) {
|
||
query = shareLink.substring(queryIndex + 1); // id=13&uniacid=1&recommend_code=XXX
|
||
}
|
||
} catch(e) {}
|
||
if (!query && this.code) query = 'recommend_code=' + encodeURIComponent(this.code);
|
||
return {
|
||
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
|
||
query: query,
|
||
imageUrl: v.shareImageUrl || ''
|
||
};
|
||
},
|
||
methods: {
|
||
|
||
mainBtnBgStyle(isCopyMini) {
|
||
const v = this.nc;
|
||
const sp = this.stylePreset;
|
||
const presetBtnColors = { gradient: '#FF9933', minimal: '#6B4423', card: '#DAA520', bookgold: '#B8A08A' };
|
||
const defaultBtnBg = presetBtnColors[sp] || '#FF9933';
|
||
const bg = isCopyMini ? 'rgba(255,255,255,0.92)' : (v.buttonBgColor || defaultBtnBg);
|
||
const color = isCopyMini ? (v.buttonBgColor || defaultBtnBg) : (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('');
|
||
},
|
||
/**
|
||
* 复制按钮样式对象(跟随后端 diyData.codeColor)
|
||
* 返回对象,用 $util.objToStyle 转字符串,
|
||
* 兼容 H5/小程序/APP/鸿蒙
|
||
* 注意:使用 px 而非 rpx 单位,避免鸿蒙元服务对动态 style 中 rpx
|
||
* 解析异常的问题。1rpx = 0.5px,直接换算成 px 数值。
|
||
* @param {Object} preset 预设差异(可选,如 {borderRadius:'16px',height:'32px'})
|
||
* @param {Boolean} textOnly 只返回文字样式(用于内嵌 text 节点)
|
||
*/
|
||
copyBtnStyleObj(preset, textOnly) {
|
||
const v = this.nc;
|
||
const codeColor = (v && v.codeColor) ? v.codeColor : '#DC143C';
|
||
if (textOnly) {
|
||
return Object.assign({
|
||
fontSize: '12px', // 原 24rpx → 12px
|
||
color: codeColor,
|
||
fontWeight: '600'
|
||
}, preset || {});
|
||
}
|
||
const base = {
|
||
background: '#FFFFFF',
|
||
border: `1px solid ${codeColor}`, // 原 2rpx → 1px
|
||
boxSizing: 'border-box'
|
||
};
|
||
return Object.assign(base, preset || {});
|
||
},
|
||
px2rpx(val) {
|
||
// 后端 diyData 数值语义是 px(商家在设计器按 px 输入,后端 wap/design.html
|
||
// 也都用 px 输出)。前端之前 *2 转 rpx 是错的,商家设置 8px 实际渲染
|
||
// 成 16px 偏大。鸿蒙元服务对动态拼接的 rpx 也有解析问题,所以这里
|
||
// 直接返回原值,作为 px 使用。
|
||
// 调用方:动态拼接时用 + 'px'(不要用 + 'px')。
|
||
return Number(val || 0)
|
||
},
|
||
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]);
|
||
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;
|
||
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') {
|
||
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;
|
||
}
|
||
}
|
||
|
||
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){}
|
||
|
||
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' });
|
||
}
|
||
// 拿到码后立即预生成分享海报(不等二维码,先占位;二维码拿到后会再次更新)
|
||
try { vm._prepareSharePoster(); } catch(e){}
|
||
// 拿到码后如果 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
|
||
*/
|
||
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);
|
||
}
|
||
// 拿到二维码后立即预生成「仅推荐码组件」分享海报
|
||
// 这样用户点"立即分享"时,storage 里已有海报,页面级 onShareAppMessage
|
||
// 同步读取后返回 imageUrl,微信就不会用全屏截图
|
||
try { vm._prepareSharePoster(); } catch(e) {}
|
||
}
|
||
});
|
||
} catch (e) {}
|
||
},
|
||
/**
|
||
* 预生成分享海报(异步,完成后写入 _recommendShareData storage)
|
||
* 小程序端:onShareAppMessage 读取这个 storage 拿到 imageUrl
|
||
* 非小程序端:跳过,onShareClick 不依赖它
|
||
*/
|
||
_prepareSharePoster() {
|
||
const vm = this;
|
||
console.log('[推荐码分享] _prepareSharePoster 被调用,isH5Platform=', this.isH5Platform, 'code=', this.code);
|
||
// 仅小程序需要这个(其他端 onShareClick 不依赖 storage)
|
||
// #ifdef MP-WEIXIN
|
||
if (!this.code) {
|
||
console.warn('[推荐码分享] 跳过预生成:推荐码为空');
|
||
return;
|
||
}
|
||
// 安全:isH5Platform 为 true 时不进入(H5 下 canvas 不渲染,createCanvasContext 会失败)
|
||
if (this.isH5Platform) {
|
||
console.log('[推荐码分享] H5 环境跳过海报预生成');
|
||
return;
|
||
}
|
||
try {
|
||
console.log('[推荐码分享] 开始调 generateSharePoster...');
|
||
this.generateSharePoster().then(function(posterPath) {
|
||
console.log('[推荐码分享] generateSharePoster resolved, posterPath=', posterPath);
|
||
if (!posterPath) {
|
||
console.warn('[推荐码分享] 海报路径为空,跳过');
|
||
return;
|
||
}
|
||
try {
|
||
uni.setStorageSync('_recommendSharePoster', posterPath);
|
||
const data = {
|
||
title: (vm.nc && (vm.nc.shareTitle || vm.nc.title)) || '邀您入驻低空经济产业平台',
|
||
path: vm.extractMiniProgramPath(),
|
||
imageUrl: posterPath
|
||
};
|
||
uni.setStorageSync('_recommendShareData', JSON.stringify(data));
|
||
console.log('[推荐码分享] 海报预生成完成,imageUrl=', posterPath);
|
||
} catch(e) {
|
||
console.error('[推荐码分享] 写 storage 失败:', e);
|
||
}
|
||
}).catch(function(err) {
|
||
console.warn('[推荐码分享] 海报预生成失败', err);
|
||
});
|
||
} catch(e) {
|
||
console.error('[推荐码分享] _prepareSharePoster try 块异常:', e);
|
||
}
|
||
// #endif
|
||
},
|
||
|
||
onShareClick() {
|
||
console.log('[推荐码分享]点击分享按钮,完整推广链接:', this.formatShareLink,'当前推荐码:',this.code);
|
||
const vm = this;
|
||
const v = this.nc;
|
||
const shareUrl = this.formatShareLink; // 改用后台拼接链接
|
||
// #ifdef H5
|
||
try {
|
||
if (navigator && navigator.share && typeof navigator.share && shareUrl) {
|
||
navigator.share({
|
||
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
|
||
text: v.shareDesc || '',
|
||
url: shareUrl
|
||
}).catch(() => vm.copyLink());
|
||
return;
|
||
}
|
||
} catch (e) {}
|
||
this.copyLink();
|
||
return;
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
if (plus && plus.share && shareUrl) {
|
||
plus.share.sendWithSystem({
|
||
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
|
||
content: v.shareDesc || '',
|
||
href: shareUrl
|
||
}, function () {}, function () { vm.copyLink(); });
|
||
return;
|
||
}
|
||
} catch (e) {}
|
||
this.copyLink();
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN
|
||
// open-type="share" 会自动触发 onShareAppMessage 弹出微信原生分享面板
|
||
// 存储分享数据供页面级 onShareAppMessage 读取(组件级钩子会被页面级覆盖)
|
||
console.log('分享链接准备,原始配置', v);
|
||
console.log('生成的分享完整路径:', this.extractMiniProgramPath());
|
||
|
||
// 先用上次缓存的海报路径占位(避免用户快速二次分享时空白)
|
||
var cachedPoster = '';
|
||
try { cachedPoster = uni.getStorageSync('_recommendSharePoster') || ''; } catch(e) {}
|
||
try {
|
||
var shareData = {
|
||
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
|
||
path: this.extractMiniProgramPath(),
|
||
imageUrl: cachedPoster || v.shareImageUrl || ''
|
||
};
|
||
uni.setStorageSync('_recommendShareData', JSON.stringify(shareData));
|
||
} catch(e) {}
|
||
|
||
// 异步生成「仅推荐码组件」的海报图,生成后更新 storage
|
||
// 微信端:onShareAppMessage 触发时 _recommendShareData 中已是新海报
|
||
this.generateSharePoster().then(function(posterPath) {
|
||
if (!posterPath) return;
|
||
try {
|
||
// 缓存本次海报,下次分享时直接用
|
||
uni.setStorageSync('_recommendSharePoster', posterPath);
|
||
var data = {
|
||
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
|
||
path: vm.extractMiniProgramPath(),
|
||
imageUrl: posterPath
|
||
};
|
||
uni.setStorageSync('_recommendShareData', JSON.stringify(data));
|
||
console.log('[推荐码分享]海报已生成并写入 storage', posterPath);
|
||
} catch(e) {}
|
||
}).catch(function(err) {
|
||
console.warn('[推荐码分享]海报生成失败,使用 fallback', err);
|
||
});
|
||
|
||
return;
|
||
// #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'
|
||
});
|
||
});
|
||
},
|
||
|
||
|
||
copyLink() {
|
||
// 直接读取后台配置拼接后的完整链接
|
||
const targetUrl = this.formatShareLink;
|
||
if (!targetUrl) {
|
||
uni.showToast({ title: '推荐码加载完成后再复制', icon: 'none' });
|
||
return;
|
||
}
|
||
rcCopyToClipboard(this, targetUrl, (ok, err) => {
|
||
uni.showToast({
|
||
title: ok ? '推广链接已复制' : (err || '复制失败'),
|
||
icon: ok ? 'success' : 'none'
|
||
});
|
||
});
|
||
},
|
||
extractMiniProgramPath() {
|
||
// 从H5分享链接中提取完整查询参数,保持小程序分享与H5链接参数一致
|
||
// H5链接如: http://xxx/hwappx/common/pages_tool/form/formdata?id=13&uniacid=1&recommend_code=XXX
|
||
// 小程序路径如: /pages_tool/form/formdata?id=13&uniacid=1&recommend_code=XXX
|
||
const shareLink = this.formatShareLink;
|
||
try {
|
||
var queryIndex = shareLink.indexOf('?');
|
||
if (queryIndex >= 0) {
|
||
var query = shareLink.substring(queryIndex); // ?id=13&uniacid=1&recommend_code=XXX
|
||
return '/pages_tool/form/formdata' + query;
|
||
}
|
||
} catch(e) {}
|
||
// 兜底:只带recommend_code
|
||
if (!this.code) return '/pages_tool/form/formdata';
|
||
return '/pages_tool/form/formdata?recommend_code=' + encodeURIComponent(this.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() {
|
||
if (this.$refs.nsLoginBox && this.$refs.nsLoginBox.open) {
|
||
this.$refs.nsLoginBox.open();
|
||
} else {
|
||
// 降级:ns-login 未就绪时跳转登录页
|
||
uni.navigateTo({
|
||
url: '/pages_tool/login/login',
|
||
fail: () => {
|
||
uni.redirectTo({ url: '/pages_tool/login/login' });
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 生成推荐码组件的分享海报(canvas 绘制 → 临时文件路径)
|
||
* 画布内容:白底 + 标题 + 推荐码大字 + 二维码(如有) + 简短提示
|
||
* 返回 Promise<tempFilePath>
|
||
*/
|
||
generateSharePoster() {
|
||
const v = this.nc;
|
||
const code = this.code || v.placeholderCode || 'ABCD1234';
|
||
const title = v.title || '我的推荐码';
|
||
const sp = this.stylePreset;
|
||
// ★ 所有视觉参数都从 diyData.value 读取,后端可改
|
||
const palette = {
|
||
bg: v.bgColor || '#FFFFFF',
|
||
bgStart: v.bgColorStart, // 渐变起始(可选)
|
||
bgEnd: v.bgColorEnd, // 渐变结束(可选)
|
||
border: v.borderColor || '#E5E7EB',
|
||
borderRadius: Number(v.borderRadius) || 12,
|
||
padding: Number(v.padding) || 16,
|
||
titleColor: v.titleColor || '#1A1A1A',
|
||
titleFontSize: Number(v.titleFontSize) || 18,
|
||
codeColor: v.codeColor || '#DC143C',
|
||
codeFontSize: Number(v.codeFontSize) || 36,
|
||
accent: v.accentColor || '#FFD700',
|
||
text: v.textColor || '#303133',
|
||
subtitle: '#999999',
|
||
codeBlockBg: '#FFF8F0', // 推荐码区背景(米黄)
|
||
codeBlockBorder: '#FFB74D'
|
||
};
|
||
const W = 375;
|
||
const H = 480;
|
||
console.log('[推荐码分享] generateSharePoster 进入,stylePreset=', sp);
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
const ctx = uni.createCanvasContext('sharePosterCanvas', this);
|
||
if (!ctx) return reject(new Error('createCanvasContext 失败'));
|
||
console.log('[推荐码分享] createCanvasContext 返回');
|
||
// ====================== 绘制海报 ======================
|
||
// 1. 整张背景(根据后端 bgColorStart/End 渐变,否则用 bgColor 单色)
|
||
this._drawPosterBg(ctx, W, H, palette);
|
||
// 2. 卡片(根据 stylePreset 选择布局)
|
||
this._drawPosterCard(ctx, W, H, palette, sp);
|
||
// 3. 标题(读 titleColor/titleFontSize)
|
||
ctx.setFontSize(palette.titleFontSize);
|
||
ctx.setFillStyle(palette.titleColor);
|
||
ctx.setTextAlign('left');
|
||
ctx.fillText(title, 24, 44);
|
||
// 4. 副标题(根据后端配置,可选)
|
||
const showLink = this.isFeatureOn(v.showLink);
|
||
const showQR = this.isFeatureOn(v.showQR);
|
||
ctx.setFontSize(12);
|
||
ctx.setFillStyle(palette.subtitle);
|
||
ctx.fillText('分享给好友,邀请入驻获得奖励', 24, 70);
|
||
// 4.5 如果后端开启 showLink,绘制分享链接
|
||
let urlY = 88;
|
||
if (showLink) {
|
||
try {
|
||
ctx.setFontSize(11);
|
||
ctx.setFillStyle(palette.text || '#666');
|
||
ctx.setTextAlign('left');
|
||
// 缩短显示:最多 30 字符
|
||
let linkText = (v.shareLink || this.shareLink || '').toString();
|
||
if (linkText.length > 36) linkText = linkText.slice(0, 36) + '…';
|
||
ctx.fillText('链接:' + (linkText || '(未生成)'), 24, urlY);
|
||
urlY += 16; // 后续内容往下挪
|
||
} catch (e) {}
|
||
}
|
||
// 5. 推荐码区域(读 codeColor/codeFontSize/borderRadius)
|
||
// blockY 根据 showLink 动态下移
|
||
const blockY = urlY, blockH = 110, blockX = 24, blockW = W - 48;
|
||
const codeBlockRadius = Math.min(palette.borderRadius, 16);
|
||
// 推荐码区背景
|
||
ctx.setFillStyle(palette.codeBlockBg);
|
||
this._roundRect(ctx, blockX, blockY, blockW, blockH, codeBlockRadius);
|
||
ctx.fill();
|
||
// 推荐码区边框(浅橙)
|
||
ctx.setStrokeStyle(palette.codeBlockBorder);
|
||
ctx.setLineWidth(1);
|
||
this._roundRect(ctx, blockX, blockY, blockW, blockH, codeBlockRadius);
|
||
ctx.stroke();
|
||
// 推荐码大字(居中)
|
||
ctx.setFontSize(palette.codeFontSize);
|
||
ctx.setFillStyle(palette.codeColor);
|
||
ctx.setTextAlign('center');
|
||
if (typeof ctx.setFontWeight === 'function') {
|
||
ctx.setFontWeight('bold');
|
||
} else {
|
||
try { ctx.font = 'bold ' + palette.codeFontSize + 'px sans-serif'; } catch(e) {}
|
||
}
|
||
ctx.fillText(code, W / 2, blockY + blockH / 2 + 12);
|
||
// 6. 二维码(如果有)
|
||
const qr = this.qrcodeRenderUrl;
|
||
const drawQrcodeAndExport = (qrLocalPath) => {
|
||
if (qrLocalPath) {
|
||
const qrSize = 180;
|
||
const qrX = (W - qrSize) / 2;
|
||
const qrY = blockY + blockH + 24;
|
||
// 白底框
|
||
ctx.setFillStyle('#FFFFFF');
|
||
this._roundRect(ctx, qrX - 6, qrY - 6, qrSize + 12, qrSize + 12, codeBlockRadius);
|
||
ctx.fill();
|
||
ctx.drawImage(qrLocalPath, qrX, qrY, qrSize, qrSize);
|
||
// 底部提示
|
||
ctx.setFontSize(11);
|
||
ctx.setFillStyle(palette.subtitle);
|
||
ctx.setTextAlign('center');
|
||
ctx.fillText(v.posterQrTip || '扫码进入并填写入驻信息', W / 2, qrY + qrSize + 24);
|
||
}
|
||
console.log('[推荐码分享] 准备 ctx.draw(false, cb)');
|
||
ctx.draw(false, () => {
|
||
console.log('[推荐码分享] ctx.draw callback 触发,准备 _exportCanvas');
|
||
this._exportCanvas(W, H).then(resolve).catch(reject);
|
||
});
|
||
};
|
||
if (qr) {
|
||
this._downloadImage(qr).then(drawQrcodeAndExport).catch(() => drawQrcodeAndExport(null));
|
||
} else {
|
||
drawQrcodeAndExport(null);
|
||
}
|
||
} catch (e) {
|
||
console.error('[推荐码分享] generateSharePoster try 块异常:', e);
|
||
reject(e);
|
||
}
|
||
});
|
||
},
|
||
/**
|
||
* 海报背景:根据 diyData.bgColorStart/End 画渐变,否则用 bgColor 单色
|
||
*/
|
||
_drawPosterBg(ctx, W, H, palette) {
|
||
// 海报"画布外"背景:用 bgColor(浅色),不做整体渐变
|
||
try {
|
||
ctx.setFillStyle(palette.bg || '#FFFFFF');
|
||
ctx.fillRect(0, 0, W, H);
|
||
} catch (e) {
|
||
ctx.setFillStyle('#FFFFFF');
|
||
ctx.fillRect(0, 0, W, H);
|
||
}
|
||
},
|
||
/**
|
||
* 卡片绘制:画海报中部的小"卡片"(模仿组件的卡片视图)
|
||
* 根据 stylePreset 选不同视觉:
|
||
* - gradient: 卡片用 bgColorStart→bgColorEnd 渐变
|
||
* - minimal: 卡片用 bgColorStart→bgColorEnd 渐变(粉/米)
|
||
* - card: 卡片用 bgColorStart→bgColorEnd 渐变(米橙)
|
||
* - horizontal: 卡片用 bgColorStart→bgColorEnd 渐变
|
||
* 所有预设的卡片圆角读 borderRadius
|
||
*/
|
||
_drawPosterCard(ctx, W, H, palette, sp) {
|
||
// 不再画独立卡片,推荐码区本身就是"卡片"
|
||
// 这里只根据 stylePreset 在卡片右上角加 emoji 装饰
|
||
try {
|
||
ctx.setFontSize(36);
|
||
// 不同预设的装饰色 + emoji
|
||
const decos = {
|
||
gradient: { color: 'rgba(255,143,0,0.4)', emoji: '🎁' },
|
||
minimal: { color: 'rgba(255,107,107,0.35)', emoji: '🎁' },
|
||
card: { color: 'rgba(218,165,32,0.4)', emoji: '🎁' },
|
||
horizontal: { color: 'rgba(184,160,138,0.4)', emoji: '🎁' }
|
||
};
|
||
const deco = decos[sp] || decos.gradient;
|
||
ctx.setFillStyle(deco.color);
|
||
ctx.setTextAlign('right');
|
||
// 装饰位置:推荐码区右上角(blockX+W/2 区域,blockY 上方)
|
||
ctx.fillText(deco.emoji, W - 40, 78);
|
||
} catch (e) {}
|
||
},
|
||
/**
|
||
* canvas 转图片(uni.canvasToTempFilePath Promise 化)
|
||
*/
|
||
_exportCanvas(W, H) {
|
||
return new Promise((resolve, reject) => {
|
||
console.log('[推荐码分享] _exportCanvas 开始执行');
|
||
uni.canvasToTempFilePath({
|
||
canvasId: 'sharePosterCanvas',
|
||
width: W,
|
||
height: H,
|
||
destWidth: W * 2,
|
||
destHeight: H * 2,
|
||
fileType: 'png',
|
||
quality: 1,
|
||
success: (r) => {
|
||
console.log('[推荐码分享] canvasToTempFilePath 成功:', r.tempFilePath);
|
||
resolve(r.tempFilePath);
|
||
},
|
||
fail: (err) => {
|
||
console.error('[推荐码分享] canvasToTempFilePath 失败:', err);
|
||
reject(err);
|
||
}
|
||
}, this);
|
||
// 兜底超时:5s 后还没返回就 reject,避免永远卡住
|
||
setTimeout(() => {
|
||
console.warn('[推荐码分享] canvasToTempFilePath 超时');
|
||
reject(new Error('canvasToTempFilePath 超时'));
|
||
}, 5000);
|
||
});
|
||
},
|
||
/**
|
||
* 圆角矩形路径(canvas API)
|
||
*/
|
||
_roundRect(ctx, x, y, w, h, r) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + r, y);
|
||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||
ctx.arcTo(x, y + h, x, y, r);
|
||
ctx.arcTo(x, y, x + w, y, r);
|
||
ctx.closePath();
|
||
},
|
||
/**
|
||
* 下载图片到临时路径(Promise 化)
|
||
* 支持本地路径直接返回
|
||
*/
|
||
_downloadImage(url) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!url) return reject(new Error('url 为空'));
|
||
if (!/^https?:\/\//i.test(url)) return resolve(url);
|
||
uni.downloadFile({
|
||
url,
|
||
success: (res) => {
|
||
if (res.statusCode === 200 && res.tempFilePath) resolve(res.tempFilePath);
|
||
else reject(new Error('下载失败:' + res.statusCode));
|
||
},
|
||
fail: reject
|
||
});
|
||
});
|
||
}
|
||
}
|
||
};
|
||
</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; }
|
||
}
|
||
|
||
/* ===== 同意遮罩:分享推荐码 邀请有礼 ===== */
|
||
.consent-overlay {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
background: linear-gradient(180deg, #FFF9F5 0%, #FFF0F0 100%);
|
||
border-radius: 24rpx;
|
||
padding: 40rpx 32rpx 36rpx;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
/* 顶部标签 */
|
||
.consent-tag {
|
||
display: inline-block;
|
||
background: rgba(255,107,107,0.1);
|
||
border-radius: 20rpx;
|
||
padding: 6rpx 20rpx;
|
||
text { font-size: 22rpx; color: #FF6B6B; font-weight: 600; }
|
||
}
|
||
|
||
/* 主标题区 */
|
||
.consent-hero {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 28rpx;
|
||
.consent-hero-text {
|
||
flex: 1;
|
||
.hero-main { font-size: 40rpx; font-weight: 800; color: #1A1A1A; display: block; }
|
||
.hero-accent {
|
||
font-size: 40rpx; font-weight: 800;
|
||
background: linear-gradient(90deg, #FF6B6B 0%, #FF9933 100%);
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
color: #FF6B6B;
|
||
}
|
||
}
|
||
.consent-gift {
|
||
flex-shrink: 0;
|
||
margin-left: 20rpx;
|
||
.gift-emoji { font-size: 80rpx; }
|
||
}
|
||
}
|
||
|
||
/* 描述文字 */
|
||
.consent-desc {
|
||
margin-top: 20rpx;
|
||
text { font-size: 25rpx; color: #888; line-height: 1.7; }
|
||
}
|
||
|
||
/* 特性图标行 */
|
||
.consent-features {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 36rpx;
|
||
padding: 0 4rpx;
|
||
.feature-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
flex: 1;
|
||
.feature-icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text { font-size: 36rpx; }
|
||
}
|
||
.feature-icon-1 { background: rgba(255,107,107,0.12); }
|
||
.feature-icon-2 { background: rgba(255,153,51,0.12); }
|
||
.feature-icon-3 { background: rgba(255,153,51,0.12); }
|
||
.feature-label { font-size: 21rpx; color: #666; margin-top: 12rpx; }
|
||
}
|
||
}
|
||
|
||
/* 按钮 */
|
||
.consent-action {
|
||
margin-top: 40rpx;
|
||
.consent-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background: linear-gradient(90deg, #FF6B6B 0%, #FF9933 100%);
|
||
border-radius: 44rpx;
|
||
text { font-size: 30rpx; font-weight: 700; color: #FFFFFF; }
|
||
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 {
|
||
color: #8B7355;
|
||
background: rgba(92,61,30,0.04);
|
||
opacity: 1;
|
||
}
|
||
&.rc-link-card .rc-link-text {
|
||
color: #B8860B;
|
||
background: rgba(184,134,11,0.06);
|
||
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 #E8D5C0; }
|
||
.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 #F0C040;
|
||
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: #5C3D1E; }
|
||
.rc-code-gradient {
|
||
background: transparent;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 36rpx 30rpx;
|
||
.rc-code-gradient-text {
|
||
text-align: center;
|
||
letter-spacing: 4rpx;
|
||
}
|
||
}
|
||
.rc-copy-link-gradient {
|
||
margin-top: 24rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
}
|
||
.rc-link-row .rc-link-text {
|
||
background: rgba(92,61,30,0.08);
|
||
color: #5C3D1E;
|
||
}
|
||
}
|
||
|
||
/* ===== 预设 2:minimal ===== */
|
||
.style-minimal {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
.rc-code-minimal {
|
||
display: flex !important;
|
||
flex-direction: row !important;
|
||
align-items: center !important;
|
||
justify-content: space-between !important;
|
||
flex-wrap: nowrap !important;
|
||
background: transparent;
|
||
border-style: dashed;
|
||
padding: 22rpx 26rpx;
|
||
gap: 16rpx;
|
||
|
||
.rc-code-minimal-text {
|
||
text-align: left;
|
||
letter-spacing: 4rpx;
|
||
line-height: 1;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.rc-copy-minimal {
|
||
font-size: 22rpx;
|
||
line-height: 1;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0 !important;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
.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: rgba(184,134,11,0.04);
|
||
padding: 28rpx 32rpx;
|
||
gap: 16rpx;
|
||
border-radius: 20rpx;
|
||
.rc-copy-card {
|
||
padding: 14rpx 30rpx;
|
||
font-weight: 700;
|
||
font-size: 24rpx;
|
||
cursor: pointer;
|
||
transition: opacity 0.2s ease, transform 0.15s ease;
|
||
&:active { opacity: 0.9; transform: scale(0.98); }
|
||
}
|
||
}
|
||
.rc-action-card { justify-content: flex-start; }
|
||
}
|
||
|
||
/* ===== 预设 4:bookgold 白底风格 ===== */
|
||
.style-bookgold {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
.rc-header-bookgold {
|
||
.rc-title {
|
||
color: #333333;
|
||
}
|
||
}
|
||
.rc-code-bookgold {
|
||
.rc-prefix {
|
||
color: rgba(92,61,30,0.7);
|
||
}
|
||
.rc-copy-bookgold {
|
||
background: #B8A08A;
|
||
color: #fff;
|
||
}
|
||
}
|
||
.rc-action-bookgold {
|
||
justify-content: flex-start;
|
||
.rc-btn-share-bookgold {
|
||
background: #B8A08A;
|
||
color: #fff;
|
||
}
|
||
}
|
||
.rc-link-bookgold {
|
||
.rc-link-text {
|
||
background: rgba(92,61,30,0.06);
|
||
color: #5C3D1E;
|
||
}
|
||
}
|
||
.rc-qr-bookgold {
|
||
.rc-qr-bookgold-img {
|
||
border: 3rpx solid #D4C4B0;
|
||
border-radius: 16rpx;
|
||
}
|
||
.rc-qr-tip-bookgold {
|
||
color: #5C3D1E;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|