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
+13
View File
@@ -175,3 +175,16 @@ export function appleLogin(params) {
cha:1
});
}
/**
* 获取直推邀请用户列表
* @param {Object} params { page, pageSize }
*/
export function getDirectReferrals(params) {
return request({
url: "/user/getDirectReferrals",
method: "get",
data: params,
});
}
+13
View File
@@ -60,3 +60,16 @@ export function getApplicableGoods(data) {
data,
});
}
/**
* 查询首页弹窗优惠券通知列表
* @param {Object} data { page, pageSize }
*/
export function getPopNotifyList(data) {
return request({
url: "/coupon/getPopNotifyList",
method: "get",
data,
});
}
+269
View File
@@ -0,0 +1,269 @@
<template>
<up-popup :show="guideShow" mode="bottom" :round="20" :closeOnClickOverlay="false" @close="closeGuide">
<view class="guide_popup_container">
<!-- 头部 -->
<view class="guide_popup_header">
<text class="guide_header_title">新手指引</text>
<view class="guide_header_close" @click="closeGuide">
<u-icon name="close" size="20" color="#333333"></u-icon>
</view>
</view>
<!-- 内容主体 -->
<scroll-view scroll-y class="guide_popup_body" :scroll-top="scrollTop" @scroll="onScroll">
<!-- 图二:第1步 -->
<view v-if="guideStep === 0" class="guide_step_content">
<view class="guide_title">信任桥商城是一个怎样的平台</view>
<view class="guide_desc">
信任桥TB商城是一个采用 F2C(工厂直链消费者)模式的大消费电商平台,以严选可追溯的供应链机制保障【低价好物】的品质底线,通过削减中间渠道实现低价,并通过独创的权益共享机制,实现【消费即贡献、贡献即共享】的全民普惠。
</view>
<view class="guide_section_title">业务逻辑</view>
<image src="https://static.tbmall.xin/static/home/guide_1.png" mode="widthFix" class="guide_img"></image>
</view>
<!-- 图三:第2步 -->
<view v-if="guideStep === 1" class="guide_step_content">
<view class="guide_title">什么是消费贡献值</view>
<view class="guide_desc">
消费贡献值是平台根据用户的真实消费自动记录生成的数值,是用户参与平台权益共享机制的确权依据。
</view>
<view class="guide_title" style="margin-top: 32rpx;">怎么获得贡献值</view>
<view class="guide_desc">
只需在平台按公允价格正常购买商品,<text class="guide_highlight">完成真实消费</text>后,系统会自动记录并生成对应消费贡献值,无需额外操作、无需额外缴费,也不用购买指定高价商品。
</view>
<image src="https://static.tbmall.xin/static/home/guide_2.png" mode="widthFix" class="guide_img"></image>
<view class="guide_title" style="margin-top: 32rpx;">贡献值有什么用</view>
<view class="guide_list">
<view class="guide_list_item">1. 参与月度权益分配:平台会按你的贡献值占比核算对应权益,每月分配到账。</view>
<view class="guide_list_item">2. 长期累计持续生效:贡献值按规则长期有效、可持续累计,消费越久、贡献越多,享受平台经营成果分享的基础越厚实。</view>
<view class="guide_list_item">3. 明细随时可核对:你可随时查询自己的贡献值明细、对应权益占比与历次分配记录,每一笔消费与权益都有据可查。</view>
<view class="guide_list_item">4.消费同时获赠平台积分,可全额兑换或抵扣兑换平台各类优质好物,规则公开透明,与消费贡献值一样长期有效、持续累计。</view>
</view>
</view>
<!-- 图四:第3步 -->
<view v-if="guideStep === 2" class="guide_step_content">
<view class="guide_title">哪里查看贡献值及权益</view>
<view class="guide_desc">
登录信任桥商城平台后,在【我的-钱包】即可随时查看本人的消费贡献值明细,包含累计贡献值、各笔消费对应的贡献记录,同时可同步查看对应的消费共享占比、历次权益分配记录。
</view>
<view class="guide_title" style="margin-top: 32rpx;">贡献值</view>
<view class="guide_desc">
展示累积贡献值,点击可查看贡献值获得明细。是用户参与平台权益共享机制的确权依据。
</view>
<image src="https://static.tbmall.xin/static/home/guide_3.png" mode="widthFix" class="guide_img"></image>
<view class="guide_title" style="margin-top: 32rpx;">数字积分</view>
<view class="guide_desc">
展示累积贡献值,点击可查看贡献值获得明细。是用户参与平台权益共享机制的确权依据。使用数字积分进行免费商品兑换
</view>
<image src="https://static.tbmall.xin/static/home/guide_4.png" mode="widthFix" class="guide_img"></image>
</view>
<!-- 五:第4步 -->
<view v-if="guideStep === 3" class="guide_step_content">
<view class="guide_title">消费者服务保障</view>
<view class="guide_desc">
在「低价好物 + 权益共享」的商业模式之外,信任桥TB商城将消费者服务保障视为平台长期运营的基础工程。平台按照制度化、公开化、可追溯的原则,建立覆盖售前、售中、售后全链路的消费者服务体系,让每一位用户在享受 F2C 低价与权益共享的同时,获得规范、便捷、可预期的购物保障
</view>
<image src="https://static.tbmall.xin/static/home/guide_5.png" mode="widthFix" class="guide_img"></image>
<image src="https://static.tbmall.xin/static/home/guide_6.png" mode="widthFix" class="guide_img" style="margin-top: 16rpx;"></image>
</view>
</scroll-view>
<!-- 底部按钮操作区 -->
<view class="guide_popup_footer">
<view class="guide_btn_skip" @click="closeGuide">跳过</view>
<view class="guide_btn_next" @click="nextGuideStep">
{{ guideStep === 3 ? '开启旅程' : '下一步' }}
</view>
</view>
</view>
</up-popup>
</template>
<script>
export default {
name: "GuidePopup",
options: {
styleIsolation: "shared",
},
data() {
return {
guideShow: false,
guideStep: 0,
scrollTop: 0,
};
},
mounted() {
this.checkGuideShow();
},
methods: {
onScroll(e) {
this.scrollTop = e.detail.scrollTop;
},
resetScroll() {
this.scrollTop = 0;
this.$nextTick(() => {
this.scrollTop = 0;
});
},
checkGuideShow() {
const hasSeen = uni.getStorageSync("has_seen_home_guide");
if (!hasSeen) {
this.guideStep = 0;
this.guideShow = true;
this.resetScroll();
}
},
closeGuide() {
this.guideShow = false;
uni.setStorageSync("has_seen_home_guide", true);
this.$emit("close");
},
nextGuideStep() {
if (this.guideStep < 3) {
this.guideStep++;
this.resetScroll();
} else {
this.closeGuide();
}
},
open() {
this.guideStep = 0;
this.guideShow = true;
this.resetScroll();
},
},
};
</script>
<style lang="scss" scoped>
.guide_popup_container {
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
max-height: 80vh;
box-sizing: border-box;
.guide_popup_header {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 32rpx 32rpx 20rpx;
.guide_header_title {
font-weight: 500;
font-size: 32rpx;
color: #71737c;
}
.guide_header_close {
position: absolute;
right: 32rpx;
top: 28rpx;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.guide_popup_body {
height: 62vh;
padding: 10rpx 36rpx 30rpx;
box-sizing: border-box;
.guide_step_content {
display: flex;
flex-direction: column;
}
.guide_title {
font-size: 34rpx;
font-weight: bold;
color: #111111;
margin-bottom: 20rpx;
line-height: 1.4;
}
.guide_section_title {
font-size: 32rpx;
font-weight: bold;
color: #111111;
margin-top: 24rpx;
margin-bottom: 20rpx;
}
.guide_desc {
font-size: 26rpx;
line-height: 1.75;
color: #666666;
margin-bottom: 24rpx;
}
.guide_highlight {
color: #7934f6;
font-weight: 500;
}
.guide_list {
margin-bottom: 24rpx;
.guide_list_item {
font-size: 26rpx;
line-height: 1.75;
color: #666666;
margin-bottom: 16rpx;
}
}
.guide_img {
width: 100%;
display: block;
margin: 10rpx 0 24rpx;
border-radius: 12rpx;
}
}
.guide_popup_footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 24rpx;
padding: 24rpx 36rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
background: #ffffff;
border-top: 1rpx solid #f5f5f5;
.guide_btn_skip {
width: 220rpx;
height: 88rpx;
background: #f5f5f5;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #999999;
font-weight: 500;
}
.guide_btn_next {
flex: 1;
height: 88rpx;
background: linear-gradient(270deg, #b164fb 0%, #7934f6 100%);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>
+21 -1
View File
@@ -72,6 +72,16 @@
"com.apple.developer.associated-domains" : [ "applinks:app.tbmall.xin" ]
}
},
"urlschemewhitelist" : [
"snssdk1128",
"douyin",
"douyinopensdk",
"douyinsharesdk",
"alipay",
"alipays",
"weixin",
"wechat"
],
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "App需要您的同意才能访问相册,以便您选择本地图片上传头像或发布商品评价。例如:当您修改个人头像或发布带图评价时,需要从相册选择图片。",
"NSPhotoLibraryAddUsageDescription" : "App需要您的同意才能添加图片到相册,以便您保存商品海报或图片到本地。例如:当您点击保存商品分享海报时,需要写入相册权限。",
@@ -141,7 +151,17 @@
"useOriginalMsgbox" : true,
"iosStyle" : "common",
"androidStyle" : "common"
}
},
"urlschemewhitelist" : [
"snssdk1128",
"douyinopensdk",
"douyinsharesdk",
"snssdk141",
"weixin",
"weixinULAPI",
"alipay",
"alipays"
]
},
/*忽略版本提示*/
"compatible" : {
+9 -3
View File
@@ -659,11 +659,11 @@
{
"path": "invite_friends/invite_friends",
"style": {
"navigationBarTitleText": "%invite.friends.title%",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTitleText": "邀请好友",
"navigationBarBackgroundColor": "#F3ECFF",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": false,
"navigationStyle": "default" // 默认样式,会显示返回图标
"navigationStyle": "custom"
}
},
{
@@ -675,6 +675,12 @@
"enablePullDownRefresh": false,
"navigationStyle": "default" // 默认样式,会显示返回图标
}
},
{
"path": "douyin_pay_test/douyin_pay_test",
"style": {
"navigationStyle": "custom"
}
}
]
},
+320
View File
@@ -113,6 +113,8 @@
</view>
</view>
<view @click="aaa">亲星星</view>
<view class="home_actived_warp">
<view class="home_actived_item_warp" v-for="item in homeActivedList" :key="item.id">
<view class="home_actived_item">
@@ -215,7 +217,66 @@
</view>
</view>
</up-overlay>
<!-- 专属优惠券弹窗 -->
<up-overlay :show="showCouponPop" @click="showCouponPop = false" zIndex="10090">
<view class="coupon_pop_wrapper">
<view class="coupon_pop_card" @tap.stop>
<!-- 头部标题与关闭按钮 -->
<view class="coupon_pop_header">
<view class="coupon_pop_title">专属优惠券已到账</view>
<view class="coupon_pop_close" @click="showCouponPop = false">
<text class="close_x">✕</text>
</view>
</view>
<!-- 优惠券列表 -->
<scroll-view class="coupon_pop_list" scroll-y>
<view class="coupon_item" v-for="(item, index) in popCouponList"
:key="item.couponUserId || item.templateId || index">
<!-- 左侧金额与门槛 -->
<view class="coupon_left">
<view class="price_wrap">
<text class="currency">¥</text>
<text class="amount">{{ item.discountAmount }}</text>
</view>
<view class="threshold_tag">{{ formatThresholdText(item) }}</view>
</view>
<!-- 垂直虚线分割线 -->
<view class="coupon_divider">
<view class="notch notch_top"></view>
<view class="dashed_line"></view>
<view class="notch notch_bottom"></view>
</view>
<!-- 右侧优惠券信息 -->
<view class="coupon_right">
<view class="coupon_name">{{ item.title || '限时秒杀优惠券' }}</view>
<view class="coupon_scope_box">
<text class="scope_badge">{{ formatScopeBadge(item) }}</text>
<text class="scope_text">{{ item.scopeText || (item.scopeType === 1 ? '所有商品可用' : '部分商品可用') }}</text>
</view>
<view class="coupon_validity">{{ formatValidityText(item) }}</view>
</view>
</view>
</scroll-view>
<!-- 立即收下按钮 -->
<view class="coupon_pop_btn" @click="showCouponPop = false">
立即收下
</view>
<!-- 底部说明文案 -->
<view class="coupon_pop_tip">
优惠券详情可在【我的-优惠券】中查看
</view>
</view>
</view>
</up-overlay>
<HomePopup :show="homeShow" :dataObj="curData" @jumpShop="onJumpShop" @close="homeShow = false"></HomePopup>
<GuidePopup />
<qiaobao-assistant page-key="pages/home/home" title="商城首页" />
</view>
</template>
@@ -238,6 +299,7 @@ import useTabber from "tabber-component";
import { getNewsList } from "@/api/news.js";
import { comparisonVersionHandler } from "@/utils/index.js";
import HomePopup from "@/components/common/home-popup.vue";
import GuidePopup from "@/components/common/guide-popup.vue";
import productSeckillCard from "@/components/common/product-seckill-card.vue";
import { SHARE_URL } from "@/utils/config.js";
import { getUnreadCount as getKefuUnreadCount } from "@/uni_modules/hashmall-customer-service/js_sdk/api.js";
@@ -246,12 +308,14 @@ import {
startKefuRealtime,
} from "@/uni_modules/hashmall-customer-service/js_sdk/kefu-realtime.js";
import { getStorageFun, TOKEN_NAME, USER_DATA } from "@/utils/auth.js";
import { getPopNotifyList } from "@/api/coupon.js";
export default {
components: {
choiceness,
CustomTabBar,
HomePopup,
GuidePopup,
productSeckillCard,
},
data() {
@@ -304,6 +368,8 @@ export default {
kefuUnreadRefreshTimer: null,
isScrolled: false,
scrollTop: 0,
showCouponPop: false,
popCouponList: [],
};
},
onPageScroll(e) {
@@ -337,6 +403,7 @@ export default {
this.querySearchList(SEARCH_TYPE.EXPLOSIVE); // 爆单专区
this.onNews(); // 获取公告
this.getCarouselImage();
this.fetchPopNotifyList(); // 获取专属优惠券弹窗通知
// 秒杀
@@ -394,6 +461,11 @@ export default {
// #endif
},
methods: {
aaa() {
uni.navigateTo({
url: '/pages/other_package/douyin_pay_test/douyin_pay_test'
});
},
getScrollTop(e) {
let top = 0;
if (typeof e === "number") {
@@ -885,6 +957,47 @@ export default {
url: "/pages/other_package/productInfo/productInfo?id=" + item.id,
});
},
/**
* 查询专属优惠券弹窗通知
*/
fetchPopNotifyList() {
if (!getStorageFun(TOKEN_NAME)) {
return;
}
getPopNotifyList({ page: 1, pageSize: 100 })
.then((res) => {
if (res && res.bizcode === 100) {
const list = res.data?.entitys || res.data || [];
if (Array.isArray(list) && list.length > 0) {
this.popCouponList = list;
this.showCouponPop = true;
}
}
})
.catch((err) => {
console.error("获取首页优惠券弹窗失败", err);
});
},
formatThresholdText(item) {
if (!item || !item.thresholdAmount || Number(item.thresholdAmount) === 0) {
return "无门槛";
}
return "满减券";
},
formatScopeBadge(item) {
if (item && item.scopeType === 1) {
return "通用券";
}
return "满减券";
},
formatValidityText(item) {
if (item && item.startTime && item.endTime) {
const start = dayjs(item.startTime).format("YYYY.MM.DD");
const end = dayjs(item.endTime).format("YYYY.MM.DD");
return `${start}-${end}`;
}
return "领取之日起30天有效";
},
},
computed: {
isBlockedDate() {
@@ -1657,4 +1770,211 @@ export default {
bottom: 60rpx;
}
}
/* 专属优惠券弹窗 */
.coupon_pop_wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.coupon_pop_card {
width: 620rpx;
background: #ffffff;
border-radius: 32rpx;
padding: 36rpx 32rpx 36rpx;
box-sizing: border-box;
position: relative;
.coupon_pop_header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-bottom: 28rpx;
.coupon_pop_title {
font-size: 34rpx;
font-weight: bold;
color: #333333;
text-align: center;
}
.coupon_pop_close {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 44rpx;
height: 44rpx;
// border: 2rpx solid #dcdcdc;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
.close_x {
color: #999999;
font-size: 24rpx;
line-height: 1;
}
}
}
.coupon_pop_list {
max-height: 520rpx;
margin-bottom: 24rpx;
}
.coupon_item {
display: flex;
align-items: center;
background: #fff5f5;
border-radius: 20rpx;
height: 160rpx;
margin-bottom: 20rpx;
position: relative;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
}
.coupon_left {
width: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.price_wrap {
display: flex;
align-items: baseline;
color: #ff1e27;
.currency {
font-size: 28rpx;
font-weight: bold;
margin-right: 2rpx;
}
.amount {
font-size: 56rpx;
font-weight: bold;
line-height: 1;
}
}
.threshold_tag {
font-size: 24rpx;
color: #ff1e27;
font-weight: bold;
margin-top: 8rpx;
}
}
.coupon_divider {
width: 1rpx;
height: 100%;
position: relative;
.dashed_line {
position: absolute;
top: 16rpx;
bottom: 16rpx;
width: 0;
border-left: 2rpx dashed #ffcdcd;
}
.notch {
width: 20rpx;
height: 20rpx;
background-color: #ffffff;
border-radius: 50%;
position: absolute;
left: 50%;
transform: translateX(-50%);
&.notch_top {
top: -10rpx;
}
&.notch_bottom {
bottom: -10rpx;
}
}
}
.coupon_right {
flex: 1;
padding-left: 24rpx;
padding-right: 16rpx;
display: flex;
flex-direction: column;
justify-content: center;
.coupon_name {
font-size: 30rpx;
font-weight: bold;
color: #222222;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.coupon_scope_box {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.scope_badge {
font-size: 20rpx;
color: #ff2437;
border: 1rpx solid #ff8d96;
background: #fff0f0;
border-radius: 6rpx;
padding: 2rpx 10rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
.scope_text {
font-size: 22rpx;
color: #666666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.coupon_validity {
font-size: 22rpx;
color: #999999;
}
}
.coupon_pop_btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #ff1e27;
border-radius: 44rpx;
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-top: 10rpx;
}
.coupon_pop_tip {
font-size: 24rpx;
color: #999999;
text-align: center;
margin-top: 24rpx;
}
}
</style>
+10 -7
View File
@@ -1,11 +1,12 @@
<template>
<view class="news_warp">
<Header leftTitle="商学院" left-path-type="switchTab" left-path="/pages/mine/mine"
<Header leftTitle="资讯" left-path-type="switchTab" left-path="/pages/mine/mine"
style="background-color: #FFFFFF;" />
<view class="news_content_warp">
<view class="heder_">
<view v-for="item in titleList" :key="item.key"
:class="form.type === item.key ? 'heder_item_sel heder_item' : 'heder_item'" @click="headerClick(item)">
:class="form.type === item.key ? 'heder_item_sel heder_item' : 'heder_item'"
@click="headerClick(item)">
{{ item.value }}
</view>
</view>
@@ -16,7 +17,7 @@
<view class="indicator-num">
<text class="indicator-num__text">{{ currentNum + 1 }}/{{ swiperList.length }}</text>
<qiaobao-assistant page-key="pages/login_package/news/news" title="商城公告" />
</view>
</view>
</template>
</up-swiper>
</view>
@@ -32,8 +33,8 @@
</template>
</up-input>
<view class="news_content_item" v-for="item in newsList" :key="item.id" v-if="newsList && newsList.length !== 0"
@click="jumpInfo(item)">
<view class="news_content_item" v-for="item in newsList" :key="item.id"
v-if="newsList && newsList.length !== 0" @click="jumpInfo(item)">
<view class="news_content_item_warp">
<view class="news_content_item_title">{{ item.title }}</view>
<view v-html="item.subTitle" class="news_content_item_content"></view>
@@ -41,14 +42,16 @@
<view>{{ formatDate(item.ctdate) }}</view>
<view class="browses_">
<view>
<up-image src="/pages/login_package/static/login/show.png" width="20" height="20" bgColor="#f1f6ff00"></up-image>
<up-image src="/pages/login_package/static/login/show.png" width="20" height="20"
bgColor="#f1f6ff00"></up-image>
</view>
<view style="margin-left: 8rpx;">{{ item.browses }}</view>
</view>
</view>
</view>
<view>
<up-image :src="item.coverImage" width="100" height="100" bgColor="#f1f6ff00" radius="8"></up-image>
<up-image :src="item.coverImage" width="100" height="100" bgColor="#f1f6ff00"
radius="8"></up-image>
</view>
</view>
<view v-else class="not_order_warp">
+1 -1
View File
@@ -381,7 +381,7 @@ export default {
// },
{
id: 8,
title: '商学院资讯',
title: '资讯',
url: 'https://static.tbmall.xin/static/mine/tool_sxy.png',
path: '/pages/login_package/news/news'
},
@@ -1,32 +1,18 @@
<template>
<view class="address_add_warp">
<Header
:leftTitle="titleText"
leftPath="1"
/>
<Header :leftTitle="titleText" leftPath="1" />
<view class="address_form">
<view class="add_form_item add_form_item_but">
<view class="add_form_title">收货人</view>
<up-input
placeholder="收货人"
border="surround"
v-model="form.name"
:customStyle="{
<up-input placeholder="收货人" border="surround" v-model="form.name" :customStyle="{
border: '0rpx',
}"
></up-input>
}"></up-input>
</view>
<view class="add_form_item add_form_item_but">
<view class="add_form_title">手机号码</view>
<up-input
placeholder="请填写收货人手机号"
border="surround"
v-model="form.mobile"
:customStyle="{
<up-input placeholder="请填写收货人手机号" border="surround" v-model="form.mobile" :customStyle="{
border: '0rpx',
}"
type="number"
></up-input>
}" type="number" maxlength="11"></up-input>
</view>
<view class="add_form_item add_form_item_but">
<view class="add_form_title">所在地区</view>
@@ -34,25 +20,15 @@
<view style="color: #808080; margin-left: 22rpx; font-size: 28rpx">{{
(form.regionStr && form.regionStr.join("/")) || "所在地区"
}}</view>
<up-image
src="/static/common/right_soild.png"
width="14"
height="14"
bgColor="#f1f6ff00"
></up-image>
<up-image src="/static/common/right_soild.png" width="14" height="14" bgColor="#f1f6ff00"></up-image>
</view>
</view>
<view class="add_form_item">
<view class="add_form_title">详细地址</view>
<up-input
placeholder="门牌号、小区、楼栋号、单元室等"
border="surround"
v-model="form.address"
:customStyle="{
<up-input placeholder="门牌号、小区、楼栋号、单元室等" border="surround" v-model="form.address" :customStyle="{
border: '0rpx',
height: '100rpx',
}"
></up-input>
}"></up-input>
</view>
</view>
<!-- <view class="address_default">
@@ -78,10 +54,10 @@
@cancel="regionShow = false"
@close="regionShow = false"
></up-picker> -->
<AddressPopup @ok="onOk"
:show="regionShow" @itemSelect="onItemSelect" @select="onSelect" :curSelect="curSelect" :selectList="curSelectList" :dataList="addressList" @close="onClose"></AddressPopup>
<AddressPopup @ok="onOk" :show="regionShow" @itemSelect="onItemSelect" @select="onSelect" :curSelect="curSelect"
:selectList="curSelectList" :dataList="addressList" @close="onClose"></AddressPopup>
<qiaobao-assistant page-key="pages/mine_package/mine_address_add/mine_address_add" title="编辑收货地址" />
</view>
</view>
</template>
<script>
@@ -91,10 +67,10 @@ import AddressPopup from '@/components/common/address-popup.vue'
import Header from "@/components/header.vue";
export default {
components: { Header,AddressPopup },
components: { Header, AddressPopup },
data() {
return {
titleText:'添加地址',
titleText: '添加地址',
form: {
id: 0,
name: null, // 收货人
@@ -108,9 +84,9 @@ export default {
address: null, // 详细地址
},
regionShow: false,
curSelect:-1,
addressList:[],
curSelectList:[],
curSelect: -1,
addressList: [],
curSelectList: [],
regionColumns: [],
regionLoading: false,
// leftPath: "/pages/mine_package/mine_address/mine_address",
@@ -126,7 +102,7 @@ export default {
}
},
methods: {
onItemSelect(item,xItem){
onItemSelect(item, xItem) {
this.curSelectList.push(xItem);
this.curSelect++;
this.getAddressList(xItem.id).then(() => {
@@ -135,12 +111,12 @@ export default {
}
});
},
onSelect(item,index){
onSelect(item, index) {
this.curSelectList.splice(index + 1);
this.curSelect = index;
this.getAddressList(this.curSelectList[this.curSelectList.length - 1].id);
},
async getAddressList(id = 0){
async getAddressList(id = 0) {
const params = {
parentId: id,
};
@@ -148,54 +124,54 @@ export default {
if (resp.bizcode === 100) {
this.addressList = [];
resp.data.forEach(item=>{
if(this.addressList.length){
let index = this.addressList.findIndex(i=>i.zi == item.firstLetter);
if(index != -1){
this.addressList[index].data.push({id:item.id,pId:item.parentId,name:item.name});
}else{
this.addressList.push({zi:item.firstLetter,data:[{id:item.id,pId:item.parentId,name:item.name}]});
resp.data.forEach(item => {
if (this.addressList.length) {
let index = this.addressList.findIndex(i => i.zi == item.firstLetter);
if (index != -1) {
this.addressList[index].data.push({ id: item.id, pId: item.parentId, name: item.name });
} else {
this.addressList.push({ zi: item.firstLetter, data: [{ id: item.id, pId: item.parentId, name: item.name }] });
}
}else{
this.addressList.push({zi:item.firstLetter,data:[{id:item.id,pId:item.parentId,name:item.name}]});
} else {
this.addressList.push({ zi: item.firstLetter, data: [{ id: item.id, pId: item.parentId, name: item.name }] });
}
})
this.addressList.sort((a,b)=>{
this.addressList.sort((a, b) => {
if (a.zi < b.zi) return -1;
if (a.zi > b.zi) return 1;
return 0;
});
}
},
onOk(){
onOk() {
this.form.regionStr = [];
this.curSelectList.forEach((item,index)=>{
if(index == 0){
this.curSelectList.forEach((item, index) => {
if (index == 0) {
this.form.countryId = item.id;
this.form.regionStr.push(item.name);
}else if(index == 1){
} else if (index == 1) {
this.form.provinceId = item.id;
this.form.regionStr.push(item.name);
}else if(index == 2){
} else if (index == 2) {
this.form.cityId = item.id;
this.form.regionStr.push(item.name);
}else if(index == 3){
} else if (index == 3) {
this.form.areaId = item.id;
this.form.regionStr.push(item.name);
}else if(index == 4){
} else if (index == 4) {
this.form.townshipId = item.id;
this.form.regionStr.push(item.name);
}
})
console.log('areaId',this.form)
console.log('areaId', this.form)
this.onClose();
},
onClose(){
this.regionShow=false;
this.curSelect=false;
this.addressList=[];
this.curSelectList=[];
onClose() {
this.regionShow = false;
this.curSelect = false;
this.addressList = [];
this.curSelectList = [];
},
/**
* 用作编辑
@@ -243,37 +219,37 @@ export default {
async regionShowCheck() {
this.curSelectList = [];
this.curSelect = -1;
if(this.form.townshipId){
this.curSelectList.push({id:this.form.countryId,name:this.form.regionStr[0]});
this.curSelectList.push({id:this.form.provinceId,name:this.form.regionStr[1]});
this.curSelectList.push({id:this.form.cityId,name:this.form.regionStr[2]});
this.curSelectList.push({id:this.form.areaId,name:this.form.regionStr[3]});
this.curSelectList.push({id:this.form.townshipId,name:this.form.regionStr[4]});
if (this.form.townshipId) {
this.curSelectList.push({ id: this.form.countryId, name: this.form.regionStr[0] });
this.curSelectList.push({ id: this.form.provinceId, name: this.form.regionStr[1] });
this.curSelectList.push({ id: this.form.cityId, name: this.form.regionStr[2] });
this.curSelectList.push({ id: this.form.areaId, name: this.form.regionStr[3] });
this.curSelectList.push({ id: this.form.townshipId, name: this.form.regionStr[4] });
this.curSelect = 4;
await this.getAddressList(this.form.townshipId);
}else if(this.form.areaId){
this.curSelectList.push({id:this.form.countryId,name:this.form.regionStr[0]});
this.curSelectList.push({id:this.form.provinceId,name:this.form.regionStr[1]});
this.curSelectList.push({id:this.form.cityId,name:this.form.regionStr[2]});
this.curSelectList.push({id:this.form.areaId,name:this.form.regionStr[3]});
} else if (this.form.areaId) {
this.curSelectList.push({ id: this.form.countryId, name: this.form.regionStr[0] });
this.curSelectList.push({ id: this.form.provinceId, name: this.form.regionStr[1] });
this.curSelectList.push({ id: this.form.cityId, name: this.form.regionStr[2] });
this.curSelectList.push({ id: this.form.areaId, name: this.form.regionStr[3] });
this.curSelect = 3;
await this.getAddressList(this.form.areaId);
}else if(this.form.cityId){
this.curSelectList.push({id:this.form.countryId,name:this.form.regionStr[0]});
this.curSelectList.push({id:this.form.provinceId,name:this.form.regionStr[1]});
this.curSelectList.push({id:this.form.cityId,name:this.form.regionStr[2]});
} else if (this.form.cityId) {
this.curSelectList.push({ id: this.form.countryId, name: this.form.regionStr[0] });
this.curSelectList.push({ id: this.form.provinceId, name: this.form.regionStr[1] });
this.curSelectList.push({ id: this.form.cityId, name: this.form.regionStr[2] });
this.curSelect = 2;
await this.getAddressList(this.form.cityId);
}else if(this.form.provinceId){
this.curSelectList.push({id:this.form.countryId,name:this.form.regionStr[0]});
this.curSelectList.push({id:this.form.provinceId,name:this.form.regionStr[1]});
} else if (this.form.provinceId) {
this.curSelectList.push({ id: this.form.countryId, name: this.form.regionStr[0] });
this.curSelectList.push({ id: this.form.provinceId, name: this.form.regionStr[1] });
this.curSelect = 1;
await this.getAddressList(this.form.provinceId);
}else if(this.form.countryId){
this.curSelectList.push({id:this.form.countryId,name:this.form.regionStr[0]});
} else if (this.form.countryId) {
this.curSelectList.push({ id: this.form.countryId, name: this.form.regionStr[0] });
this.curSelect = 0;
await this.getAddressList(this.form.countryId);
}else{
} else {
await this.getAddressList();
}
this.regionShow = true;
@@ -356,6 +332,13 @@ export default {
});
return;
}
if (!/^1[3-9]\d{9}$/.test(params.mobile)) {
this.$refs.uToastRef.show({
type: "error",
message: "请输入正确的手机号码",
});
return;
}
if (!params.regionStr) {
this.$refs.uToastRef.show({
type: "error",
@@ -390,7 +373,7 @@ export default {
}
delete params.regionStr;
if(params.countryId != 47021){
if (params.countryId != 47021) {
this.$refs.uToastRef.show({
type: "error",
message: '选择地址错误'
@@ -96,26 +96,25 @@
<template v-if="orderInfo.orderPackages && orderInfo.orderPackages.length > 0">
<view class="express_header_row">
<view class="express_title">运单号</view>
<view
class="express_toggle_btn"
v-if="orderInfo.orderPackages.length > 1"
@click="isExpandExpress = !isExpandExpress"
>
<text>{{ isExpandExpress ? '收起' : `展开全部(${orderInfo.orderPackages.length})` }}</text>
<u-icon :name="isExpandExpress ? 'arrow-up' : 'arrow-down'" color="#7934F6" size="12" style="margin-left: 4rpx;"></u-icon>
<view class="express_toggle_btn" v-if="orderInfo.orderPackages.length > 1"
@click="isExpandExpress = !isExpandExpress">
<text>{{ isExpandExpress ? '收起' : `展开全部(${orderInfo.orderPackages.length})`
}}</text>
<u-icon :name="isExpandExpress ? 'arrow-up' : 'arrow-down'" color="#7934F6"
size="12" style="margin-left: 4rpx;"></u-icon>
</view>
</view>
<view
class="express_item_row"
<view class="express_item_row"
v-for="(pkg, pkgIdx) in (isExpandExpress ? orderInfo.orderPackages : orderInfo.orderPackages.slice(0, 1))"
:key="pkg.id || pkgIdx"
>
:key="pkg.id || pkgIdx">
<view class="express_info_text">
<text class="express_name_tag" v-if="pkg.expressName">{{ pkg.expressName }}</text>
<text class="express_name_tag" v-if="pkg.expressName">{{ pkg.expressName
}}</text>
<text class="express_no_num">{{ pkg.expressNo }}</text>
</view>
<view class="express_copy_btn" @click="copyData(pkg.expressNo)">
<up-image src="/static/common/copy.png" width="12" height="12" bgColor="#f1f6ff00"></up-image>
<up-image src="/static/common/copy.png" width="12" height="12"
bgColor="#f1f6ff00"></up-image>
<text class="copy_text">复制</text>
</view>
</view>
@@ -130,7 +129,8 @@
<text class="express_no_num">{{ pItem.expressNo }}</text>
</view>
<view class="express_copy_btn" @click="copyData(pItem.expressNo)">
<up-image src="/static/common/copy.png" width="12" height="12" bgColor="#f1f6ff00"></up-image>
<up-image src="/static/common/copy.png" width="12" height="12"
bgColor="#f1f6ff00"></up-image>
<text class="copy_text">复制</text>
</view>
</view>
@@ -225,8 +225,8 @@
<view class="operate_comm operate_but_2" v-if="orderInfo.status === ORDER_TYPE.UNPAID" @click="goPay">去付款
</view>
<!-- <view class="operate_comm operate_but_1" v-if="orderInfo.status === ORDER_TYPE.COMPLETED">申请退款</view> -->
<view class="operate_comm operate_but_1" style="border-color: #7732ff" v-if="orderInfo.status === ORDER_TYPE.NOT_RECEIVE"
@click="onExpress(orderInfo.id)">
<view class="operate_comm operate_but_1" style="border-color: #7732ff"
v-if="orderInfo.status === ORDER_TYPE.NOT_RECEIVE" @click="onExpress(orderInfo.id)">
<text class="text-zi">查看物流</text>
</view>
<view class="operate_comm operate_but_2" v-if="orderInfo.status === ORDER_TYPE.NOT_RECEIVE"
@@ -285,6 +285,7 @@ import {
import {
appWxpayFun,
zfbPayFun,
douyinPayFun,
jumpWayOk,
jumpWayError,
} from "@/utils/payUtils.js";
@@ -358,6 +359,10 @@ export default {
let params = {
types: "1,2,3",
};
// #ifdef APP-PLUS
params.types = "1,2,3,6"
// #endif
const result = await getSupportPay(params);
if (result && result.bizcode === 100) {
const data = result.data || [];
@@ -433,6 +438,9 @@ export default {
item.alipay
) {
this.zfbPay(item.alipay, item.orderId, item.orderNum);
} else if (item.douyin && Object.keys(item.douyin).length !== 0) {
// 抖音支付
this.appDouying(item.douyin, item.orderId, item.orderNum);
} else {
this.$refs.uToastRef.show({
type: "error",
@@ -446,6 +454,18 @@ export default {
this.getOrderInfo();
}
},
// app 抖音支付
async appDouying(douyin, orderId, orderNum) {
try {
const result = await douyinPayFun(douyin, orderId, orderNum);
console.log("抖音支付发起成功", result);
// 处理支付发起成功后的逻辑
} catch (error) {
console.error("抖音支付失败", error);
// 处理支付失败后的逻辑
}
},
// app 微信支付
async appWxpay(alipay, orderId, orderNum) {
try {
@@ -457,6 +477,7 @@ export default {
// 处理支付失败后的逻辑,如提示用户等
}
},
// 支付宝支付
async zfbPay(alipay, orderId, orderNum) {
try {
+126 -14
View File
@@ -8,6 +8,8 @@
<up-image :src="`https://static.tbmall.xin/static/mine-purse/${item.categoty}.png`" width="22" height="22"
bgColor="#f1f6ff00" class="purse_amount"></up-image>
<view class="title">{{ item.name }}</view>
<up-image v-if="item.categoty != 'balance'" src="/pages/rwa_package/static/tips.png" width="16" height="16"
bgColor="#f1f6ff00" @click.stop="openTip(item.categoty)" />
<up-image src="/static/common/right_soild.png" width="10" height="10" bgColor="#fff"
class="right_soild"></up-image>
</view>
@@ -31,17 +33,20 @@
</view>
<view class="bottom_item" v-if="item.categoty == 'vcoin'">
<view class="bottom_item_amount">{{ item.bonusAmount || 0 }}</view>
<view class="bottom_item_title">消费者权益</view>
<view class="bottom_item_amount bottom_item_amount_center">{{ item.bonusAmount || 0 }}</view>
<view class="bottom_item_title bottom_item_title_center">
<text class="title_text">消费者权益</text>
<up-image v-if="item.categoty != 'balance'" src="/pages/rwa_package/static/tips.png" width="12" height="12"
bgColor="#f1f6ff00" class="tip_icon" @click.stop="openTip('rights')" />
</view>
</view>
<view class="bottom_item" @click.stop="onTip(vIndex)">
<view class="bottom_item_lock">{{ item.lockAmount || 0 }}</view>
<view class="bottom_item_title bottom_item_title_right">
<text class="title_text">{{ item.categoty === "raffle" ? "不可用次数" : "不可用余额" }}</text>
<view class="tips_icon_box" v-if="item.categoty == 'balance' || item.categoty == 'raffle'">
<up-image src="/pages/rwa_package/static/tips.png" width="12" height="12" bgColor="#fff" />
</view>
<up-image v-if="item.categoty == 'balance' || item.categoty == 'raffle'"
src="/pages/rwa_package/static/tips.png" width="12" height="12" bgColor="#fff" class="tip_icon" />
</view>
</view>
</view>
@@ -99,6 +104,17 @@
</view>
</view>
</up-popup>
<!-- 字段解释弹窗 -->
<up-popup :show="tipShow" mode="center" :round="16" @close="tipShow = false">
<view class="tip_popup_content">
<view class="tip_popup_title">字段解释</view>
<view class="tip_popup_body">
<view class="tip_subtitle">{{ currentTip.subTitle }}</view>
<view class="tip_detail">{{ currentTip.detail }}</view>
</view>
<view class="tip_popup_btn" @click="tipShow = false">我知道了</view>
</view>
</up-popup>
<qiaobao-assistant page-key="pages/mine_package/mine_purse/mine_purse" title="钱包与账户" />
</view>
</template>
@@ -111,6 +127,9 @@ import { getValue } from "@/utils/enumUtils.js";
import { formatDate, handleAmount } from "@/utils/index.js";
export default {
options: {
styleIsolation: 'shared',
},
components: { headerComponent: Header },
data() {
return {
@@ -120,6 +139,8 @@ export default {
collectShow: false,
collectSuccess: false,
availableAmount: 0,
tipShow: false,
tipCategory: "",
collectForm: {
mobile: "",
amount: "",
@@ -128,10 +149,38 @@ export default {
},
};
},
computed: {
currentTip() {
const map = {
expend: {
subTitle: "贡献值:",
detail: "购买商品确认签收后立即可获得贡献值。按毛利的1:1发放",
},
raffle: {
subTitle: "抽奖次数:",
detail: "购买商品可获得抽奖次数,抽奖次数可在抽奖活动中进行使用",
},
vcoin: {
subTitle: "数字积分:",
detail:
"根据贡献值按照比例进行转换获得1. 商城贡献值10W内的贡献值1:1转换成数字积分2. 商城贡献值10W外的根据数字积分数量和总贡献值及贡献值占比进行获得。",
},
rights: {
subTitle: "消费者权益:",
detail: "根据数字积分进行转换,根据消费者权益可获得共享计划权益",
},
};
return map[this.tipCategory] || { subTitle: "", detail: "" };
},
},
onLoad() {
this.getAcctBalance();
},
methods: {
openTip(category) {
this.tipCategory = category;
this.tipShow = true;
},
getValue,
formatDate,
handleAmount,
@@ -365,6 +414,10 @@ export default {
font-weight: bold;
font-size: 36rpx;
color: #333333;
&.bottom_item_amount_center {
text-align: center;
}
}
.bottom_item_lock {
@@ -379,6 +432,7 @@ export default {
font-size: 20rpx;
color: #999999;
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
@@ -386,23 +440,34 @@ export default {
justify-content: flex-end;
}
.title_text {
white-space: nowrap;
&.bottom_item_title_center {
justify-content: center;
}
.tips_icon_box {
margin-left: 8rpx;
flex-shrink: 0;
.title_text {
white-space: nowrap;
font-size: 20rpx;
color: #999999;
line-height: 1;
display: flex;
align-items: center;
}
::v-deep(.bottom_item_tip_title) {
.u-image__image {
margin-top: 8rpx;
::v-deep(.tip_icon),
.tip_icon {
margin-left: 8rpx;
}
display: flex;
align-items: center;
justify-content: center;
.u-image,
.u-image__image,
image {
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 !important;
}
}
}
}
@@ -553,5 +618,52 @@ export default {
}
}
}
.tip_popup_content {
background: #ffffff;
width: 600rpx;
border-radius: 24rpx;
padding: 44rpx 36rpx;
box-sizing: border-box;
.tip_popup_title {
font-weight: 600;
font-size: 34rpx;
color: #333333;
text-align: center;
margin-bottom: 40rpx;
}
.tip_popup_body {
margin-bottom: 48rpx;
.tip_subtitle {
font-size: 28rpx;
color: #333333;
font-weight: 500;
margin-bottom: 12rpx;
}
.tip_detail {
font-size: 28rpx;
color: #333333;
line-height: 1.6;
white-space: pre-wrap;
}
}
.tip_popup_btn {
width: 100%;
height: 84rpx;
background: #707070;
border-radius: 42rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>
@@ -218,6 +218,8 @@
<image v-if="item.type == 'alipay'" src="https://static.tbmall.xin/static/new/zfb.png"></image>
<image v-if="item.type == 'balance'" src="https://static.tbmall.xin/static/new/bank.png"></image>
<image v-if="item.type == 'redpacket'" src="https://static.tbmall.xin/static/new/jf.png"></image>
<image v-if="item.type == 'douyin'" src="https://static.tbmall.xin/static/new/jf.png"></image>
<text v-if="item.type == 'wechat' || item.type == 'alipay'" class="text-32 text-zu"
style="margin-left: 20rpx">{{ item.name }}</text>
<text v-else style="margin-left: 20rpx" class="text-32 text-zu">{{ item.name }}
@@ -338,6 +340,7 @@ import { getUserInfo } from "@/api/auth.js";
import {
appWxpayFun,
zfbPayFun,
douyinPayFun,
jumpWayOk,
jumpWayError,
} from "@/utils/payUtils.js";
@@ -597,9 +600,9 @@ export default {
}
// #endif
// #ifndef MP-WEIXIN
parmas["types"] = "1,2,3";
parmas["types"] = "1,2,3,6";
if (this.isCoupon) {
parmas["types"] = "1,2,3,5";
parmas["types"] = "1,2,3,5,6";
parmas.isDeduction = true;
}
// #endif
@@ -1052,6 +1055,7 @@ export default {
if (this.selectedCoupon) {
params["couponUserId"] = this.getCouponUserId(this.selectedCoupon);
}
console.log("秦星星----2", params);
resp = await settle(params);
}
@@ -1073,6 +1077,10 @@ export default {
item.alipay
) {
this.zfbPay(item.alipay, item.orderId, item.orderNum);
} else if (item.douyin &&
Object.keys(item.douyin).length !== 0 &&
item.douyin) {
this.appDouying(item.douyin, item.orderId, item.orderNum);
} else {
this.$refs.uToastRef.show({
type: "error",
@@ -1086,6 +1094,19 @@ export default {
this.getOrder();
}
},
// app 抖音支付
async appDouying(douyin, orderId, orderNum) {
try {
const result = await douyinPayFun(douyin, orderId, orderNum);
console.log("抖音支付发起成功", result);
// 处理支付发起成功后的逻辑
} catch (error) {
console.error("抖音支付失败", error);
// 处理支付失败后的逻辑
}
},
// app 微信支付
async appWxpay(alipay, orderId, orderNum) {
try {
@@ -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,44 +1,98 @@
<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="invite_but">
<button
class="share-btn"
@click="openSharePopup"
>
<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>
<!-- 3. 卡片容器 (填充中间区域,内部固定标题) -->
<view class="card_container">
<view class="card_title">我的邀请</view>
<!-- 无数据状态 (图一) -->
<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>
<!-- 分享弹窗 -->
<SharePopup
ref="share"
v-if="shareOpen"
:show="shareShow"
@close="onClose"
@wxShare="onWxShare"
@share="onShare"
:id="posterId"
text="我的推广海报"
:tu="inviteImage || shareImg"
></SharePopup>
<!-- 分享弹窗组件 -->
<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>
<qiaobao-assistant page-key="pages/other_package/invite_friends/invite_friends" title="我的推广" />
</view>
</template>
<script>
import { getSharePoster, generateSharePoster } from "@/api/common.js";
import SharePopup from "@/pages/other_package/components/share-popup.vue";
<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 {
export default {
components: {
Header,
SharePopup
},
data() {
@@ -48,11 +102,20 @@
shareImg: '',
shareShow: false,
shareOpen: true,
posterId: ''
posterId: '',
// 邀请列表相关数据
list: [],
page: 1,
pageSize: 10,
totalPage: 1,
loading: false,
refreshing: false,
finished: false
};
},
mounted() {
this.getSharePoster();
this.getReferralList(true);
},
onShareAppMessage() {
return {
@@ -69,7 +132,56 @@
};
},
methods: {
// 打开分享弹窗 / 微信小程序下直接触发朋友圈(图片分享菜单)效果
// 获取邀请好友列表
async getReferralList(isRefresh = false) {
if (this.loading) return;
if (isRefresh) {
this.page = 1;
this.finished = false;
}
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;
}
}
} catch (error) {
console.error("获取邀请列表失败:", error);
} finally {
this.loading = false;
this.refreshing = false;
}
},
// 下拉刷新
async onRefresh() {
this.refreshing = true;
await this.getReferralList(true);
},
// 触底加载更多
onReachBottom() {
if (!this.finished && !this.loading) {
this.page++;
this.getReferralList(false);
}
},
// 打开分享弹窗 / 微信小程序下直接触发朋友圈/分享
openSharePopup() {
if (!this.inviteImage && !this.shareImg) {
this.$refs.uToastRef.show({
@@ -95,10 +207,8 @@
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({
@@ -289,11 +399,9 @@
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);
}
@@ -305,50 +413,221 @@
}
}
}
};
</script>
};
</script>
<style lang="scss" scoped>
.invite_friends_warp {
height: calc(100vh - 80rpx);
padding: 10rpx 40rpx 40rpx;
<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;
}
.invite_img {
.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;
}
/* 有数据表格状态 */
.table_box {
flex: 1;
height: 0;
min-height: 0;
display: flex;
flex-direction: column;
border: 1rpx solid #F0EBF8;
border-radius: 16rpx;
overflow: hidden;
.loading {
/* 固定表头 */
.table_header {
flex-shrink: 0;
display: flex;
background-color: #FAF7FF;
border-bottom: 1rpx solid #F0EBF8;
.th {
flex: 1;
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;
padding: 22rpx 8rpx;
font-size: 26rpx;
font-weight: 500;
color: #666666;
&.th_border {
border-right: 1rpx solid #F0EBF8;
}
}
}
/* 仅此处表格列表可垂直滑动,支持 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.8;
opacity: 0.9;
}
}
</style>
}
</style>
@@ -45,7 +45,7 @@ export function getCurrentMember() {
return request({ url: '/user/getUserInfo', method: 'get', isShowLoading: false }).then(result => result.data)
}
export function getMessages({ userId, conversationId, page = 1, pageSize = 80 }) {
export function getMessages({ userId, conversationId, page = 1, pageSize = 20 }) {
const data = conversationId ? { userId, conversationId, page, pageSize } : { userId, page, pageSize }
return kefuRequest({
url: '/kefu/user/messages',
+233
View File
@@ -0,0 +1,233 @@
# 抖音支付:前端调用商城后端接口文档
日期:2026-09-05。适用:商城 App 前端。本文只描述前端调用我们后端的接口,不涉及抖音官方服务端接口、证书配置或插件开发。
## 一、公共约定
测试环境 Base URL:`https://api.o.tbmall.xin`。生产环境沿用项目正式用户端 API 配置,不要使用后台管理 API 地址。
请求头沿用现有登录请求封装:
```http
Authorization: <项目当前环境的应用授权值>
HSM-AUTH: <当前用户登录令牌>
Content-Type: application/x-www-form-urlencoded
```
POST 参数以表单提交,不是 JSON body;GET 使用 URL 查询参数。不要把后台管理 Token 填到 HSM-AUTH。
业务响应统一格式:
```json
{ "bizcode": 100, "errcode": 0, "msg": "成功", "data": {} }
```
`bizcode=100` 表示本次接口调用成功,不等于用户已付款。其他业务码按 msg 提示;HTTP 错误、超时或网络失败不能视作付款成功。以下所有 ID、金额、签名均为示例。
## 二、调用顺序
1. 调用 `/cart/getSupportPay`,判断返回列表是否包含 `type=douyin`。
2. 根据业务场景选择一个下单/支付接口,提交 `payway=douyin`。
3. 保存返回的 `data.orderNum`,把 `data.douyin` 中的支付参数交给 App 支付能力拉起抖音。
4. 用户返回 App 后调用 `/payment/queryResult` 查询结果,只有 `data.state=2` 才提示支付成功。
**重点:支付方式筛选传 `types=6`;实际支付传 `payway=douyin`,不是 `payway=6`。**
## 三、查询支付方式
**POST `/cart/getSupportPay`**
| 参数 | 类型 | 必填 | 说明 |
| ----------- | ------- | ---- | -------------------------------------------- |
| types | string | 否 | 只查抖音传 `6`;多种传 `1,2,6`;全部传 `ALL` |
| isDeduction | boolean | 否 | 默认 false,沿用现有抵扣券结算逻辑 |
请求表单:
```text
types=6&isDeduction=false
```
已启用抖音支付时的响应示例:
```json
{
"bizcode": 100,
"errcode": 0,
"msg": "成功",
"data": [{ "type": "douyin", "name": "抖音支付", "balance": 0 }]
}
```
没有启用对应配置时,列表可能为空。显示名称使用 name,提交支付方式使用 type;balance 不是抖音钱包余额。
## 四、发起支付(按场景选一个)
### 4.1 已创建订单,继续支付
**POST `/order/payment`**
| 参数 | 类型 | 必填 | 说明 |
| ------ | ------ | ---- | ----------------------- |
| id | long | 是 | 待支付的商城业务订单 ID |
| payway | string | 是 | 固定 `douyin` |
```text
id=123456&payway=douyin
```
后端接口还接受可选 password/code,但抖音支付不需要提交。成功响应见第五节。
### 4.2 商品直接购买并支付
**POST `/cart/settlementOrder`**
| 参数 | 类型 | 必填 | 说明 |
| ------------ | ------- | -------- | ------------------------------------ |
| addrId | long | 是 | 收货地址 ID |
| goodsId | long | 是 | 商品 ID |
| specsId | long | 是 | 规格 ID |
| goodsNum | decimal | 是 | 购买数量 |
| payway | string | 本流程是 | `douyin` |
| payway2 | string | 否 | `vcoin` 表示抵扣券,沿用现有结算选择 |
| couponUserId | long | 否 | 已选用户优惠券 ID |
| remark | string | 否 | 现有分组备注 JSON 字符串 |
```text
addrId=10001&goodsId=20001&specsId=30001&goodsNum=1&payway=douyin
```
remark 格式为 `[{"gkey":"结算返回的分组标识","rmk":"备注内容"}]`,通过现有请求封装表单编码。金额由后端计算,不传前端自算金额。优惠券、抵扣券沿用结算规则,不因选择抖音改变适用范围。
### 4.3 购物车结算并支付
**POST `/cart/settle`**
| 参数 | 类型 | 必填 | 说明 |
| ------------ | ------ | ---- | ---------------------------------- |
| addrId | long | 是 | 收货地址 ID |
| cartIds | string | 是 | 购物车 ID 逗号分隔,`ALL` 表示全选 |
| payway | string | 是 | `douyin` |
| couponUserId | long | 否 | 用户优惠券 ID |
| remark | string | 否 | 分组备注 JSON 字符串,同上一接口 |
```text
addrId=10001&cartIds=40001,40002&payway=douyin
```
购物车可能拆分业务订单并合并支付,查询支付结果仍使用本次返回的 orderNum。
### 4.4 余额充值
**POST `/finance/recharge`**
| 参数 | 类型 | 必填 | 说明 |
| -------- | ------- | ---- | -------------------- |
| category | string | 是 | 余额充值传 `balance` |
| amount | decimal | 是 | 充值金额,单位元 |
| payway | string | 是 | `douyin` |
```text
category=balance&amount=1.00&payway=douyin
```
注意:上述四个 POST 都可能创建支付单,请勿反复点击或自动重试创建。新下单接口已经返回支付参数时,不要紧接着再调用 `/order/payment`。
## 五、下单/支付接口统一返回
上述四个接口均返回 PayInfoDto。抖音待支付响应示例(省略其他业务字段):
```json
{
"bizcode": 100,
"errcode": 0,
"msg": "成功",
"data": {
"orderId": 123456,
"orderNum": "PAY_EXAMPLE_001",
"amount": 1.0,
"status": 0,
"douyin": {
"appId": "EXAMPLE_APP_ID",
"mchId": "EXAMPLE_MERCHANT_ID",
"prepayId": "EXAMPLE_PREPAY_ID",
"callbackScheme": "trustbridgeapp",
"packageValue": "Sign=DYPay",
"nonceStr": "0123456789abcdef0123456789abcdef",
"timeStamp": "1788585600",
"sign": "EXAMPLE_BASE64_SIGNATURE"
}
}
}
```
| 字段 | 类型 | 前端用途 |
| --------------------- | ------- | ---------------------------------------------- |
| orderId | long | 后端返回的订单 ID |
| orderNum | string | 保存下来,支付结果查询必须用它 |
| amount | decimal | 实际支付金额(元) |
| status | integer | 此 DTO 中 0=待支付、1=已支付;不同于查单 state |
| douyin | object | App 拉起抖音所需参数 |
| douyin.appId | string | 抖音应用 ID |
| douyin.mchId | string | 抖音商户 ID |
| douyin.prepayId | string | 预支付 ID |
| douyin.callbackScheme | string | App 返回 Scheme |
| douyin.packageValue | string | `Sign=DYPay` |
| douyin.nonceStr | string | 随机字符串 |
| douyin.timeStamp | string | 秒级时间戳,保留字符串类型 |
| douyin.sign | string | 后端生成的签名,原样使用 |
后端已处理签名,前端无需请求商户私钥或另调签名接口。参数缺失时停止拉起,提示支付参数异常并反馈后端,不要自行伪造字段。
若 App 支付桥接要求 SDK 小写字段,映射如下;这不是另一个 HTTP 请求:
```js
const d = response.data.douyin;
const payInfo = {
appid: d.appId,
partnerid: d.mchId,
prepayid: d.prepayId,
package: d.packageValue,
noncestr: d.nonceStr,
timestamp: d.timeStamp,
sign: d.sign,
};
```
## 六、查询支付结果
**GET `/payment/queryResult`**
| 参数 | 类型 | 必填 | 说明 |
| -------- | ------ | ---- | ------------------------------------------------------- |
| orderNum | string | 是 | 发起支付接口返回的 data.orderNum,不是 orderId/prepayId |
```http
GET https://api.o.tbmall.xin/payment/queryResult?orderNum=PAY_EXAMPLE_001
```
```json
{
"bizcode": 100,
"errcode": 0,
"msg": "成功",
"data": { "state": 2 }
}
```
| state | 含义 | 前端处理 |
| ----- | ---------- | -------------------------------- |
| 0 | 订单不存在 | 核对支付编号及环境,不能提示成功 |
| 1 | 支付处理中 | 稍后再次查询 |
| 2 | 支付成功 | 展示成功,刷新订单或充值余额 |
| 3 | 支付失败 | 提示失败,刷新订单状态 |
当前 App 采用最多 5 次、间隔 1.5 秒的查询;这属于前端策略,不是后端强制限制。暂未确认、断网或超时应显示“支付结果确认中”,保存编号以便回前台再次查询,避免直接重复付款。
用户取消或支付 SDK 提示成功后,都需要以服务端查询结果为准。不要调用 `/payment/feedback` 上报成功来替代查单;前端也不需要调用后端的抖音通知/退款回调接口。
## 七、对接范围与发布注意
- 本文按当前工作区后端代码整理;SDK 签名新增字段(packageValue、nonceStr、timeStamp、sign)尚未确认已部署到测试/生产服务器。
- 后台配置已完成不代表预下单、签名、支付回调均验收通过,实际支付链路仍需联调。
- 本次抖音支付是 App 原生支付;H5 仅可检查适用的后端业务请求,不能凭网页调用拉起此原生能力。
+3 -3
View File
@@ -454,7 +454,7 @@ export const FINANCE_WITHDRAWAL_STATUS = [
];
/**
* 页面:1-APP首页,2-APP分类页,3-商学院资讯,4-爆单区,5-好物区,6-积分专区,7-数字积分页 8-新人专享、9-超级补贴、10-限时秒杀、11-领券中心
* 页面:1-APP首页,2-APP分类页,3-资讯,4-爆单区,5-好物区,6-积分专区,7-数字积分页 8-新人专享、9-超级补贴、10-限时秒杀、11-领券中心
* @type {[{value: string, key: string}]}
*/
export const APP_PAGE_TYPE = {
@@ -486,8 +486,8 @@ export const NEWS_TYPE = [
key: 3,
value: "新闻"
}, {
key: 5,
value: "商学院"
// key: 5,
// value: "商学院"
},
// {
// key: 4,
+249
View File
@@ -109,6 +109,255 @@ export function zfbPayFun(alipay, orderId, orderNum) {
// });
}
/**
* 抖音支付(兼容 iOS、Android App 端)
* @param {Object|String} dyPay 抖音支付参数 (支持 orderStr, schema, url, payUrl, orderInfo 等)
* @param {Object|String|Number} orderId 支付的订单ID
* @param {Object|String} orderNum 支付的订单编号
*/
export function douyinPayFun(dyPay, orderId, orderNum) {
console.log("抖音支付---入参:", dyPay, orderId, orderNum);
return new Promise((resolve, reject) => {
if (!dyPay || (typeof dyPay === "object" && Object.keys(dyPay).length === 0)) {
uni.showToast({
title: "支付参数错误",
icon: "none",
});
jumpWayError();
return reject(new Error("支付参数为空"));
}
const d = typeof dyPay === "object" ? dyPay : {};
const sub = (typeof d.orderInfo === "object" && d.orderInfo) ? d.orderInfo : {};
const type = typeof dyPay === "object" ? (dyPay.type || "app") : "app";
// 1. 提取或生成 Scheme / URL (注意:排除 sign,避免将签名误认为 URL)
let orderStr = "";
if (typeof dyPay === "string") {
orderStr = dyPay;
} else if (typeof dyPay === "object") {
orderStr =
dyPay.orderStr ||
dyPay.schema ||
dyPay.scheme ||
dyPay.url ||
dyPay.payUrl ||
dyPay.link ||
sub.orderStr ||
sub.schema ||
sub.scheme ||
sub.url ||
"";
// 如果未配置直接链接,则尝试通过 prepayId / pay_token 自动生成抖音收银台 Scheme 备用
const token = d.prepayId || d.prepayid || sub.prepayid || sub.prepayId;
if (!orderStr && token) {
orderStr = `snssdk1128://pay?pay_token=${encodeURIComponent(token)}`;
}
}
console.log("抖音支付---提取到的 Scheme / orderStr:", orderStr);
// 2. 深度合并/映射原生支付所需的 orderInfo(融合根节点与 orderInfo 子节点,同时提供驼峰与小写命名)
const appId = d.appId || d.appid || sub.appId || sub.appid || "";
const mchId = d.mchId || d.partnerid || d.mchid || sub.mchId || sub.partnerid || sub.mchid || "";
const prepayId = d.prepayId || d.prepayid || sub.prepayId || sub.prepayid || "";
const callbackScheme = d.callbackScheme || sub.callbackScheme || "trustbridgeapp";
const packageValue = d.packageValue || d.package || sub.packageValue || sub.package || "Sign=DYPay";
const nonceStr = d.nonceStr || d.noncestr || sub.nonceStr || sub.noncestr || "";
const timeStamp = String(d.timeStamp || d.timestamp || sub.timeStamp || sub.timestamp || "");
const sign = d.sign || sub.sign || "";
const orderInfo = {
// 驼峰命名(标准 uni.requestPayment / 抖音 SDK)
appId,
mchId,
prepayId,
callbackScheme,
packageValue,
nonceStr,
timeStamp,
sign,
// 小写命名(部分原生桥接 SDK 兼容)
appid: appId,
partnerid: mchId,
mchid: mchId,
prepayid: prepayId,
package: packageValue,
noncestr: nonceStr,
timestamp: timeStamp,
service: 5,
};
// #ifdef APP-PLUS
// 情形 A:如果显式指定为 H5 方式
if (
type === "h5" &&
orderStr &&
(orderStr.startsWith("http://") || orderStr.startsWith("https://"))
) {
uni.navigateTo({
url:
`/pages/other_package/payment_processing/payment_processing?link=${encodeURIComponent(
orderStr
)}&orderNum=` + orderNum,
});
resolve({ status: "processing" });
return;
}
// 情形 B:如果后端直接传入了协议 Scheme 串(如以 snssdk1128://, douyin:// 开头)
if (
typeof dyPay === "string" ||
(dyPay.orderStr && (dyPay.orderStr.startsWith("snssdk") || dyPay.orderStr.startsWith("douyin")))
) {
let openSuccess = false;
// iOS Native.js 尝试
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(orderStr);
if (app && nsUrl && app.openURL(nsUrl)) {
openSuccess = true;
}
} catch (nativeErr) {
console.warn("iOS Native.js openURL 尝试:", nativeErr);
}
}
// Android Native.js 尝试
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(orderStr));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
main.startActivity(intent);
openSuccess = true;
} catch (androidErr) {
console.warn("Android Native.js Intent 尝试:", androidErr);
}
}
if (!openSuccess) {
plus.runtime.openURL(
orderStr,
() => {
if (orderNum) {
uni.navigateTo({
url:
`/pages/other_package/payment_processing/payment_processing?orderNum=` +
orderNum,
});
}
resolve({ status: "invoked" });
},
(err) => {
console.error("拉起抖音客户端失败:", err);
const isSimulator =
plus.navigator && plus.navigator.isSimulator
? plus.navigator.isSimulator()
: false;
let tip = "未检测到抖音客户端或拉起失败,请确认是否已安装抖音客户端";
if (isSimulator) {
tip = "iOS模拟器无法打开第三方应用Scheme,请在安装了抖音的真机上测试";
} else if (err && err.code === -3) {
tip = "未检测到抖音应用或当前基座未配置Scheme白名单,请确认真机已安装抖音";
}
uni.showToast({
title: tip,
icon: "none",
duration: 3000,
});
jumpWayError();
reject(err);
}
);
} else {
if (orderNum) {
uni.navigateTo({
url:
`/pages/other_package/payment_processing/payment_processing?orderNum=` +
orderNum,
});
}
resolve({ status: "invoked" });
}
return;
}
// 情形 C:使用 uni.requestPayment 调起原生支付,失败时通过生成 Scheme 降级拉起
uni.requestPayment({
provider: "toutiao",
orderInfo: orderInfo,
service: 5,
success: function (res) {
console.log("抖音支付成功:", res);
payFeedbackFun(orderId, orderNum, 1);
jumpWayOk();
resolve(res);
},
fail: function (err) {
console.error("uni.requestPayment 抖音支付失败/不兼容,尝试 Scheme 降级拉起:", err);
if (orderStr) {
plus.runtime.openURL(
orderStr,
() => {
if (orderNum) {
uni.navigateTo({
url:
`/pages/other_package/payment_processing/payment_processing?orderNum=` +
orderNum,
});
}
resolve({ status: "invoked_fallback" });
},
(openErr) => {
console.error("openURL 降级失败:", openErr);
jumpWayError();
reject(err);
}
);
} else {
jumpWayError();
reject(err);
}
},
});
return;
// #endif
// 兜底处理 (H5 及非 App 环境)
if (orderStr) {
uni.navigateTo({
url:
`/pages/other_package/payment_processing/payment_processing?link=${encodeURIComponent(
orderStr
)}&orderNum=` + orderNum,
});
resolve({ status: "processing" });
} else {
uni.showToast({
title: "抖音支付暂仅支持在 App 端调用",
icon: "none",
});
jumpWayError();
reject(new Error("抖音支付暂仅支持在 App 端调用"));
}
});
}
// 导出别名方便不同调用习惯
export const dyPayFun = douyinPayFun;
export const appDyPayFun = douyinPayFun;
export const appDouyinPayFun = douyinPayFun;
// 跳转到支付成功页面
export function jumpWayOk() {
uni.navigateTo({