feat:抖音支付

This commit is contained in:
2026-09-09 11:46:36 +08:00
parent 75060faa39
commit b350f1c07a
18 changed files with 2670 additions and 531 deletions
@@ -0,0 +1,597 @@
<template>
<view class="test_page">
<!-- 顶部导航 -->
<NavHeader leftTitle="抖音支付测试" leftPathType="navigateTo" />
<view class="test_container">
<!-- 状态与设备信息卡片 -->
<view class="card_box">
<view class="card_title">
<text class="title_text">📱 设备与环境信息</text>
<view class="tag_badge" :class="isApp ? 'badge_app' : 'badge_h5'">
{{ envText }}
</view>
</view>
<view class="info_grid">
<view class="info_row">
<text class="info_label">运行平台:</text>
<text class="info_val">{{ platformText }}</text>
</view>
<view class="info_row">
<text class="info_label">抖音安装状态:</text>
<text class="info_val" :style="{ color: isDyInstalled ? '#00B578' : '#FA5151' }">
{{ dyInstallStatusText }}
</text>
</view>
</view>
<view class="btn_row" style="margin-top: 20rpx;">
<button class="mini_btn check_btn" @click="checkDouyinInstalled">
🔍 检测是否安装抖音
</button>
<button class="mini_btn open_btn" @click="testDirectOpenDouyin">
🚀 直接打开抖音客户端
</button>
</view>
</view>
<!-- 支付测试参数配置 -->
<view class="card_box">
<view class="card_title">
<text class="title_text">⚙️ 支付测试参数</text>
</view>
<!-- 快捷预设模版 -->
<view class="preset_section">
<text class="section_label">快速填充测试数据:</text>
<view class="preset_btn_group">
<view class="preset_chip" @click="applyPreset('scheme')">普通App唤起 Scheme</view>
<view class="preset_chip" @click="applyPreset('cashier')">模拟收银台 Scheme</view>
<view class="preset_chip" @click="applyPreset('h5')">H5收银台链接</view>
</view>
</view>
<view class="tip_box">
<text class="tip_text">💡 提示:普通的 "snssdk1128://open" 仅能拉起抖音 App 首页。如需弹出【抖音收银台支付窗口】,orderStr 必须包含后端从字节/抖音开放平台获取的真实 pay_token 或收银台参数(如 snssdk1128://pay?pay_token=xxx 或 https://cashier.douyin.com/pay?token=xxx)。</text>
</view>
<view class="form_item">
<text class="form_label">支付类型 (type)</text>
<radio-group class="radio_group" @change="onTypeChange">
<label class="radio_item">
<radio value="app" :checked="payType === 'app'" color="#7934F6" style="transform: scale(0.8);" />
<text>app</text>
</label>
<label class="radio_item">
<radio value="h5" :checked="payType === 'h5'" color="#7934F6" style="transform: scale(0.8);" />
<text>h5</text>
</label>
</radio-group>
</view>
<view class="form_item">
<text class="form_label">支付串 / Scheme / URL (orderStr)</text>
<textarea
class="form_textarea"
v-model="orderStr"
placeholder="请输入或粘贴抖音支付链接 / Scheme(如 snssdk1128://... 或 https://...)"
></textarea>
</view>
<view class="form_row">
<view class="form_item half">
<text class="form_label">订单ID (orderId)</text>
<input class="form_input" type="number" v-model="orderId" placeholder="例如: 10001" />
</view>
<view class="form_item half">
<text class="form_label">订单编号 (orderNum)</text>
<input class="form_input" v-model="orderNum" placeholder="例如: ORD20260905001" />
</view>
</view>
</view>
<!-- 支付操作区域 -->
<view class="action_section">
<button class="main_pay_btn" :loading="loading" @click="handleTestPay">
⚡ 调用 douyinPayFun 测试支付
</button>
</view>
<!-- 运行日志与调试面板 -->
<view class="card_box">
<view class="card_title">
<text class="title_text">📋 运行日志与回调</text>
<text class="clear_link" @click="clearLogs">清空日志</text>
</view>
<scroll-view scroll-y="true" class="log_console">
<view v-if="logs.length === 0" class="empty_log">暂无测试日志,点击上方按钮开始测试</view>
<view v-for="(log, idx) in logs" :key="idx" class="log_item" :class="'log_' + log.level">
<text class="log_time">[{{ log.time }}]</text>
<text class="log_content">{{ log.msg }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import NavHeader from "@/components/header.vue";
import { douyinPayFun, jumpWayOk, jumpWayError } from "@/utils/payUtils.js";
export default {
components: {
NavHeader,
},
data() {
return {
payType: "app",
orderStr: "snssdk1128://open", // 默认测试 scheme
orderId: 99999,
orderNum: "TEST_ORDER_" + Date.now(),
loading: false,
isApp: false,
platformText: "未知",
envText: "H5 / 网页",
dyInstallStatusText: "待检测",
isDyInstalled: false,
logs: [],
};
},
onLoad() {
this.initPlatformInfo();
this.addLog("页面初始化完成", "info");
},
methods: {
initPlatformInfo() {
// #ifdef APP-PLUS
this.isApp = true;
this.envText = "App 端 (App-Plus)";
const platform = uni.getSystemInfoSync().platform;
this.platformText = platform === "ios" ? "iOS App" : "Android App";
this.checkDouyinInstalled();
// #endif
// #ifdef H5
this.isApp = false;
this.envText = "H5 浏览器";
this.platformText = "Web / H5";
this.dyInstallStatusText = "Web端不支持检测安装";
// #endif
},
addLog(msg, level = "info") {
const now = new Date();
const timeStr = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
const formattedMsg = typeof msg === "object" ? JSON.stringify(msg, null, 2) : String(msg);
this.logs.unshift({
time: timeStr,
msg: formattedMsg,
level: level,
});
},
clearLogs() {
this.logs = [];
},
onTypeChange(e) {
this.payType = e.detail.value;
this.addLog(`切换支付类型为: ${this.payType}`, "info");
},
applyPreset(presetType) {
if (presetType === "scheme") {
this.payType = "app";
this.orderStr = "snssdk1128://open";
this.addLog("已加载基础唤起 Scheme: snssdk1128://open (仅打开App首页)", "info");
} else if (presetType === "cashier") {
this.payType = "app";
this.orderStr = "snssdk1128://pay?pay_token=DEMO_PAY_TOKEN_123456&order_id=TEST_ORDER_001";
this.addLog("已加载模拟收银台 Scheme 结构 (真实支付需要后端接口返回的合法 pay_token)", "info");
} else if (presetType === "h5") {
this.payType = "h5";
this.orderStr = "https://cashier.douyin.com/pay?token=DEMO_PAY_TOKEN_123456";
this.addLog("已加载 H5 收银台链接预设", "info");
}
},
// 检测抖音是否已安装 (iOS & Android)
checkDouyinInstalled() {
// #ifdef APP-PLUS
const platform = uni.getSystemInfoSync().platform;
let installed = false;
let foundPkg = "";
try {
if (platform === "android") {
const androidPkgs = [
{ name: "抖音", pname: "com.ss.android.ugc.aweme" },
{ name: "抖音极速版", pname: "com.ss.android.ugc.aweme.lite" },
{ name: "抖音火山版", pname: "com.ss.android.ugc.live" },
];
for (const pkg of androidPkgs) {
if (plus.runtime.isApplicationExist({ pname: pkg.pname })) {
installed = true;
foundPkg = pkg.name;
break;
}
}
} else if (platform === "ios") {
const iosSchemes = [
{ name: "抖音", action: "snssdk1128://" },
{ name: "抖音(douyin)", action: "douyin://" },
{ name: "抖音极速版", action: "snssdk141://" },
];
for (const sch of iosSchemes) {
if (plus.runtime.isApplicationExist({ action: sch.action })) {
installed = true;
foundPkg = sch.name;
break;
}
}
}
this.isDyInstalled = installed;
this.dyInstallStatusText = installed ? `已检测到 (${foundPkg}) ✅` : "未检测到 ❌";
this.addLog(`检测抖音客户端安装状态: ${this.dyInstallStatusText}`, installed ? "success" : "warn");
if (!installed && platform === "ios") {
this.addLog("说明: iOS检测需要将 Scheme 加入 LSApplicationQueriesSchemes。若在【标准基座】中调试,因标准基座无此配置会返回未检测到,需使用【自定义基座】运行", "info");
}
} catch (e) {
this.dyInstallStatusText = "检测异常";
this.addLog(`检测安装状态发生异常: ${e.message}`, "error");
}
// #endif
// #ifndef APP-PLUS
this.addLog("当前环境为非 App 环境,无法直接调用 5+ isApplicationExist", "warn");
// #endif
},
// 直接测试打开抖音客户端
testDirectOpenDouyin() {
this.addLog(`尝试打开抖音: ${this.orderStr || 'snssdk1128://open'}`, "info");
const targetUrl = this.orderStr || "snssdk1128://open";
// #ifdef APP-PLUS
let opened = false;
if (plus.os.name === "iOS") {
try {
const UIApplication = plus.ios.importClass("UIApplication");
const NSURL = plus.ios.importClass("NSURL");
const app = UIApplication.sharedApplication();
const nsUrl = NSURL.URLWithString(targetUrl);
if (app && nsUrl) {
if (app.openURL(nsUrl)) {
opened = true;
this.addLog("iOS 原生 Native.js 成功拉起客户端", "success");
}
}
} catch (e) {
console.warn(e);
}
} else if (plus.os.name === "Android") {
try {
const Intent = plus.android.importClass("android.content.Intent");
const Uri = plus.android.importClass("android.net.Uri");
const main = plus.android.runtimeMainActivity();
const intent = new Intent(Intent.ACTION_VIEW, Uri.parse(targetUrl));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
main.startActivity(intent);
opened = true;
this.addLog("Android 原生 Native.js Intent 成功拉起客户端", "success");
} catch (e) {
console.warn(e);
}
}
if (!opened) {
plus.runtime.openURL(
targetUrl,
(err) => {
this.addLog(`打开抖音失败: ${JSON.stringify(err)}`, "error");
const isSim = plus.navigator && plus.navigator.isSimulator ? plus.navigator.isSimulator() : false;
if (isSim) {
this.addLog("提示: iOS模拟器未安装抖音客户端,无法响应 snssdk1128:// Scheme,请使用安装了抖音的真机测试", "warn");
} else if (err && err.code === -3) {
this.addLog("提示: 错误 code -3 说明设备上未检测到抖音应用或Scheme未添加白名单,请确认真机上已安装抖音", "warn");
}
uni.showToast({
title: isSim ? "模拟器不支持打开Scheme,请在真机测试" : "拉起失败,请确认是否已安装抖音",
icon: "none",
duration: 3000,
});
}
);
}
// #endif
// #ifdef H5
window.location.href = targetUrl;
this.addLog("已在 H5 环境尝试跳转: " + targetUrl, "info");
// #endif
},
// 调用 payUtils.js 中的 douyinPayFun
async handleTestPay() {
if (!this.orderStr) {
uni.showToast({
title: "请先输入支付串或链接",
icon: "none",
});
return;
}
const payParams = {
type: this.payType,
orderStr: this.orderStr,
};
this.loading = true;
this.addLog(`开始调用 douyinPayFun,入参: ${JSON.stringify(payParams)}`, "info");
try {
const result = await douyinPayFun(payParams, this.orderId, this.orderNum);
this.addLog(`douyinPayFun 调用完成返回: ${JSON.stringify(result)}`, "success");
} catch (err) {
this.addLog(`douyinPayFun 抛出错误: ${JSON.stringify(err)}`, "error");
} finally {
this.loading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.test_page {
min-height: 100vh;
background-color: #F6F7FB;
padding-bottom: 60rpx;
}
.test_container {
padding: 24rpx;
}
.card_box {
background: #FFFFFF;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
.card_title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
.title_text {
font-size: 30rpx;
font-weight: 600;
color: #1F2937;
}
.tag_badge {
font-size: 22rpx;
padding: 4rpx 14rpx;
border-radius: 20rpx;
font-weight: 500;
&.badge_app {
background-color: #EDE9FE;
color: #7934F6;
}
&.badge_h5 {
background-color: #E0F2FE;
color: #0284C7;
}
}
.clear_link {
font-size: 24rpx;
color: #7934F6;
}
}
}
.info_grid {
background: #F9FAFB;
border-radius: 12rpx;
padding: 16rpx 20rpx;
.info_row {
display: flex;
justify-content: space-between;
font-size: 26rpx;
margin-bottom: 8rpx;
&:last-child {
margin-bottom: 0;
}
.info_label {
color: #6B7280;
}
.info_val {
font-weight: 500;
color: #111827;
}
}
}
.btn_row {
display: flex;
gap: 16rpx;
.mini_btn {
flex: 1;
font-size: 24rpx;
border-radius: 12rpx;
line-height: 2.2;
margin: 0;
border: none;
&.check_btn {
background: #F3F4F6;
color: #374151;
}
&.open_btn {
background: #EDE9FE;
color: #7934F6;
}
}
}
.preset_section {
margin-bottom: 20rpx;
.section_label {
font-size: 24rpx;
color: #6B7280;
display: block;
margin-bottom: 12rpx;
}
.preset_btn_group {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.preset_chip {
font-size: 24rpx;
color: #7934F6;
background: #F4EFFF;
border: 1rpx solid #E4D4FF;
border-radius: 30rpx;
padding: 8rpx 22rpx;
}
}
}
.tip_box {
background: #FFFBEB;
border: 1rpx solid #FDE68A;
border-radius: 12rpx;
padding: 16rpx 20rpx;
margin-bottom: 24rpx;
.tip_text {
font-size: 24rpx;
color: #B45309;
line-height: 1.5;
}
}
.form_item {
margin-bottom: 20rpx;
.form_label {
font-size: 26rpx;
color: #374151;
font-weight: 500;
display: block;
margin-bottom: 10rpx;
}
.form_input {
background: #F9FAFB;
border: 1rpx solid #E5E7EB;
border-radius: 12rpx;
height: 72rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #111827;
}
.form_textarea {
width: 100%;
box-sizing: border-box;
background: #F9FAFB;
border: 1rpx solid #E5E7EB;
border-radius: 12rpx;
padding: 16rpx 20rpx;
font-size: 26rpx;
color: #111827;
height: 140rpx;
}
.radio_group {
display: flex;
gap: 30rpx;
.radio_item {
display: flex;
align-items: center;
font-size: 26rpx;
color: #374151;
}
}
&.half {
flex: 1;
margin-bottom: 0;
}
}
.form_row {
display: flex;
gap: 20rpx;
}
.action_section {
margin-bottom: 24rpx;
.main_pay_btn {
background: linear-gradient(135deg, #8B5CF6 0%, #7934F6 100%);
color: #FFFFFF;
font-size: 30rpx;
font-weight: 600;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
box-shadow: 0 8rpx 20rpx rgba(121, 52, 246, 0.3);
border: none;
&:active {
opacity: 0.9;
}
}
}
.log_console {
background: #1E1E2E;
border-radius: 16rpx;
padding: 20rpx;
height: 380rpx;
box-sizing: border-box;
.empty_log {
color: #6C7086;
font-size: 24rpx;
text-align: center;
padding: 60rpx 0;
}
.log_item {
font-size: 24rpx;
font-family: monospace;
line-height: 1.6;
margin-bottom: 10rpx;
word-break: break-all;
.log_time {
color: #A6ADC8;
margin-right: 12rpx;
}
&.log_info .log_content {
color: #CDD6F4;
}
&.log_success .log_content {
color: #A6E3A1;
}
&.log_warn .log_content {
color: #F9E2AF;
}
&.log_error .log_content {
color: #F38BA8;
}
}
}
</style>
@@ -1,354 +1,633 @@
<template>
<view class="invite_friends_warp">
<view class="invite_img">
<image :src="inviteImage" class="invite_imgage" v-if="inviteImage"></image>
<view class="loading" v-else>
<text>加载分享图片中...</text>
<!-- 1. 顶部导航 (固定) -->
<Header leftTitle="邀请好友" />
<!-- 2. 头部 Banner 区域 (固定) -->
<view class="banner_header">
<view class="banner_left">
<view class="title_row">
<text class="title_black">旧朋带新伴</text>
</view>
<view class="title_row">
<text class="title_black">邀约</text>
<text class="title_purple">享福利</text>
</view>
<view class="sub_title">
<text>累计邀请达标,可解锁更多券礼</text>
</view>
</view>
<view class="banner_right">
<up-image src="https://static.tbmall.xin/static/mine/invite_friends_img.png" width="130" height="115"
bgColor="#f1f6ff00"></up-image>
</view>
</view>
</view>
<view class="invite_but">
<button
class="share-btn"
@click="openSharePopup"
>
分享好友
</button>
</view>
<!-- 分享弹窗 -->
<SharePopup
ref="share"
v-if="shareOpen"
:show="shareShow"
@close="onClose"
@wxShare="onWxShare"
@share="onShare"
:id="posterId"
text="我的推广海报"
:tu="inviteImage || shareImg"
></SharePopup>
<!-- 3. 卡片容器 (填充中间区域,内部固定标题) -->
<view class="card_container">
<view class="card_title">我的邀请</view>
<up-toast ref="uToastRef"></up-toast>
<qiaobao-assistant page-key="pages/other_package/invite_friends/invite_friends" title="我的推广" />
<!-- 无数据状态 (图一) -->
<view class="empty_box" v-if="!loading && list.length === 0">
<up-image src="https://static.tbmall.xin/static/mine/invite_friends_empty.png" width="120" height="120"
bgColor="#f1f6ff00"></up-image>
<text class="empty_text">暂无邀请好友</text>
</view>
<!-- 有数据状态 (图二) -->
<view class="table_box" v-else-if="list.length > 0">
<!-- 固定表头 (用户ID | 手机号 | 注册时间) -->
<view class="table_header">
<view class="th th_border">用户ID</view>
<view class="th th_border">手机号</view>
<view class="th">注册时间</view>
</view>
<!-- 仅在此 scroll-view 区域内(表格数据行)可滚动 -->
<scroll-view scroll-y class="table_body_scroll" style="height: 100%;" @scrolltolower="onReachBottom"
refresher-enabled :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
<view class="table_body">
<view class="table_row" v-for="(item, index) in list" :key="index">
<view class="td td_border">{{ item.userId || '-' }}</view>
<view class="td td_border">{{ item.mobile || '-' }}</view>
<view class="td">{{ item.registTime || '-' }}</view>
</view>
<!-- 加载完成提示 -->
<view class="load_more_tip" v-if="finished && list.length > 0">
<text class="no_more">已加载全部</text>
</view>
</view>
</scroll-view>
</view>
<!-- 初次加载动画 -->
<view class="loading_box" v-if="loading && list.length === 0">
<up-loading-icon show color="#7934F6"></up-loading-icon>
</view>
</view>
<!-- 4. 底部固定分享按钮 -->
<view class="bottom_action">
<button class="share_btn" @click="openSharePopup">
分享好友
</button>
</view>
<!-- 分享弹窗组件 -->
<view v-if="shareOpen">
<SharePopup ref="share" :show="shareShow" @close="onClose" @wxShare="onWxShare" @share="onShare"
:id="posterId" text="我的推广海报" :tu="inviteImage || shareImg"></SharePopup>
</view>
<up-toast ref="uToastRef"></up-toast>
</view>
</template>
<script>
import { getSharePoster, generateSharePoster } from "@/api/common.js";
import SharePopup from "@/pages/other_package/components/share-popup.vue";
export default {
<script>
import Header from "@/components/header.vue";
import { getSharePoster, generateSharePoster } from "@/api/common.js";
import { getDirectReferrals } from "@/api/auth.js";
import SharePopup from "@/pages/other_package/components/share-popup.vue";
export default {
components: {
SharePopup
Header,
SharePopup
},
data() {
return {
inviteImage: null,
imageLoading: false,
shareImg: '',
shareShow: false,
shareOpen: true,
posterId: ''
};
return {
inviteImage: null,
imageLoading: false,
shareImg: '',
shareShow: false,
shareOpen: true,
posterId: '',
// 邀请列表相关数据
list: [],
page: 1,
pageSize: 10,
totalPage: 1,
loading: false,
refreshing: false,
finished: false
};
},
mounted() {
this.getSharePoster();
this.getSharePoster();
this.getReferralList(true);
},
onShareAppMessage() {
return {
title: "邀请您体验信任桥TB商城",
path: "/pages/home/home",
imageUrl: this.inviteImage || this.shareImg,
};
return {
title: "邀请您体验信任桥TB商城",
path: "/pages/home/home",
imageUrl: this.inviteImage || this.shareImg,
};
},
onShareTimeline() {
return {
title: "邀请您体验信任桥TB商城",
query: "",
imageUrl: this.inviteImage || this.shareImg,
};
return {
title: "邀请您体验信任桥TB商城",
query: "",
imageUrl: this.inviteImage || this.shareImg,
};
},
methods: {
// 打开分享弹窗 / 微信小程序下直接触发朋友圈(图片分享菜单)效果
openSharePopup() {
if (!this.inviteImage && !this.shareImg) {
this.$refs.uToastRef.show({
type: 'warning',
message: "分享图正在加载,请稍后重试",
});
return;
}
// #ifdef MP-WEIXIN
this.onWxShare(1);
return;
// #endif
this.shareShow = true;
},
// 关闭分享弹窗
onClose() {
this.shareShow = false;
},
// 微信小程序环境分享
onWxShare(index) {
console.log('小程序分享', index);
if (index === 0) {
// 微信好友: 已通过 button open-type="share" 触发,关闭弹窗即可
this.onClose();
} else if (index === 1) {
// 朋友圈: 唤起小程序原生分享菜单或系统提示
// #ifdef MP-WEIXIN
if (typeof wx !== 'undefined' && wx.showShareImageMenu && this.shareImg) {
wx.showShareImageMenu({
path: this.shareImg,
fail: () => {
uni.showShareMenu({
withShareTicket: true,
menus: ["shareTimeline"]
});
}
});
} else {
uni.showShareMenu({
withShareTicket: true,
menus: ["shareTimeline"]
});
}
// #endif
this.onClose();
}
},
// App 及 H5 环境分享
onShare(index) {
console.log('App/H5分享', index);
if (index === 0) {
// 微信好友
// #ifdef APP-PLUS
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 2,
imageUrl: this.shareImg || this.inviteImage,
success: () => {
this.$refs.uToastRef.show({ type: 'success', message: '分享成功' });
},
fail: (err) => {
console.error('App分享好友失败:', err);
this.savePosterToAlbum();
// 获取邀请好友列表
async getReferralList(isRefresh = false) {
if (this.loading) return;
if (isRefresh) {
this.page = 1;
this.finished = false;
}
});
// #endif
// #ifdef H5
uni.previewImage({ urls: [this.shareImg || this.inviteImage] });
this.$refs.uToastRef.show({ type: 'info', message: '请长按图片发送给好友' });
// #endif
this.onClose();
} else if (index === 1) {
// 微信朋友圈
// #ifdef APP-PLUS
uni.share({
provider: 'weixin',
scene: 'WXSceneTimeline',
type: 2,
imageUrl: this.shareImg || this.inviteImage,
success: () => {
this.$refs.uToastRef.show({ type: 'success', message: '分享成功' });
},
fail: (err) => {
console.error('App分享朋友圈失败:', err);
this.savePosterToAlbum();
}
});
// #endif
// #ifdef H5
uni.previewImage({ urls: [this.shareImg || this.inviteImage] });
this.$refs.uToastRef.show({ type: 'info', message: '请长按图片分享到朋友圈' });
// #endif
this.onClose();
} else if (index === 2) {
// 复制链接
uni.setClipboardData({
data: 'https://m.tbmall.xin/#/pages/home/home',
success: () => {
this.$refs.uToastRef.show({ type: 'success', message: '链接已复制到剪贴板' });
}
});
this.onClose();
} else if (index === 3) {
// 保存图片
this.savePosterToAlbum();
this.onClose();
} else if (index === 4) {
// QQ
this.$refs.uToastRef.show({ type: 'warning', message: '暂未开放' });
this.onClose();
}
},
// 保存海报至相册
savePosterToAlbum() {
const imgPath = this.shareImg || this.inviteImage;
if (!imgPath) {
this.$refs.uToastRef.show({ type: 'warning', message: '海报图片生成中,请稍后重试' });
return;
}
// #ifdef APP-PLUS || MP-WEIXIN
uni.saveImageToPhotosAlbum({
filePath: imgPath,
success: () => {
this.$refs.uToastRef.show({ type: 'success', message: '海报已保存至手机相册' });
},
fail: (err) => {
console.error('保存至相册失败:', err);
if (err && err.errMsg && err.errMsg.includes('auth')) {
uni.showModal({
title: '提示',
content: '需要保存图片到相册权限,请在设置中开启相册权限',
success: (res) => {
if (res.confirm) uni.openSetting();
this.loading = true;
try {
const params = {
page: this.page,
pageSize: this.pageSize
};
const resp = await getDirectReferrals(params);
if (resp && (resp.bizcode === 100 || resp.bizcode === 0)) {
const data = resp.data || {};
const newEntities = data.entitys || data.entities || [];
if (isRefresh) {
this.list = newEntities;
} else {
this.list = [...this.list, ...newEntities];
}
this.totalPage = data.totalPage || 1;
if (this.page >= this.totalPage || newEntities.length < this.pageSize) {
this.finished = true;
}
}
});
} else {
uni.previewImage({ urls: [imgPath] });
} catch (error) {
console.error("获取邀请列表失败:", error);
} finally {
this.loading = false;
this.refreshing = false;
}
}
});
// #endif
},
// #ifdef H5
uni.previewImage({ urls: [imgPath] });
this.$refs.uToastRef.show({ type: 'info', message: '请长按图片保存到手机相册' });
// #endif
},
// 下拉刷新
async onRefresh() {
this.refreshing = true;
await this.getReferralList(true);
},
// 将base64转换为本地临时文件
base64ToTempFile(base64Data) {
return new Promise((resolve, reject) => {
if (!base64Data) {
return reject(new Error('Base64数据为空'));
}
// 触底加载更多
onReachBottom() {
if (!this.finished && !this.loading) {
this.page++;
this.getReferralList(false);
}
},
// #ifdef APP-PLUS
try {
const bitmap = new plus.nativeObj.Bitmap('poster_' + Date.now());
bitmap.loadBase64Data(
base64Data,
() => {
const tempPath = `_doc/share_${Date.now()}.png`;
bitmap.save(
tempPath,
{ overwrite: true, format: 'png', quality: 100 },
(e) => {
bitmap.clear();
resolve(e.target);
},
(err) => {
bitmap.clear();
// 打开分享弹窗 / 微信小程序下直接触发朋友圈/分享
openSharePopup() {
if (!this.inviteImage && !this.shareImg) {
this.$refs.uToastRef.show({
type: 'warning',
message: "分享图正在加载,请稍后重试",
});
return;
}
// #ifdef MP-WEIXIN
this.onWxShare(1);
return;
// #endif
this.shareShow = true;
},
// 关闭分享弹窗
onClose() {
this.shareShow = false;
},
// 微信小程序环境分享
onWxShare(index) {
console.log('小程序分享', index);
if (index === 0) {
this.onClose();
} else if (index === 1) {
// #ifdef MP-WEIXIN
if (typeof wx !== 'undefined' && wx.showShareImageMenu && this.shareImg) {
wx.showShareImageMenu({
path: this.shareImg,
fail: () => {
uni.showShareMenu({
withShareTicket: true,
menus: ["shareTimeline"]
});
}
});
} else {
uni.showShareMenu({
withShareTicket: true,
menus: ["shareTimeline"]
});
}
// #endif
this.onClose();
}
},
// App 及 H5 环境分享
onShare(index) {
console.log('App/H5分享', index);
if (index === 0) {
// 微信好友
// #ifdef APP-PLUS
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 2,
imageUrl: this.shareImg || this.inviteImage,
success: () => {
this.$refs.uToastRef.show({ type: 'success', message: '分享成功' });
},
fail: (err) => {
console.error('App分享好友失败:', err);
this.savePosterToAlbum();
}
});
// #endif
// #ifdef H5
uni.previewImage({ urls: [this.shareImg || this.inviteImage] });
this.$refs.uToastRef.show({ type: 'info', message: '请长按图片发送给好友' });
// #endif
this.onClose();
} else if (index === 1) {
// 微信朋友圈
// #ifdef APP-PLUS
uni.share({
provider: 'weixin',
scene: 'WXSceneTimeline',
type: 2,
imageUrl: this.shareImg || this.inviteImage,
success: () => {
this.$refs.uToastRef.show({ type: 'success', message: '分享成功' });
},
fail: (err) => {
console.error('App分享朋友圈失败:', err);
this.savePosterToAlbum();
}
});
// #endif
// #ifdef H5
uni.previewImage({ urls: [this.shareImg || this.inviteImage] });
this.$refs.uToastRef.show({ type: 'info', message: '请长按图片分享到朋友圈' });
// #endif
this.onClose();
} else if (index === 2) {
// 复制链接
uni.setClipboardData({
data: 'https://m.tbmall.xin/#/pages/home/home',
success: () => {
this.$refs.uToastRef.show({ type: 'success', message: '链接已复制到剪贴板' });
}
});
this.onClose();
} else if (index === 3) {
// 保存图片
this.savePosterToAlbum();
this.onClose();
} else if (index === 4) {
// QQ
this.$refs.uToastRef.show({ type: 'warning', message: '暂未开放' });
this.onClose();
}
},
// 保存海报至相册
savePosterToAlbum() {
const imgPath = this.shareImg || this.inviteImage;
if (!imgPath) {
this.$refs.uToastRef.show({ type: 'warning', message: '海报图片生成中,请稍后重试' });
return;
}
// #ifdef APP-PLUS || MP-WEIXIN
uni.saveImageToPhotosAlbum({
filePath: imgPath,
success: () => {
this.$refs.uToastRef.show({ type: 'success', message: '海报已保存至手机相册' });
},
fail: (err) => {
console.error('保存至相册失败:', err);
if (err && err.errMsg && err.errMsg.includes('auth')) {
uni.showModal({
title: '提示',
content: '需要保存图片到相册权限,请在设置中开启相册权限',
success: (res) => {
if (res.confirm) uni.openSetting();
}
});
} else {
uni.previewImage({ urls: [imgPath] });
}
}
});
// #endif
// #ifdef H5
uni.previewImage({ urls: [imgPath] });
this.$refs.uToastRef.show({ type: 'info', message: '请长按图片保存到手机相册' });
// #endif
},
// 将base64转换为本地临时文件
base64ToTempFile(base64Data) {
return new Promise((resolve, reject) => {
if (!base64Data) {
return reject(new Error('Base64数据为空'));
}
// #ifdef APP-PLUS
try {
const bitmap = new plus.nativeObj.Bitmap('poster_' + Date.now());
bitmap.loadBase64Data(
base64Data,
() => {
const tempPath = `_doc/share_${Date.now()}.png`;
bitmap.save(
tempPath,
{ overwrite: true, format: 'png', quality: 100 },
(e) => {
bitmap.clear();
resolve(e.target);
},
(err) => {
bitmap.clear();
reject(err);
}
);
},
(err) => {
bitmap.clear();
reject(err);
}
);
} catch (err) {
reject(err);
}
);
},
(err) => {
bitmap.clear();
reject(err);
}
);
} catch (err) {
reject(err);
}
// #endif
}
// #endif
// #ifdef MP-WEIXIN
try {
const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '');
const arrayBuffer = uni.base64ToArrayBuffer(base64);
const fs = typeof wx !== 'undefined' && wx.getFileSystemManager ? wx.getFileSystemManager() : uni.getFileSystemManager();
const tempFilePath = `${wx.env.USER_DATA_PATH}/share_${Date.now()}.png`;
fs.writeFileSync(tempFilePath, arrayBuffer, 'binary');
resolve(tempFilePath);
} catch (err) {
reject(err);
}
// #endif
// #ifdef MP-WEIXIN
try {
const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '');
const arrayBuffer = uni.base64ToArrayBuffer(base64);
const fs = typeof wx !== 'undefined' && wx.getFileSystemManager ? wx.getFileSystemManager() : uni.getFileSystemManager();
const tempFilePath = `${wx.env.USER_DATA_PATH}/share_${Date.now()}.png`;
fs.writeFileSync(tempFilePath, arrayBuffer, 'binary');
resolve(tempFilePath);
} catch (err) {
reject(err);
}
// #endif
// #ifdef H5
resolve(base64Data);
// #endif
});
},
// #ifdef H5
resolve(base64Data);
// #endif
});
},
// 查询分享模版
async getSharePoster() {
this.imageLoading = true;
try {
const resp = await getSharePoster();
if (resp && resp.bizcode === 100) {
const data = resp.data[0];
if (data) this.posterId = data.id || '';
// 生成分享图
const generateResp = await generateSharePoster({ id: data.id });
if (generateResp && generateResp.bizcode === 100) {
this.inviteImage = generateResp.data;
// 将base64转换为本地临时文件并存储到shareImg
this.shareImg = await this.base64ToTempFile(generateResp.data);
console.log('图片转换成功:', this.shareImg);
// 查询分享模版
async getSharePoster() {
this.imageLoading = true;
try {
const resp = await getSharePoster();
if (resp && resp.bizcode === 100) {
const data = resp.data[0];
if (data) this.posterId = data.id || '';
const generateResp = await generateSharePoster({ id: data.id });
if (generateResp && generateResp.bizcode === 100) {
this.inviteImage = generateResp.data;
this.shareImg = await this.base64ToTempFile(generateResp.data);
console.log('图片转换成功:', this.shareImg);
}
}
} catch (error) {
console.error("获取分享图失败:", error);
} finally {
this.imageLoading = false;
}
}
} catch (error) {
console.error("获取分享图失败:", error);
} finally {
this.imageLoading = false;
}
}
}
};
</script>
<style lang="scss" scoped>
.invite_friends_warp {
height: calc(100vh - 80rpx);
padding: 10rpx 40rpx 40rpx;
}
.invite_img {
};
</script>
<style lang="scss" scoped>
.invite_friends_warp {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #F3ECFF 0%, #F6F3FE 300rpx, #F5F2FC 600rpx, #F5F2FC 100%);
overflow: hidden;
box-sizing: border-box;
}
/* 头部 Banner 区域 (固定不滑动) */
.banner_header {
flex-shrink: 0;
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 20rpx 20rpx 10rpx 40rpx;
position: relative;
.banner_left {
flex: 1;
padding-top: 16rpx;
.title_row {
display: flex;
align-items: flex-start;
font-size: 40rpx;
font-weight: 800;
line-height: 1.25;
color: #1D1D1F;
.title_black {
color: #1D1D1F;
}
.title_purple {
color: #8C48FF;
margin-left: 6rpx;
}
}
.sub_title {
font-size: 22rpx;
color: #949497;
margin-top: 16rpx;
letter-spacing: 0.5rpx;
}
}
.banner_right {
width: 260rpx;
height: 230rpx;
display: flex;
justify-content: flex-end;
align-items: flex-start;
}
}
/* 白框卡片:填充中间区域 */
.card_container {
flex: 1;
height: 0;
min-height: 0;
display: flex;
flex-direction: column;
background: #FFFFFF;
border-radius: 24rpx;
margin: 0 24rpx 20rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(121, 52, 246, 0.03);
box-sizing: border-box;
overflow: hidden;
.card_title {
flex-shrink: 0;
font-size: 32rpx;
font-weight: bold;
color: #1D1D1F;
margin-bottom: 28rpx;
}
}
/* 无数据空状态 */
.empty_box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.empty_text {
font-size: 26rpx;
color: #8A8A8E;
margin-top: 24rpx;
}
}
/* 初次加载动画居中 */
.loading_box {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-height: 920rpx;
}
.invite_imgage {
width: 100%;
height: 920rpx;
}
.loading {
text-align: center;
color: #999;
font-size: 28rpx;
}
.invite_but {
padding: 20rpx 50rpx;
margin-top: 52rpx;
}
.share-btn {
width: 100%;
background: $app-bt-bj;
border-radius: 45rpx;
color: #FFFFFF;
font-weight: 500;
font-size: 32rpx;
// 清除默认按钮样式
border: none;
outline: none;
&:active {
opacity: 0.8;
}
/* 有数据表格状态 */
.table_box {
flex: 1;
height: 0;
min-height: 0;
display: flex;
flex-direction: column;
border: 1rpx solid #F0EBF8;
border-radius: 16rpx;
overflow: hidden;
/* 固定表头 */
.table_header {
flex-shrink: 0;
display: flex;
background-color: #FAF7FF;
border-bottom: 1rpx solid #F0EBF8;
.th {
flex: 1;
text-align: center;
padding: 22rpx 8rpx;
font-size: 26rpx;
font-weight: 500;
color: #666666;
&.th_border {
border-right: 1rpx solid #F0EBF8;
}
}
}
}
</style>
/* 仅此处表格列表可垂直滑动,支持 App / 微信小程序 / H5 三端兼容 */
.table_body_scroll {
flex: 1;
height: 0;
min-height: 0;
box-sizing: border-box;
/* #ifdef H5 || APP-PLUS */
::v-deep uni-scroll-view,
::v-deep .uni-scroll-view {
height: 100% !important;
}
/* #endif */
.table_body {
.table_row {
display: flex;
border-bottom: 1rpx solid #F0EBF8;
&:last-child {
border-bottom: none;
}
.td {
flex: 1;
text-align: center;
padding: 24rpx 8rpx;
font-size: 26rpx;
color: #333333;
display: flex;
align-items: center;
justify-content: center;
word-break: break-all;
&.td_border {
border-right: 1rpx solid #F0EBF8;
}
}
}
}
}
}
.load_more_tip {
text-align: center;
padding: 24rpx 0;
font-size: 22rpx;
color: #B0B0B0;
}
/* 底部固定分享按钮 */
.bottom_action {
flex-shrink: 0;
background: #FFFFFF;
padding: 20rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
box-sizing: border-box;
.share_btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #9553FF 0%, #7934F6 100%);
border-radius: 44rpx;
color: #FFFFFF;
font-weight: bold;
font-size: 32rpx;
border: none;
outline: none;
text-align: center;
&:active {
opacity: 0.9;
}
}
}
</style>