Files
frontend-app/pages/other_package/douyin_pay_test/douyin_pay_test.vue
T
2026-09-09 11:46:36 +08:00

598 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>