598 lines
15 KiB
Vue
598 lines
15 KiB
Vue
<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>
|