feat: 新增拼团

This commit is contained in:
2026-09-23 10:29:41 +08:00
parent f7b9e66172
commit 67f8b3f352
33 changed files with 7491 additions and 858 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,211 @@
<template xlang="wxml" minapp="mpvue">
<view class="tki-qrcode">
<!-- #ifndef MP-ALIPAY -->
<canvas class="tki-qrcode-canvas" :canvas-id="cid" :style="{width:cpSize+'px',height:cpSize+'px'}" />
<!-- #endif -->
<!-- #ifdef MP-ALIPAY -->
<canvas :id="cid" :width="cpSize" :height="cpSize" class="tki-qrcode-canvas" />
<!-- #endif -->
<image v-show="show" :src="result" :style="{width:cpSize+'px',height:cpSize+'px'}" />
</view>
</template>
<script>
import QRCode from "./qrcode.js"
let qrcode
export default {
name: "tki-qrcode",
props: {
cid: {
type: String,
default: 'tki-qrcode-canvas'
},
size: {
type: Number,
default: 200
},
unit: {
type: String,
default: 'upx'
},
show: {
type: Boolean,
default: true
},
val: {
type: String,
default: ''
},
background: {
type: String,
default: '#ffffff'
},
foreground: {
type: String,
default: '#000000'
},
pdground: {
type: String,
default: '#000000'
},
icon: {
type: String,
default: ''
},
iconSize: {
type: Number,
default: 40
},
lv: {
type: Number,
default: 3
},
onval: {
type: Boolean,
default: false
},
loadMake: {
type: Boolean,
default: false
},
usingComponents: {
type: Boolean,
default: true
},
showLoading: {
type: Boolean,
default: true
},
loadingText: {
type: String,
default: '二维码生成中'
},
},
data() {
return {
result: '',
}
},
methods: {
_makeCode() {
let that = this
if (!this._empty(this.val)) {
qrcode = new QRCode({
context: that, // 上下文环境
canvasId:that.cid, // canvas-id
usingComponents: that.usingComponents, // 是否是自定义组件
showLoading: that.showLoading, // 是否显示loading
loadingText: that.loadingText, // loading文字
text: that.val, // 生成内容
size: that.cpSize, // 二维码大小
background: that.background, // 背景色
foreground: that.foreground, // 前景色
pdground: that.pdground, // 定位角点颜色
correctLevel: that.lv, // 容错级别
image: that.icon, // 二维码图标
imageSize: that.iconSize,// 二维码图标大小
cbResult: function (res) { // 生成二维码的回调
that._result(res)
},
});
} else {
uni.showToast({
title: '二维码内容不能为空',
icon: 'none',
duration: 2000
});
}
},
_clearCode() {
this._result('')
qrcode.clear()
},
_saveCode() {
let that = this;
if (this.result != "") {
uni.saveImageToPhotosAlbum({
filePath: that.result,
success: function () {
uni.showToast({
title: '二维码保存成功',
icon: 'success',
duration: 2000
});
}
});
}
},
_result(res) {
this.result = res;
this.$emit('result', res)
},
_empty(v) {
let tp = typeof v,
rt = false;
if (tp == "number" && String(v) == "") {
rt = true
} else if (tp == "undefined") {
rt = true
} else if (tp == "object") {
if (JSON.stringify(v) == "{}" || JSON.stringify(v) == "[]" || v == null) rt = true
} else if (tp == "string") {
if (v == "" || v == "undefined" || v == "null" || v == "{}" || v == "[]") rt = true
} else if (tp == "function") {
rt = false
}
return rt
}
},
watch: {
size: function (n, o) {
if (n != o && !this._empty(n)) {
this.cSize = n
if (!this._empty(this.val)) {
setTimeout(() => {
this._makeCode()
}, 100);
}
}
},
val: function (n, o) {
if (this.onval) {
if (n != o && !this._empty(n)) {
setTimeout(() => {
this._makeCode()
}, 0);
}
}
}
},
computed: {
cpSize() {
if(this.unit == "upx"){
return uni.upx2px(this.size)
}else{
return this.size
}
}
},
mounted: function () {
if (this.loadMake) {
if (!this._empty(this.val)) {
setTimeout(() => {
this._makeCode()
}, 0);
}
}
},
}
</script>
<style>
.tki-qrcode {
position: relative;
}
.tki-qrcode-canvas {
position: fixed;
top: -99999upx;
left: -99999upx;
z-index: -99999;
}
</style>
+274
View File
@@ -0,0 +1,274 @@
<template>
<view class="content-two">
<!-- 顶部轮播图 + 返回按钮 -->
<view class="product_info_header_warp">
<up-swiper :list="swiperList" height="660rpx" class="product_info_header_swiper" @click="onSwiper"
@change="(e) => (currentNum = e.current)">
<template #indicator>
<view class="indicator-num">
<text class="indicator-num__text">{{ currentNum + 1 }}/{{ swiperList.length }}</text>
<qiaobao-assistant page-key="pages/active/group-buying/group-buying" title="超值拼团" />
</view>
</template>
</up-swiper>
<view class="img_comm left_warp">
<up-image src="/static/common/left_b_st.png" width="30" height="30" bgColor="#f1f6ff00"
@click="jumpLeft"></up-image>
</view>
</view>
<!-- 拼团商品列表 -->
<mescroll-uni ref="mescrollRef" top="660rpx" @down="downCallback" @up="upCallback" :up="upOption"
:down="downOption" @init="mescrollInit">
<view class="setting-view">
<view class="name-box">
<view class="mescrollUniView">
<productGroupCard v-for="item in dataList"
:key="item.activityGoodsId || item.goodsId || item.id" :item="item"
@onGroupBuy="onDetail" />
</view>
</view>
</view>
</mescroll-uni>
<up-toast ref="uToastRef"></up-toast>
</view>
</template>
<script>
import { getCarouselImage } from "@/api/common.js";
import { getGroupBuyList } from "@/api/product.js";
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
import MescrollUni from "@/components/mescroll-uni/mescroll-uni.vue";
import productGroupCard from "@/components/common/product-group-card.vue";
import { APP_PAGE_TYPE } from "@/utils/enumUtils.js";
export default {
name: "group-buying",
mixins: [MescrollMixin],
components: {
MescrollUni,
productGroupCard
},
data() {
return {
swiperList: [
"/pages/active/static/group_banner_cart.jpg"
],
currentNum: 0,
dataList: [],
activityInfo: {},
upOption: {
auto: false,
page: {
size: 10 // 每页数据的数量
},
noMoreSize: 5,
empty: {
tip: "暂无拼团商品"
},
textColor: "#333",
bgColor: "rgba(0,0,0,0)"
},
downOption: {
auto: false,
textColor: "#333",
bgColor: "rgba(0,0,0,0)"
},
mescroll: null,
activity: {},
fromBuyAlone: false
};
},
onLoad(options) {
if (options?.fromBuyAlone) {
this.fromBuyAlone = true;
}
this.getCarouselImage();
this.upCallback({ num: 1, size: 10 });
},
onBackPress() {
const pages = getCurrentPages();
if (this.fromBuyAlone || pages.length <= 1) {
this.fromBuyAlone = false;
uni.switchTab({
url: "/pages/home/home"
});
return true;
}
},
methods: {
// 查询轮播图
async getCarouselImage() {
const params = {
pageUi: APP_PAGE_TYPE.GROUP_BUY // 可配置为对应拼团页面类型
};
try {
const resp = await getCarouselImage(params);
if (resp && resp.bizcode === 100 && resp.data && resp.data.length > 0) {
this.swiperList = resp.data.map((obj) => obj.carouselImage);
}
} catch (e) {
console.log("getCarouselImage error:", e);
}
},
// 获取拼团商品列表
upCallback(page) {
let params = {
page: page.num,
pageSize: page.size
};
getGroupBuyList(params)
.then((res) => {
if (res && res.bizcode === 100 && res.data) {
this.activity = res.data.activity;
const pageData = res.data.page || {};
const entitys = pageData.entitys || [];
const curPageLen = entitys.length;
const totalCount = pageData.totalCount !== undefined ? pageData.totalCount : 0;
if (page.num === 1) {
this.dataList = [];
if (res.data.activity) {
this.activityInfo = res.data.activity;
}
}
this.dataList = this.dataList.concat(entitys);
this.mescroll && this.mescroll.endBySize(curPageLen, totalCount);
} else {
this.mescroll && this.mescroll.endErr();
}
})
.catch((err) => {
console.log("getGroupBuyList error:", err);
this.mescroll && this.mescroll.endErr();
});
},
onDetail(item) {
const goodsId = item.goodsId || item.id;
uni.navigateTo({
url: "/pages/other_package/productInfo/productInfo?id=" + goodsId + "&activedType=groupBuy" + "&activityId=" + this.activity.activityId
});
},
downCallback() {
this.mescroll && this.mescroll.resetUpScroll();
},
mescrollInit(mescroll) {
this.mescroll = mescroll;
},
jumpLeft() {
if (this.fromBuyAlone) {
uni.switchTab({
url: "/pages/home/home"
});
return;
}
const pages = getCurrentPages();
if (pages.length <= 1) {
uni.switchTab({
url: "/pages/home/home"
});
return;
}
uni.navigateBack({
delta: 1,
fail: () => {
uni.switchTab({
url: "/pages/home/home"
});
}
});
},
onSwiper(index) {
if (this.swiperList && this.swiperList.length > 0) {
uni.previewImage({
current: index,
loop: true,
urls: this.swiperList
});
}
}
}
};
</script>
<style lang="scss" scoped>
.content-two {
height: 100vh;
background-color: #fff;
position: relative;
overflow: hidden;
}
.product_info_header_warp {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 660rpx;
z-index: 10;
background-color: #fff;
.product_info_header_swiper {
height: 660rpx !important;
border-radius: 0 !important;
::v-deep(.u-swiper__wrapper) {
height: 660rpx !important;
.u-swiper__wrapper {
height: 660rpx !important;
.u-swiper__wrapper__item__wrapper__image {
height: 660rpx !important;
border-radius: 0 !important;
}
}
}
}
.img_comm {
position: absolute;
top: 140rpx;
}
.left_warp {
left: 40rpx;
}
.right_warp {
right: 40rpx;
}
}
.setting-view {
width: 100%;
padding: 32rpx 32rpx 20rpx;
box-sizing: border-box;
position: relative;
background-color: #fff;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
min-height: calc(100vh - 600rpx);
.name-box {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
.mescrollUniView {
width: 100%;
display: flex;
flex-direction: column;
}
}
}
</style>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

+21 -6
View File
@@ -379,9 +379,9 @@ export default {
// #ifdef MP
const hasSeenGuide = uni.getStorageSync("has_seen_home_guide");
if (hasSeenGuide) {
uni.showTabBar({ animation: false, fail: () => {} });
uni.showTabBar({ animation: false, fail: () => { } });
} else {
uni.hideTabBar({ animation: false, fail: () => {} });
uni.hideTabBar({ animation: false, fail: () => { } });
}
// #endif
this.refreshKefuUnread();
@@ -721,7 +721,9 @@ export default {
uni.getClipboardData({
success: function (res) {
// http://localhost:5173/#/pages/other_package/productInfo/productInfo?id=1038
console.log(res.data); // 剪贴板内容
console.log('剪贴板内容', res.data, res.data.indexOf(
`${SHARE_URL}/#/pages/other_package/productInfo/productInfo?id=`,
) != -1); // 剪贴板内容
if (
res.data.indexOf(
`${SHARE_URL}/#/pages/other_package/productInfo/productInfo?id=`,
@@ -732,12 +734,25 @@ export default {
content: "当前APP想要粘贴商品详情,是否继续?",
success: (res1) => {
if (res1.confirm) {
let id = res.data.match(/=(.*?)\s/);
_this.curId = Number(id[1]);
_this.getShop();
// https://h.o.tbmall.xin/#/pages/other_package/productInfo/productInfo?id=53641&activedType=groupBuy&activityId=1
const match = res.data.match(/[?&]id=([^& \s]+)/);
const id = match ? match[1] : "";
console.log("获取id", id, res.data.indexOf('activedType=groupBuy&activityId=1') != -1)
// 拼团
if (res.data.indexOf('activedType=groupBuy&activityId=1') != -1) {
uni.navigateTo({
url: `/pages/other_package/productInfo/productInfo?id=${id}&activedType=groupBuy&activityId=1`,
});
} else {
_this.curId = Number(id);
_this.getShop();
}
uni.setClipboardData({
data: "",
});
}
},
fail: (res2) => {
+43 -11
View File
@@ -167,7 +167,11 @@ export default {
});
// #endif
},
onLoad() {
onLoad(options) {
if (options && options.redirect) {
this.redirect = decodeURIComponent(options.redirect);
console.log("登录后重定向目标页面:", this.redirect);
}
// #ifdef APP-PLUS
// 判断微信是否安装 (iOS 的 scheme 为 weixin://)
this.isWechatInstalled = plus.runtime.isApplicationExist({
@@ -201,18 +205,48 @@ export default {
loading: false,
showPolicyModal: false,
isWechatInstalled: false,
redirect: "",
};
},
methods: {
onBack() {
console.log('onBack',)
uni.switchTab({
url: '/pages/home/home'
})
console.log('onBack')
const pages = getCurrentPages();
if (pages && pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url: '/pages/home/home'
});
}
},
jumpAfterLogin() {
if (this.redirect) {
uni.redirectTo({
url: this.redirect,
fail: () => {
uni.switchTab({
url: this.redirect,
fail: () => {
uni.switchTab({ url: "/pages/home/home" });
},
});
},
});
} else {
const pages = getCurrentPages();
if (pages && pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url: "/pages/home/home",
});
}
}
},
jumpHome() {
uni.switchTab({
url: "/pages/home/home",
});
@@ -271,8 +305,8 @@ export default {
});
setTimeout(() => {
then.jumpHome();
}, 2000);
then.jumpAfterLogin();
}, 1500);
},
checkNameParam(val) {
// console.log("val",val);
@@ -647,9 +681,7 @@ export default {
message: response.msg,
});
setTimeout(() => {
uni.switchTab({
url: "/pages/home/home",
});
this.jumpAfterLogin();
}, 1200);
} else if (response && response.msg) {
this.$refs.uToastRef.show({
+53 -5
View File
@@ -31,8 +31,9 @@
<view v-if="
item.status !== ORDER_TYPE.UNPAID &&
form.type !== MINE_ORDER_TYPE.AFTER_SALE
" class="order_item_header_right" :style="{ color: ORDER_STATUS_CSS[item.status] }">
{{ getValue(ORDER_STATUS, item.status) }}
" class="order_item_header_right"
:style="{ color: item.groupStatus === 0 ? '#999' : ORDER_STATUS_CSS[item.status] }">
{{ item.groupStatus === 0 ? '待成团' : getValue(ORDER_STATUS, item.status) }}
</view>
<!-- 售后显示订单状态 -->
<view v-if="
@@ -56,7 +57,11 @@
<view class="content_item" @click="jumpOrderInfo(item)">
<up-image :src="pItem.mainGraph" width="80" height="80" bgColor="#f1f6ff00"></up-image>
<view class="content_info">
<view class="content_info_title">{{ pItem.title }}</view>
<view class="content_info_title">
<image v-if="item.teamId" class="group_buy_tag"
src="https://static.tbmall.xin/static/mine/groupBuy.png" mode="heightFix"></image>
<text class="title_text">{{ pItem.title }}</text>
</view>
<view class="content_info_tag">{{ pItem.goodsSpece }}</view>
<!-- <view>
<text class="content_info_description" v-for="dItem in pItem.description">
@@ -103,8 +108,8 @@
+¥{{ item.sumAmount }}</view>
<view class="collect_msg" v-if="!item.buyDeductionValue">运费:¥{{ item.postage }},商品总价:¥{{ item.sumAmount }}
</view>
<view class="collect_msg" v-if="!item.buyDeductionValue">共{{ item.buyNum }}件,合计¥{{ item.sumAmount -
item.buyDeduction }}</view>
<view class="collect_msg" v-if="!item.buyDeductionValue">共{{ item.buyNum }}件,合计¥{{
calcOrderTotal(item.sumAmount, item.buyDeduction) }}</view>
<view class="collect_operate">
<!-- <view class="operate_comm operate_but_1" v-if="item.status === ORDER_TYPE.UNSHIPPED" @click="checkCancel(true)">取消订单</view> -->
<view class="operate_comm operate_but_2" v-if="
@@ -158,6 +163,13 @@
申请售后
</view>
<!-- 查看拼团按钮 (待发货 + 拼团商品 item.teamId) -->
<view class="operate_comm operate_but_2"
v-if="item.teamId && (item.status == 2 || item.status === ORDER_TYPE.UNSHIPPED)"
@click="jumpGroupDetail(item)">
查看拼团
</view>
<view class="operate_comm operate_but_1" v-if="form.type === MINE_ORDER_TYPE.AFTER_SALE"
@click="onAfterSaleDetail(item)">
售后详情
@@ -381,6 +393,7 @@ import { CUSTOMER_SERVICE_PHONE_NUMBER } from "@/utils/config.js";
import afterSales from "./after-sales/index";
import refundPopup from "@/pages/mine_package/mine_order/after-sales/refund-popup.vue";
import { RETURN_REASON_LIST } from "@/pages/mine_package/mine_order/emun/index.js";
import Decimal from "decimal.js";
export default {
components: { DownPopup, afterSales, refundPopup },
@@ -500,6 +513,19 @@ export default {
this.userinfo();
},
methods: {
calcOrderTotal(sumAmount, buyDeduction) {
try {
const sum = new Decimal(sumAmount || 0);
const deduction = new Decimal(buyDeduction || 0);
const total = sum.minus(deduction);
return (total.greaterThan(0) ? total : new Decimal(0)).toFixed(2);
} catch (e) {
const sum = Number(sumAmount) || 0;
const deduction = Number(buyDeduction) || 0;
const total = sum - deduction;
return (total > 0 ? total : 0).toFixed(2);
}
},
initMPHeader() {
// #ifdef MP-WEIXIN || MP
try {
@@ -788,6 +814,12 @@ export default {
"/pages/mine_package/mine_order_info/mine_order_info?id=" + item.id + '&type=' + this.form.type,
});
},
jumpGroupDetail(item) {
if (!item || !item.teamId) return;
uni.navigateTo({
url: `/pages/active/group-detail/group-detail?teamId=${item.teamId}`,
});
},
// 处理到期时间
handleExpressTime(expressTime) {
let resultM = 0;
@@ -1350,6 +1382,22 @@ export default {
.content_info_title {
width: 370rpx;
display: flex;
align-items: center;
.group_buy_tag {
height: 30rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
.title_text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.content_item_yd {
@@ -9,7 +9,7 @@
<up-image src="/pages/mine_package/static/order_status_1.png" width="20" height="19"
bgColor="#f1f6ff00"></up-image>
</view>
<text class="msg">{{ getValue(ORDER_STATUS, orderInfo.status) }}</text>
<text class="msg">{{ orderStatusText }}</text>
</view>
</view>
<view class="order_info_content">
@@ -78,7 +78,12 @@
<view class="content_item">
<up-image :src="pItem.mainGraph" width="80" height="80" bgColor="#f1f6ff00"></up-image>
<view class="content_info">
<view class="content_info_title">{{ pItem.title }}</view>
<view class="content_info_title">
<image v-if="orderInfo.teamId" class="group_buy_tag"
src="https://static.tbmall.xin/static/mine/groupBuy.png" mode="heightFix">
</image>
<text class="title_text">{{ pItem.title }}</text>
</view>
<view class="content_info_tag">{{ pItem.goodsSpece }}</view>
<view>
<text class="content_info_description" v-for="dItem in pItem.description"
@@ -362,6 +367,20 @@ export default {
},
LOGISTICS_STATUS_UPDATE() {
return LOGISTICS_STATUS_UPDATE
},
orderStatusText() {
if (this.orderInfo?.teamId) {
if (this.orderInfo.groupStatus == 0) {
return "待成团";
}
if (this.orderInfo.groupStatus == 2) {
return "拼团失败";
}
if (this.orderInfo.groupStatus == 1) {
return getValue(ORDER_STATUS, this.orderInfo.status);
}
}
return getValue(ORDER_STATUS, this.orderInfo?.status);
}
},
data() {
@@ -892,6 +911,22 @@ export default {
.content_info_title {
width: 370rpx;
display: flex;
align-items: center;
.group_buy_tag {
height: 30rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
.title_text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.data_item_yd {
@@ -60,7 +60,7 @@
<view class="content_right_total">
<view>¥<text style="font-size: 36rpx">{{
pItem.totalPrice
}}</text></view>
}}</text></view>
<view>每件到手价¥{{ pItem.unitPrice }}</view>
</view>
<view class="shopping_info_quantity">
@@ -84,7 +84,7 @@
<view class="content_right_total">
<view>¥<text style="font-size: 36rpx">{{
pItem.totalPrice
}}</text></view>
}}</text></view>
<view>每件到手价¥{{ pItem.unitPrice }}</view>
</view>
<view class="shopping_info_quantity">
@@ -108,7 +108,7 @@
<view class="order_freight_item">
<view>运费<text style="color: #c0c3c6; margin-left: 20rpx">{{
getValue(PRODUCT_DELIVERY_TIME, addItem.deliveryTime)
}}</text></view>
}}</text></view>
<view class="value_">
{{ orderInfo.postage ? "¥" + orderInfo.postage : "包邮" }}
</view>
@@ -351,7 +351,8 @@ import {
settle,
settlementOrder,
updateGoodsNum,
setSeckillOrder
setSeckillOrder,
setGroupBuyOrder
} from "@/api/cart.js";
import { getSupportPay } from "@/api/order.js";
import { assembleAddress } from "@/utils/index.js";
@@ -365,6 +366,7 @@ import {
} from "@/utils/payUtils.js";
import CryptoJS from "crypto-js";
import Decimal from "decimal.js";
import CouponDialog from "./coupon-dialog.vue";
export default {
@@ -478,6 +480,16 @@ export default {
this.sed = JSON.parse(option.sed);
this.leftPathType = "navigateTo";
this.leftPath = null;
if (this.sed?.activedType == 'groupBuy') {
const goodsId = this.sed.goodsId || this.sed.id;
const activityId = this.sed.activityId || 1;
let path = `/pages/other_package/productInfo/productInfo?id=${goodsId}&activedType=groupBuy&activityId=${activityId}`;
if (this.sed.teamId) {
path += `&teamId=${this.sed.teamId}`;
}
this.leftPath = path;
}
await this.addressList();
// console.log('addressListOne',this.addressListOne)
// if(this.addressListOne && this.addressListOne.length){
@@ -497,15 +509,36 @@ export default {
if (addressShowPop && addressShowPop !== "null") {
this.addressShow = addressShowPop;
}
// 支付弹框
// const payOpen = option.payOpen;
// if (payOpen && payOpen !== "null") {
// this.getWayOption();
// this.wayShow = payOpen;
// }
this.getWayOption();
this.getUserInfo();
},
onBackPress(options) {
if (this.sed?.activedType == 'groupBuy') {
const goodsId = this.sed.goodsId || this.sed.id;
const activityId = this.sed.activityId || 1;
let path = `/pages/other_package/productInfo/productInfo?id=${goodsId}&activedType=groupBuy&activityId=${activityId}`;
if (this.sed.teamId) {
path += `&teamId=${this.sed.teamId}`;
}
const pages = getCurrentPages();
if (pages && pages.length > 1) {
const prevPage = pages[pages.length - 2];
const route = prevPage ? (prevPage.route || (prevPage.$page && prevPage.$page.route)) : '';
if (route && path.includes(route)) {
return false;
}
}
uni.redirectTo({
url: path,
fail: () => {
uni.navigateTo({ url: path });
}
});
return true;
}
return false;
},
mounted() { },
methods: {
getValue,
@@ -727,15 +760,30 @@ export default {
params.activityId = Number(this.sed?.activityId);
params.activityType = "seckill";
}
if (this.sed?.activedType === "groupBuy") {
params.activityId = Number(this.sed?.activityId);
params.type = 5
}
const resp = await getGoodsDetail(params);
if (resp && resp.bizcode === 100) {
const data = resp.data;
this.isPoints = data.type == 3 ? 1 : 0;
console.log("秦星星data", data);
console.log("秦星星data", data, this.sed);
const goodsNum = this.sed?.goodsNum || 1;
const unitPrice =
this.sed?.activedType == "seckill"
? (data.activity?.activityPrice ?? 0)
: (this.sed?.specsId?.price ?? 0);
const amount = new Decimal(goodsNum).times(new Decimal(unitPrice || 0)).toFixed(2);
this.orderInfo = {
address: null,
amount: this.sed?.activedType == "seckill" ? data.activity?.activityPrice : this.sed.goodsNum * this.sed.specsId.price || 0,
amount: amount,
postage: 0,
expendNum: data.expendNum,
groupPrice: data.groupPrice, //拼团标识
shops: [
{
addrTimes: [
@@ -756,13 +804,13 @@ export default {
id: this.sed.specsId.id,
img: data.goodsGraph.split(",")[0],
name: null,
num: this.sed.goodsNum,
num: goodsNum,
phone: null,
provinceName: null,
specs: this.sed.specsId.combNames,
title: data.name,
totalPrice: this.sed.goodsNum * this.sed.specsId.price,
unitPrice: this.sed.specsId.price,
totalPrice: amount,
unitPrice: unitPrice,
},
],
},
@@ -777,6 +825,7 @@ export default {
"this.defaultAddress",
this.defaultAddress,
this.addressListOne,
this.orderInfo
);
if (
this.defaultAddress &&
@@ -805,28 +854,7 @@ export default {
if (!this.isCoupon && this.sed?.activedType !== "seckill") {
await this.refreshDirectSettle();
}
// if(this.addressListOne && this.addressListOne.length){
// this.orderInfo.address = this.addressListOne[0];
// }
}
// const params = {
// cartIds: ids,
// }
// const resp = await getSettleList(params);
// if (resp && resp.bizcode === 100) {
// const data = resp.data;
// const shops = data.shops;
// if (shops && shops.length !== 0) {
// shops.map(item => {
// item.addrTimes.map(addItem => {
// addItem.remark = null;
// })
// })
// }
// data.shops = shops;
// data.paymentMethod = null;// 支付方式
// this.orderInfo = data;
// }
},
/**
* 数量进行增加删除
@@ -1028,10 +1056,8 @@ export default {
// #endif
}
console.log("收货地址ID--params", params);
let resp = null;
console.log("orderInfo--22--", this.sed, this.orderInfo);
console.log("收货地址ID--params", params, this.sed, this.orderInfo);
if (this.sed) {
if (
this.orderInfo.paymentMethod == "balance" ||
@@ -1044,11 +1070,12 @@ export default {
params["specsId"] = this.sed.specsId.id;
params["goodsNum"] = this.orderInfo.shops[0].addrTimes[0].goods[0].num;
console.log("秦星星params-----", this.sed?.activedType);
if (this.sed?.activedType == "seckill") {
params["activityId"] = Number(this.sed?.activityId); // 秒杀活动ID
resp = await setSeckillOrder(params);
} else if (this.orderInfo.groupPrice && this.sed?.activedType == 'groupBuy') {
params["activityId"] = Number(this.sed?.activityId); // 秒杀活动ID
resp = await setGroupBuyOrder(params);
} else {
if (this.isCoupon) {
params.payway2 = "vcoin";
@@ -1095,22 +1122,37 @@ export default {
}
if (resp && resp.bizcode === 100) {
console.log("提交订单成功", resp.data);
const item = resp.data;
console.log("提交订单成功", resp.data, resp.data.teamId);
const isGroupBuy = Boolean(
this.orderInfo.groupPrice && this.sed?.activedType == "groupBuy"
);
const item = isGroupBuy ? resp.data?.payment : resp.data;
const groupDetailUrl = isGroupBuy
? `/pages/active/group-detail/group-detail?teamId=${resp.data.teamId}`
: null;
if (item.status === 1) {
if (isGroupBuy) {
uni.redirectTo({
url: groupDetailUrl,
});
return;
}
jumpWayOk();
return;
}
if (item.douyin) {
if (groupDetailUrl) item.customSuccessUrl = groupDetailUrl;
await appDypayFun(item);
} else if (item.wechat && Object.keys(item.wechat).length !== 0) {
this.appWxpay(item.wechat, item.orderId, item.orderNum, item);
this.appWxpay(item.wechat, item.orderId, item.orderNum, item, groupDetailUrl);
} else if (
item.alipay &&
Object.keys(item.alipay).length !== 0 &&
item.alipay
) {
this.zfbPay(item.alipay, item.orderId, item.orderNum, item);
this.zfbPay(item.alipay, item.orderId, item.orderNum, item, groupDetailUrl);
} else {
this.$refs.uToastRef.show({
type: "error",
@@ -1128,9 +1170,9 @@ export default {
} finally { this._paySubmitting = false; }
},
// app 微信支付
async appWxpay(alipay, orderId, orderNum, item) {
async appWxpay(alipay, orderId, orderNum, item, customSuccessUrl) {
try {
const result = await appWxpayFun(alipay, orderId, orderNum, item);
const result = await appWxpayFun(alipay, orderId, orderNum, customSuccessUrl);
console.log("支付成功", result);
// 处理支付成功后的逻辑,如更新订单状态等
} catch (error) {
@@ -1139,9 +1181,9 @@ export default {
}
},
// 支付宝支付
async zfbPay(alipay, orderId, orderNum, item) {
async zfbPay(alipay, orderId, orderNum, item, customSuccessUrl) {
try {
const result = await zfbPayFun(alipay, orderId, orderNum, item);
const result = await zfbPayFun(alipay, orderId, orderNum, customSuccessUrl);
console.log("支付成功", result);
// 处理支付成功后的逻辑,如更新订单状态等
} catch (error) {
@@ -1227,7 +1269,7 @@ export default {
this.selectedCoupon = coupon;
},
async refreshDirectSettle(couponUserId) {
if (!this.sed || !this.sed.specsId) return false;
if (!this.sed || !this.sed.specsId || !couponUserId) return false;
const resp = await getDirectSettleList({
goodsId: this.sed.id,
specsId: this.sed.specsId.id,
+746 -18
View File
@@ -35,7 +35,7 @@
</view>
<view class="share-line" style="margin-top:40rpx;"></view>
<view class="qr-view" v-if="qrvalue">
<view class="qr-view" v-if="qrvalue && !isGroupBuy">
<tki-qrcode ref="qrcode" :val="qrvalue" :size="252" />
</view>
@@ -45,7 +45,57 @@
<text class="text-32 text-fu1">取消</text>
</view>
<view class="share-top-image-view">
<!-- 拼团专属卡片 -->
<view class="group-share-card" v-if="isGroupBuy">
<image :src="tu" class="group-card-img" mode="aspectFill"></image>
<view class="group-card-price-row">
<view class="group-card-price-box">
<text class="group-card-price-label">拼团价</text>
<text class="group-card-price-symbol">¥</text>
<text class="group-card-price-num">{{ groupPrice || price }}</text>
</view>
<view class="group-card-tag" v-if="requiredNum">
<text>{{ requiredNum }}人成团</text>
</view>
</view>
<view class="group-card-title">
{{ text }}
</view>
<view class="group-card-divider"></view>
<view class="group-card-bottom">
<view class="group-card-bottom-left">
<view class="group-card-main-title">超值拼团</view>
<view class="group-card-sub-title">人多价更优 拼团更实惠</view>
<view class="group-card-user-row">
<image :src="currentInviterAvatar" class="group-card-avatar" mode="aspectFill"></image>
<text class="group-card-username">{{ currentInviterName }}</text>
</view>
</view>
<view class="group-card-bottom-right">
<view class="group-card-qr-box">
<tki-qrcode
ref="groupQrcode"
cid="tki-group-qrcode-canvas"
:val="qrvalue"
:size="260"
unit="px"
:onval="true"
:lv="2"
:showLoading="false"
@result="onGroupQrResult"
/>
</view>
<text class="group-card-qr-tip">长按识别 参与拼团</text>
</view>
</view>
</view>
<!-- 原普通商品卡片 -->
<view class="share-top-image-view" v-else>
<image :src="tu" class="tu-img"></image>
<view class="image-view-bottom">
<view class="image-view-bottom-left"
@@ -57,6 +107,15 @@
</view>
</view>
</view>
<!-- 隐藏 Canvas 用于生成拼团卡片海报 -->
<view class="hideCanvasView">
<canvas
canvas-id="groupCardCanvas"
id="groupCardCanvas"
style="width: 560px; height: 910px;"
></canvas>
</view>
</view>
</up-popup>
</template>
@@ -65,13 +124,16 @@
import Func from '/utils/func'
import tkiQrcode from './tki-qrcode/tki-qrcode.vue'
import { SHARE_URL } from '@/utils/config.js'
import { getStorageFun, USER_DATA } from '@/utils/auth.js'
export default {
data() {
return {
btnList: ['微信', '朋友圈'],
appBtnList: ['微信', '朋友圈', '复制链接', '保存图片', 'QQ'],
qrvalue: ''
qrvalue: '',
defaultAvatar: 'https://static.tbmall.xin/static/new/default_avatar.png',
groupQrPath: '',
isGeneratingPoster: false
}
},
@@ -82,7 +144,7 @@ export default {
default: false
},
id: {
type: String,
type: [String, Number],
default: ''
},
tu: {
@@ -100,21 +162,50 @@ export default {
hideCopyAndQQ: {
type: Boolean,
default: false
},
sharePath: {
type: String,
default: ''
},
isGroupBuy: {
type: Boolean,
default: false
},
groupPrice: {
type: [String, Number],
default: ''
},
requiredNum: {
type: [String, Number],
default: 2
},
inviterName: {
type: String,
default: ''
},
inviterAvatar: {
type: String,
default: ''
}
},
watch: {
show(val) {
if (val) {
this.initQrCode();
}
},
sharePath() {
this.initQrCode();
},
id() {
this.initQrCode();
},
isGroupBuy() {
this.initQrCode();
}
},
mounted() {
// #ifdef APP-PLUS || H5
if (!this.isInvitePage) {
this.qrvalue = `${SHARE_URL}/#/pages/other_package/productInfo/productInfo?id=${this.id}`
this.$nextTick(() => {
if (this.qrvalue) {
this.creatQrcode();
}
})
}
// #endif
this.initQrCode();
},
computed: {
@@ -134,6 +225,9 @@ export default {
if (this.isInvitePage) {
return items.filter(item => item.name !== '微信');
}
if (this.isGroupBuy) {
return items.filter(item => item.name !== 'QQ');
}
return items;
},
filteredAppBtnList() {
@@ -141,14 +235,79 @@ export default {
if (this.isInvitePage) {
return items.filter(item => item.name !== '复制链接' && item.name !== 'QQ');
}
if (this.isGroupBuy) {
return items.filter(item => item.name !== 'QQ');
}
return items;
},
currentInviterName() {
if (this.inviterName) return this.inviterName;
try {
const u = getStorageFun(USER_DATA) || {};
return u.name || u.nickName || 'Haonan Mu';
} catch (e) {
return 'Haonan Mu';
}
},
currentInviterAvatar() {
if (this.inviterAvatar) return this.inviterAvatar;
try {
const u = getStorageFun(USER_DATA) || {};
return u.avatar || u.avatarUrl || this.defaultAvatar;
} catch (e) {
return this.defaultAvatar;
}
}
},
methods: {
initQrCode() {
if (!this.isInvitePage) {
this.groupQrPath = '';
if (this.isGroupBuy) {
// 拼团专属卡片二维码:路由后面加 id=${this.id}&activedType=groupBuy&activityId=1
let basePath = 'pages/other_package/productInfo/productInfo';
let targetId = this.id != null ? String(this.id) : '';
if (this.sharePath) {
const cleanPath = this.sharePath.startsWith('/') ? this.sharePath.slice(1) : this.sharePath;
const pathOnly = cleanPath.split('?')[0];
if (pathOnly) {
basePath = pathOnly;
}
if (!targetId) {
const idMatch = cleanPath.match(/[?&]id=([^&]+)/);
if (idMatch) {
targetId = idMatch[1];
}
}
}
// 拼团专属卡片二维码:扫码后直接打开 H5 对应商品详情页,路由后面加 id=${this.id}&activedType=groupBuy&activityId=1
this.qrvalue = `${SHARE_URL}/#/${basePath}?id=${targetId}&activedType=groupBuy&activityId=1`;
} else if (this.sharePath) {
const cleanPath = this.sharePath.startsWith('/') ? this.sharePath.slice(1) : this.sharePath;
this.qrvalue = `${SHARE_URL}/#/${cleanPath}`;
} else if (this.id) {
this.qrvalue = `${SHARE_URL}/#/pages/other_package/productInfo/productInfo?id=${this.id}`;
}
if (this.qrvalue) {
this.$nextTick(() => {
setTimeout(() => {
if (this.$refs.qrcode && this.$refs.qrcode._makeCode) {
this.$refs.qrcode._makeCode();
}
if (this.$refs.groupQrcode && this.$refs.groupQrcode._makeCode) {
this.$refs.groupQrcode._makeCode();
}
}, 50);
});
}
}
},
creatQrcode() {
this.$refs.qrcode._makeCode();
if (this.$refs.qrcode && this.$refs.qrcode._makeCode) {
this.$refs.qrcode._makeCode();
}
},
onBtn(index) {
if (index == 2) {
@@ -197,10 +356,395 @@ export default {
Func.myToast('复制成功')
})
// #endif
} else if (index == 3) {
if (this.isGroupBuy) {
this.saveGroupCard();
return;
}
}
this.$emit('share', index)
},
onGroupQrResult(res) {
this.groupQrPath = res;
},
getLocalImage(url) {
return new Promise((resolve) => {
if (!url) {
resolve('');
return;
}
if (
url.startsWith('file://') ||
url.startsWith('_doc') ||
url.startsWith('/storage') ||
url.startsWith('blob:') ||
url.startsWith('data:')
) {
resolve(url);
return;
}
uni.downloadFile({
url: url,
success: (res) => {
if (res.statusCode === 200 && res.tempFilePath) {
resolve(res.tempFilePath);
} else {
uni.getImageInfo({
src: url,
success: (info) => resolve(info.path),
fail: () => resolve('')
});
}
},
fail: () => {
uni.getImageInfo({
src: url,
success: (info) => resolve(info.path),
fail: () => resolve('')
});
}
});
});
},
base64ToTempFile(base64Data) {
return new Promise((resolve) => {
if (!base64Data || !base64Data.startsWith('data:image')) {
resolve(base64Data);
return;
}
// #ifdef APP-PLUS
try {
const bitmap = new plus.nativeObj.Bitmap('qr_' + Date.now());
bitmap.loadBase64Data(
base64Data,
() => {
const tempPath = `_doc/qr_${Date.now()}.png`;
bitmap.save(
tempPath,
{ overwrite: true, format: 'png', quality: 100 },
(e) => {
bitmap.clear();
resolve(e.target);
},
() => {
bitmap.clear();
resolve(base64Data);
}
);
},
() => {
bitmap.clear();
resolve(base64Data);
}
);
} catch (e) {
resolve(base64Data);
}
// #endif
// #ifndef APP-PLUS
resolve(base64Data);
// #endif
});
},
drawRoundedRect(ctx, x, y, width, height, radius, fill, stroke) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.arcTo(x + width, y, x + width, y + radius, radius);
ctx.lineTo(x + width, y + height - radius);
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
ctx.lineTo(x + radius, y + height);
ctx.arcTo(x, y + height, x, y + height - radius, radius);
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.closePath();
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
},
drawWrapText(ctx, text, x, y, maxWidth, lineHeight, maxLines = 2) {
if (!text) return y;
const chars = String(text).split('');
let line = '';
let currentLine = 1;
let currentY = y;
for (let n = 0; n < chars.length; n++) {
const testLine = line + chars[n];
const metrics = ctx.measureText ? ctx.measureText(testLine) : { width: testLine.length * 14 };
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
if (currentLine === maxLines) {
let ellipsisLine = line;
while (ellipsisLine.length > 0) {
const lineWithEllipsis = ellipsisLine + '...';
const m = ctx.measureText ? ctx.measureText(lineWithEllipsis) : { width: lineWithEllipsis.length * 14 };
if (m.width <= maxWidth) {
ctx.fillText(lineWithEllipsis, x, currentY);
break;
}
ellipsisLine = ellipsisLine.slice(0, -1);
}
return currentY + lineHeight;
}
ctx.fillText(line, x, currentY);
line = chars[n];
currentY += lineHeight;
currentLine++;
} else {
line = testLine;
}
}
ctx.fillText(line, x, currentY);
return currentY + lineHeight;
},
async saveGroupCard() {
if (this.isGeneratingPoster) return;
this.isGeneratingPoster = true;
uni.showLoading({ title: '海报生成中...', mask: true });
try {
let localTu = '';
if (this.tu) {
localTu = await this.getLocalImage(this.tu);
}
const avatarUrl = this.currentInviterAvatar;
let localAvatar = '';
if (avatarUrl) {
localAvatar = await this.getLocalImage(avatarUrl);
}
let qrImg = this.groupQrPath || (this.$refs.groupQrcode && this.$refs.groupQrcode.result);
if (!qrImg && this.$refs.groupQrcode) {
if (this.$refs.groupQrcode._makeCode) {
this.$refs.groupQrcode._makeCode();
}
await new Promise((r) => setTimeout(r, 450));
qrImg = this.groupQrPath || (this.$refs.groupQrcode && this.$refs.groupQrcode.result);
}
if (!qrImg) {
await new Promise((r) => setTimeout(r, 300));
qrImg = this.groupQrPath || (this.$refs.groupQrcode && this.$refs.groupQrcode.result);
}
if (qrImg && qrImg.startsWith('data:image')) {
qrImg = await this.base64ToTempFile(qrImg);
}
const ctx = uni.createCanvasContext('groupCardCanvas', this);
const W = 560;
const H = 910;
const pad = 24;
const innerW = W - pad * 2; // 512
// 1. 卡片背景
ctx.fillStyle = '#FFFFFF';
this.drawRoundedRect(ctx, 0, 0, W, H, 24, true, false);
// 2. 商品主图
if (localTu) {
ctx.save();
this.drawRoundedRect(ctx, pad, pad, innerW, 500, 16, false, false);
ctx.clip();
ctx.drawImage(localTu, pad, pad, innerW, 500);
ctx.restore();
} else {
ctx.fillStyle = '#F7F7F7';
this.drawRoundedRect(ctx, pad, pad, innerW, 500, 16, true, false);
}
// 3. 价格行
const priceRowY = pad + 500 + 20; // 544
ctx.font = 'normal 500 24px sans-serif';
ctx.fillStyle = '#7F3BF5';
ctx.textBaseline = 'middle';
ctx.fillText('拼团价', pad, priceRowY + 18);
const labelWidth = ctx.measureText ? ctx.measureText('拼团价').width : 72;
ctx.font = 'bold 22px sans-serif';
ctx.fillStyle = '#7F3BF5';
const symbolX = pad + labelWidth + 6;
ctx.fillText('¥', symbolX, priceRowY + 19);
const symbolWidth = ctx.measureText ? ctx.measureText('¥').width : 16;
const displayPrice = String(this.groupPrice || this.price || '0');
ctx.font = 'bold 42px sans-serif';
ctx.fillStyle = '#7F3BF5';
const numX = symbolX + symbolWidth + 4;
ctx.fillText(displayPrice, numX, priceRowY + 16);
if (this.requiredNum) {
const tagText = `${this.requiredNum}人成团`;
ctx.font = 'normal 500 20px sans-serif';
const tagTextWidth = ctx.measureText ? ctx.measureText(tagText).width : 74;
const tagW = tagTextWidth + 24;
const tagH = 34;
const tagX = W - pad - tagW;
const tagY = priceRowY + 2;
ctx.fillStyle = '#F3EBFE';
this.drawRoundedRect(ctx, tagX, tagY, tagW, tagH, 8, true, false);
ctx.fillStyle = '#7F3BF5';
ctx.textBaseline = 'middle';
ctx.fillText(tagText, tagX + 12, tagY + tagH / 2);
}
// 4. 商品名称 (最多2行,溢出显示省略号)
const titleY = priceRowY + 54;
ctx.fillStyle = '#222222';
ctx.font = 'bold 26px sans-serif';
ctx.textBaseline = 'top';
const nextY = this.drawWrapText(ctx, this.text || '', pad, titleY, innerW, 36, 2);
// 5. 分割线
const dividerY = Math.max(nextY + 12, titleY + 76);
ctx.strokeStyle = '#F0F0F0';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad, dividerY);
ctx.lineTo(W - pad, dividerY);
ctx.stroke();
// 6. 底部区域
const bottomY = dividerY + 18;
ctx.fillStyle = '#1E1E1E';
ctx.font = 'bold 30px sans-serif';
ctx.textBaseline = 'top';
ctx.fillText('超值拼团', pad, bottomY);
ctx.fillStyle = '#999999';
ctx.font = 'normal 20px sans-serif';
ctx.fillText('人多价更优 拼团更实惠', pad, bottomY + 38);
const userRowY = bottomY + 74;
const avatarSize = 44;
if (localAvatar) {
ctx.save();
ctx.beginPath();
ctx.arc(pad + avatarSize / 2, userRowY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI);
ctx.closePath();
ctx.clip();
ctx.drawImage(localAvatar, pad, userRowY, avatarSize, avatarSize);
ctx.restore();
} else {
ctx.fillStyle = '#EEEEEE';
ctx.beginPath();
ctx.arc(pad + avatarSize / 2, userRowY + avatarSize / 2, avatarSize / 2, 0, 2 * Math.PI);
ctx.fill();
}
ctx.fillStyle = '#333333';
ctx.font = 'normal 500 22px sans-serif';
ctx.textBaseline = 'middle';
const username = this.currentInviterName;
ctx.fillText(username, pad + avatarSize + 12, userRowY + avatarSize / 2);
// 二维码绘制(带纯白底色与四周静区 Padding,确保微信扫码/长按识别成功率)
const qrBoxSize = 144;
const qrInnerPad = 10;
const qrSize = qrBoxSize - qrInnerPad * 2; // 124px
const qrBoxX = W - pad - qrBoxSize;
const qrBoxY = bottomY;
// 绘制白色背景底卡
ctx.fillStyle = '#FFFFFF';
this.drawRoundedRect(ctx, qrBoxX, qrBoxY, qrBoxSize, qrBoxSize, 8, true, false);
if (qrImg) {
ctx.drawImage(qrImg, qrBoxX + qrInnerPad, qrBoxY + qrInnerPad, qrSize, qrSize);
}
ctx.fillStyle = '#999999';
ctx.font = 'normal 17px sans-serif';
ctx.textBaseline = 'top';
const tipText = '长按识别 参与拼团';
const tipWidth = ctx.measureText ? ctx.measureText(tipText).width : 120;
const tipX = qrBoxX + (qrBoxSize - tipWidth) / 2;
ctx.fillText(tipText, tipX, qrBoxY + qrBoxSize + 8);
// 7. 提交绘制
await new Promise((resolve) => {
ctx.draw(false, () => {
setTimeout(resolve, 200);
});
});
// 8. 导出临时图片
const tempFilePath = await new Promise((resolve, reject) => {
uni.canvasToTempFilePath(
{
canvasId: 'groupCardCanvas',
fileType: 'png',
width: W,
height: H,
destWidth: W * 2,
destHeight: H * 2,
quality: 1,
success: (res) => resolve(res.tempFilePath),
fail: (err) => reject(err)
},
this
);
});
uni.hideLoading();
// 9. 保存至相册
// #ifdef APP-PLUS || MP-WEIXIN
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
uni.showToast({
title: '已保存至手机相册',
icon: 'success',
duration: 2000
});
this.onClose();
},
fail: (err) => {
console.error('保存相册失败:', err);
if (err && err.errMsg && (err.errMsg.includes('auth') || err.errMsg.includes('authorize'))) {
uni.showModal({
title: '提示',
content: '需要保存图片到相册权限,请在系统设置中开启相册权限',
success: (res) => {
if (res.confirm) uni.openSetting();
}
});
} else {
uni.previewImage({ urls: [tempFilePath] });
}
}
});
// #endif
// #ifdef H5
uni.previewImage({ urls: [tempFilePath] });
uni.showToast({ title: '请长按图片保存到手机相册', icon: 'none' });
this.onClose();
// #endif
} catch (err) {
uni.hideLoading();
console.error('生成或保存海报失败:', err);
uni.showToast({
title: '生成海报失败,请重试',
icon: 'none'
});
} finally {
this.isGeneratingPoster = false;
}
},
onClose() {
this.$emit('close');
},
@@ -215,6 +759,181 @@ export default {
</script>
<style lang="scss" scoped>
.group-share-card {
position: absolute;
bottom: 100%;
margin-bottom: 24rpx;
left: 50%;
transform: translateX(-50%);
width: 560rpx;
background-color: #ffffff;
border-radius: 24rpx;
padding: 24rpx;
box-sizing: border-box;
box-shadow: 0 10rpx 36rpx rgba(0, 0, 0, 0.16);
z-index: 10;
.group-card-img {
width: 100%;
height: 500rpx;
border-radius: 16rpx;
background-color: #f7f7f7;
display: block;
}
.group-card-price-row {
margin-top: 20rpx;
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
.group-card-price-box {
display: flex;
flex-direction: row;
align-items: baseline;
.group-card-price-label {
font-size: 26rpx;
color: #7f3bf5;
font-weight: 500;
}
.group-card-price-symbol {
font-size: 24rpx;
color: #7f3bf5;
font-weight: bold;
margin-left: 6rpx;
margin-right: 4rpx;
}
.group-card-price-num {
font-size: 42rpx;
color: #7f3bf5;
font-weight: bold;
line-height: 1;
}
}
.group-card-tag {
background-color: #f3ebfe;
color: #7f3bf5;
font-size: 22rpx;
font-weight: 500;
padding: 6rpx 14rpx;
border-radius: 8rpx;
line-height: 1;
}
}
.group-card-title {
margin-top: 14rpx;
font-size: 28rpx;
font-weight: 500;
color: #222222;
line-height: 38rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.group-card-divider {
width: 100%;
height: 1rpx;
background-color: #f0f0f0;
margin: 20rpx 0 16rpx 0;
}
.group-card-bottom {
display: flex;
flex-direction: row;
align-items: flex-end;
justify-content: space-between;
.group-card-bottom-left {
flex: 1;
padding-right: 16rpx;
.group-card-main-title {
font-size: 32rpx;
font-weight: bold;
color: #1e1e1e;
line-height: 1.2;
}
.group-card-sub-title {
font-size: 22rpx;
color: #999999;
margin-top: 6rpx;
line-height: 1.2;
}
.group-card-user-row {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 20rpx;
.group-card-avatar {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background-color: #f0f0f0;
flex-shrink: 0;
}
.group-card-username {
font-size: 24rpx;
color: #333333;
font-weight: 500;
margin-left: 12rpx;
max-width: 200rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.group-card-bottom-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
.group-card-qr-box {
width: 140rpx;
height: 140rpx;
padding: 8rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
border-radius: 8rpx;
overflow: hidden;
::v-deep .tki-qrcode,
::v-deep .tki-qrcode image {
width: 124rpx !important;
height: 124rpx !important;
display: block;
}
}
.group-card-qr-tip {
font-size: 18rpx;
color: #999999;
margin-top: 6rpx;
white-space: nowrap;
}
}
}
}
.share-top-image-view {
width: 400rpx;
min-height: 496rpx;
@@ -361,4 +1080,13 @@ export default {
}
}
.hideCanvasView {
position: fixed;
top: 10000px;
left: 10000px;
width: 560px;
height: 910px;
z-index: -9999;
}
</style>
@@ -0,0 +1,151 @@
<template>
<!-- 贡献值说明弹窗 -->
<up-popup :show="show" mode="bottom" :round="16" :closeOnClickOverlay="true"
@close="$emit('update:show', false)">
<view class="contribution_popup_container">
<!-- 头部 -->
<view class="contribution_popup_header">
<text class="contribution_header_title">贡献值说明</text>
<view class="contribution_header_close" @click="$emit('update:show', false)">
<u-icon name="close" size="18" color="#71737c"></u-icon>
</view>
</view>
<!-- 内容主体 -->
<scroll-view scroll-y class="contribution_popup_body">
<view class="contribution_section">
<view class="contribution_section_title">我能获得多少贡献值?</view>
<view class="contribution_text">1.商品详情页中展示为预估获得贡献值。</view>
<view class="contribution_text">2.实际赠送的贡献值将根据订单实付金额及商品贡献系数综合计算,其中商品贡献系数由商品毛利等因素确定。</view>
<view class="contribution_text">3.使用优惠券、发生退款或售后时,贡献值可能相应调整,最终以订单完成后的实际到账为准。</view>
</view>
<view class="contribution_section">
<view class="contribution_section_title">生效条件</view>
<view class="contribution_text">1.收到商品后,在订单中点击【确认收货】时,立马到账。可在【我的-钱包】贡献值进行查看</view>
<view class="contribution_text">2.消费者权益次日到账,可在【我的-钱包】数字积分-消费者权益中进行查看</view>
</view>
<view class="contribution_section">
<view class="contribution_section_title">售后调整</view>
<view class="contribution_text">1.如发生退货/退款,所有的贡献值、消费者权益将会进行扣除。</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="contribution_popup_footer">
<view class="contribution_btn_confirm" @click="$emit('update:show', false)">
我知道了
</view>
</view>
</view>
</up-popup>
</template>
<script>
export default {
name: "product-contribution-dialog",
props: {
show: {
type: Boolean,
default: false,
},
},
};
</script>
<style lang="scss" scoped>
.contribution_popup_container {
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
max-height: 80vh;
box-sizing: border-box;
.contribution_popup_header {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 32rpx 32rpx 16rpx;
.contribution_header_title {
font-weight: 500;
font-size: 32rpx;
color: #71737c;
}
.contribution_header_close {
position: absolute;
right: 32rpx;
top: 28rpx;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
}
.contribution_popup_body {
max-height: 60vh;
padding: 10rpx 36rpx 20rpx;
box-sizing: border-box;
.contribution_section {
margin-bottom: 28rpx;
&:last-child {
margin-bottom: 10rpx;
}
.contribution_section_title {
font-size: 34rpx;
font-weight: bold;
color: #111111;
margin-bottom: 20rpx;
line-height: 1.4;
}
.contribution_text {
font-size: 28rpx;
line-height: 1.75;
color: #666666;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
}
}
.contribution_popup_footer {
padding: 20rpx 36rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
background: #ffffff;
box-sizing: border-box;
.contribution_btn_confirm {
width: 100%;
height: 88rpx;
background: #7934f6;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #ffffff;
font-weight: 500;
cursor: pointer;
transition: opacity 0.15s ease;
&:active {
opacity: 0.85;
}
}
}
}
</style>
@@ -0,0 +1,420 @@
<template>
<up-popup :show="show" mode="bottom" :round="16" :closeable="true" :safeAreaInsetBottom="true"
@close="$emit('update:show', false)">
<view class="goods_coupon_popup_container">
<view class="popup_header">
<text class="popup_title">优惠券</text>
</view>
<!-- 优惠券列表 (可滑动) -->
<scroll-view scroll-y class="popup_coupon_scroll">
<view class="popup_coupon_list">
<view v-for="(coupon, index) in list" :key="coupon.templateId || coupon.id || index"
class="coupon-card" :class="{ 'card-disabled': getCardDisabled(coupon) }">
<!-- 左侧:金额与门槛 -->
<view class="card-left">
<view class="price-box">
<text class="currency">¥</text>
<text class="amount">{{ getDiscountAmount(coupon) }}</text>
</view>
<text class="condition">{{ getThresholdText(coupon) }}</text>
</view>
<!-- 带有上下半圆凹槽的虚线分割线 -->
<view class="divider-wrapper">
<view class="notch top-notch"></view>
<view class="divider-line"></view>
<view class="notch bottom-notch"></view>
</view>
<!-- 右侧:详细内容与操作按钮 -->
<view class="card-right">
<view class="info-content">
<view class="coupon-title">{{ coupon.title }}</view>
<view class="tag-row">
<text class="coupon-tag" :class="{ 'tag-disabled': getCardDisabled(coupon) }">
{{ getTagText(coupon) }}
</text>
<text class="coupon-scope">{{ getScopeText(coupon) }}</text>
</view>
<view class="coupon-validity">{{ getValidityText(coupon) }}</view>
</view>
<!-- 操作按钮 -->
<view class="btn-box">
<view v-if="!getCardReceived(coupon) && getCouponCanReceive(coupon)" class="action-btn btn-claim"
@click="$emit('receive', { coupon, index })">
立即领取
</view>
<view v-else-if="getCardReceived(coupon)" class="action-btn btn-received">
已领取
</view>
<view v-else class="action-btn btn-disabled">
{{ coupon.receiveStatusText || '已达上限' }}
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部确定按钮 -->
<view class="popup_bottom_btn_wrap">
<view class="popup_btn_ok" @click="$emit('update:show', false)">
确定
</view>
</view>
</view>
</up-popup>
</template>
<script>
export default {
name: "product-coupon-popup",
props: {
show: {
type: Boolean,
default: false,
},
list: {
type: Array,
default: () => [],
},
},
methods: {
getDiscountAmount(coupon) {
return coupon.discountAmount !== undefined ? coupon.discountAmount : (coupon.amount || 0);
},
getThresholdText(coupon) {
const threshold = coupon.thresholdAmount !== undefined ? Number(coupon.thresholdAmount) : 0;
if (threshold === 0) {
return "无门槛";
}
return `满${threshold}元可用`;
},
getTagText(coupon) {
if (coupon.scopeType === 1) return "通用券";
if (coupon.scopeType === 2) return "商品券";
if (coupon.scopeType === 3 || coupon.scopeType === 4) return "品类券";
if (coupon.scopeText && coupon.scopeText.includes("通用")) return "通用券";
return "通用券";
},
getScopeText(coupon) {
if (coupon.scopeText) return coupon.scopeText;
if (coupon.scopeType === 1) return "所有商品可用";
if (coupon.scopeType === 2) return "指定商品可用";
if (coupon.scopeType === 3) return "指定类目可用";
if (coupon.scopeType === 4) return "指定专区商品可用";
return "所有商品可用";
},
getValidityText(coupon) {
if (coupon.validDays && coupon.validDays > 0) {
return `领取之日起${coupon.validDays}天内有效`;
}
if (coupon.useEndTime) {
return `有效期至 ${this.formatTime(coupon.useEndTime)}`;
}
if (coupon.receiveEndTime) {
return `截止时间 ${this.formatTime(coupon.receiveEndTime)}`;
}
return "领取之日起30天内有效";
},
getCardReceived(coupon) {
return coupon.received || coupon.receiveStatus === 1;
},
getCouponCanReceive(coupon) {
return coupon.canReceive !== false && coupon.receiveStatus !== 2;
},
getCardDisabled(coupon) {
return coupon.receiveStatus === 2 || (coupon.canReceive === false && !coupon.received && coupon.receiveStatus !== 1);
},
formatTime(time) {
if (!time) return "";
if (typeof time === "string" && time.includes("-")) return time;
const date = new Date(Number(time));
if (isNaN(date.getTime())) return String(time);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
const hh = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
const ss = String(date.getSeconds()).padStart(2, "0");
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
},
},
};
</script>
<style lang="scss" scoped>
.goods_coupon_popup_container {
width: 100%;
max-width: 100vw;
background-color: #ffffff;
border-radius: 32rpx 32rpx 0 0;
padding: 32rpx 24rpx 40rpx 24rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
.popup_header {
position: relative;
text-align: center;
margin-bottom: 24rpx;
.popup_title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
}
.popup_coupon_scroll {
width: 100%;
max-width: 100%;
box-sizing: border-box;
max-height: 700rpx;
min-height: 300rpx;
.popup_coupon_list {
width: 100%;
max-width: 100%;
box-sizing: border-box;
padding: 8rpx 0;
}
}
.popup_bottom_btn_wrap {
margin-top: 24rpx;
padding: 0 16rpx;
.popup_btn_ok {
width: 100%;
height: 84rpx;
line-height: 84rpx;
text-align: center;
background: linear-gradient(135deg, #a855f7 0%, #7a35f6 100%);
color: #ffffff;
font-size: 30rpx;
font-weight: bold;
border-radius: 42rpx;
box-shadow: 0 8rpx 20rpx rgba(122, 53, 246, 0.25);
transition: transform 0.1s ease;
&:active {
transform: scale(0.98);
}
}
}
.coupon-card {
position: relative;
width: 100%;
max-width: 100%;
display: flex;
align-items: center;
background: rgba(253, 28, 36, 0.06);
border-radius: 16rpx;
margin-bottom: 20rpx;
box-sizing: border-box;
overflow: hidden;
&.card-disabled {
background: #f5f5f5 !important;
.card-left {
.price-box {
color: #a0a0a0 !important;
}
.condition {
color: #999999 !important;
}
}
.coupon-title {
color: #333333 !important;
}
}
.card-left {
width: 150rpx;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20rpx 0;
.price-box {
display: flex;
align-items: baseline;
color: #ff2442;
.currency {
font-size: 26rpx;
font-weight: bold;
margin-right: 2rpx;
}
.amount {
font-size: 52rpx;
font-weight: 700;
line-height: 1;
}
}
.condition {
font-size: 24rpx;
color: #ff2442;
margin-top: 10rpx;
font-weight: 500;
}
}
.divider-wrapper {
position: relative;
width: 2rpx;
align-self: stretch;
.divider-line {
height: 100%;
border-left: 2rpx dashed #fca5a5;
}
.notch {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 24rpx;
height: 24rpx;
background-color: #ffffff;
border-radius: 50%;
z-index: 10;
}
.top-notch {
top: -12rpx;
}
.bottom-notch {
bottom: -12rpx;
}
}
.card-disabled .divider-line {
border-left: 2rpx dashed #dddddd !important;
}
.card-right {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 16rpx;
box-sizing: border-box;
.info-content {
flex: 1;
min-width: 0;
margin-right: 10rpx;
overflow: hidden;
.coupon-title {
font-size: 30rpx;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 10rpx;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tag-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
min-width: 0;
overflow: hidden;
.coupon-tag {
display: inline-block;
font-size: 20rpx;
color: #ff2442;
background: #ffe4e6;
border: 1px solid #ff99a4;
border-radius: 6rpx;
padding: 2rpx 10rpx;
margin-right: 10rpx;
line-height: 1.2;
font-weight: 500;
flex-shrink: 0;
white-space: nowrap;
&.tag-disabled {
color: #888888 !important;
background: #eeeeee !important;
border: 1px solid #cccccc !important;
}
}
.coupon-scope {
flex: 1;
min-width: 0;
font-size: 22rpx;
color: #666666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.coupon-validity {
font-size: 22rpx;
color: #999999;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.btn-box {
flex-shrink: 0;
.action-btn {
width: 124rpx;
height: 54rpx;
line-height: 54rpx;
text-align: center;
font-size: 24rpx;
font-weight: bold;
border-radius: 8rpx;
box-sizing: border-box;
flex-shrink: 0;
&.btn-claim {
background: #ff1e38;
color: #ffffff;
}
&.btn-received {
background: #ffe8eb;
color: #ff2442;
border: 1px solid #ff4d5e;
line-height: 52rpx;
font-weight: 500;
}
&.btn-disabled {
background: #eeeeee;
color: #bbbbbb;
}
}
}
}
}
}
</style>
@@ -0,0 +1,247 @@
<template>
<!-- 拼团玩法与可直接参与的拼团 (高保真还原) -->
<view class="group_rules_card" v-if="isGroupProduct">
<!-- 拼团玩法 -->
<view class="group_rules_section">
<view class="group_section_title">拼团玩法</view>
<view class="group_steps_row">
<!-- Step 1 -->
<view class="group_step_item">
<image class="step_icon_img" src="https://static.tbmall.xin/static/new/group_buying_1.png"
mode="aspectFit">
</image>
<text class="step_text">开团/参加</text>
</view>
<!-- Step 2 -->
<view class="group_step_item">
<image class="step_icon_img" src="https://static.tbmall.xin/static/new/group_buying_2.png"
mode="aspectFit">
</image>
<text class="step_text">邀请好友参团</text>
</view>
<!-- Step 3 -->
<view class="group_step_item">
<image class="step_icon_img" src="https://static.tbmall.xin/static/new/group_buying_3.png"
mode="aspectFit">
</image>
<text class="step_text">成团发货</text>
</view>
</view>
<view class="group_rules_notice">
购买须知:付款后等待成团,成团后 1-3 天发货。未成团且不支持自动成团时全额退款。
</view>
</view>
<!-- 可直接参与的拼团 (无数据时不展示) -->
<view v-if="displayGroupTeams && displayGroupTeams.length > 0">
<!-- 分割线 -->
<view class="group_section_divider"></view>
<!-- 可直接参与的拼团 -->
<view class="group_direct_section">
<view class="group_direct_header" @click="$emit('openJoinablePopup')">
<text class="group_direct_title">可直接参与的拼团</text>
<view class="group_direct_more">
<up-image src="/static/common/right.png" width="12" height="12" bgColor="#f1f6ff00"></up-image>
</view>
</view>
<view class="group_teams_list">
<view class="group_team_item" v-for="(team, tIdx) in displayGroupTeams.slice(0, 3)" :key="team.id || tIdx">
<view class="team_user_info">
<view class="team_avatar_wrap">
<image class="team_avatar avatar_1" :src="team.avatar1" mode="aspectFill"></image>
<image class="team_avatar avatar_2" :src="team.avatar2" mode="aspectFill"></image>
</view>
<text class="team_user_name">{{ team.nickname }}</text>
</view>
<view class="team_action_wrap">
<text class="team_countdown_text">{{ team.countdownText }}</text>
<view class="btn_join_team" @click="$emit('joinTeam', team)">
<text class="btn_join_text">去拼团</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: "product-group-rules",
props: {
isGroupProduct: {
type: Boolean,
default: false,
},
displayGroupTeams: {
type: Array,
default: () => [],
},
},
};
</script>
<style lang="scss" scoped>
.group_rules_card {
background-color: #ffffff;
border-radius: 24rpx;
padding: 28rpx 24rpx;
margin-top: 20rpx;
.group_section_title {
font-size: 28rpx;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 24rpx;
}
.group_steps_row {
display: flex;
align-items: flex-start;
justify-content: space-around;
padding: 10rpx 0;
.group_step_item {
display: flex;
flex-direction: column;
align-items: center;
width: 180rpx;
.step_icon_img {
width: 96rpx;
height: 96rpx;
}
.step_text {
font-size: 24rpx;
color: #333333;
margin-top: 14rpx;
font-weight: 500;
text-align: center;
}
}
}
.group_rules_notice {
font-size: 22rpx;
color: #999999;
line-height: 36rpx;
margin-top: 20rpx;
padding-top: 16rpx;
}
.group_section_divider {
height: 1rpx;
background-color: #f2f2f2;
margin: 24rpx 0;
}
.group_direct_section {
.group_direct_header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
.group_direct_title {
font-size: 28rpx;
font-weight: bold;
color: #1a1a1a;
}
}
.group_teams_list {
.group_team_item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
.team_user_info {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
.team_avatar_wrap {
display: flex;
align-items: center;
position: relative;
width: 80rpx;
height: 52rpx;
flex-shrink: 0;
.team_avatar {
width: 52rpx;
height: 52rpx;
border-radius: 50%;
border: 2rpx solid #ffffff;
background-color: #e6e6e6;
position: absolute;
top: 0;
&.avatar_1 {
left: 0;
z-index: 2;
}
&.avatar_2 {
left: 28rpx;
z-index: 1;
}
}
}
.team_user_name {
font-size: 26rpx;
color: #333333;
font-weight: 500;
margin-left: 12rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
}
}
.team_action_wrap {
display: flex;
align-items: center;
flex-shrink: 0;
.team_countdown_text {
font-size: 22rpx;
color: #999999;
margin-right: 16rpx;
}
.btn_join_team {
background: linear-gradient(90deg, #8a3bf8 0%, #742af5 100%);
border-radius: 26rpx;
padding: 0 24rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
&:active {
opacity: 0.85;
}
.btn_join_text {
color: #ffffff;
font-size: 24rpx;
font-weight: bold;
}
}
}
}
}
}
}
</style>
@@ -0,0 +1,199 @@
<template>
<!-- 可直接参与的拼团弹窗 -->
<up-popup :show="show" mode="bottom" :round="16" :closeOnClickOverlay="true"
@close="$emit('update:show', false)">
<view class="joinable_popup_container">
<!-- 头部 -->
<view class="joinable_popup_header">
<text class="joinable_popup_title">可直接参与的拼团</text>
<view class="joinable_popup_close" @click="$emit('update:show', false)">
<u-icon name="close" size="18" color="#71737c"></u-icon>
</view>
</view>
<!-- 拼团列表 -->
<scroll-view scroll-y class="joinable_popup_body">
<view class="joinable_popup_list">
<view class="joinable_team_item" v-for="(team, idx) in list" :key="team.teamId || team.id || idx">
<!-- 头像区域(叠层) -->
<view class="joinable_avatar_wrap">
<image class="joinable_avatar avatar_main" :src="team.avatar1" mode="aspectFill"></image>
<image class="joinable_avatar avatar_sub" :src="team.avatar2" mode="aspectFill"></image>
</view>
<!-- 昵称 -->
<text class="joinable_name">{{ team.nickname }}</text>
<!-- 倒计时 -->
<text class="joinable_countdown">{{ team.countdownText }}</text>
<!-- 去拼团按钮 -->
<view class="joinable_btn" @click="$emit('join', team)">
<text class="joinable_btn_text">去拼团</text>
</view>
</view>
<!-- 列表为空 -->
<view v-if="!list || list.length === 0" class="joinable_empty">
<text class="joinable_empty_text">暂无可参与的拼团</text>
</view>
</view>
</scroll-view>
</view>
</up-popup>
</template>
<script>
export default {
name: "product-joinable-popup",
props: {
show: {
type: Boolean,
default: false,
},
list: {
type: Array,
default: () => [],
},
},
};
</script>
<style lang="scss" scoped>
.joinable_popup_container {
display: flex;
flex-direction: column;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
max-height: 80vh;
.joinable_popup_header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 36rpx 40rpx 24rpx;
border-bottom: 1rpx solid #f2f2f2;
.joinable_popup_title {
font-size: 32rpx;
font-weight: bold;
color: #1a1a1a;
}
.joinable_popup_close {
position: absolute;
right: 40rpx;
top: 50%;
transform: translateY(-50%);
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.joinable_popup_body {
flex: 1;
max-height: calc(80vh - 120rpx);
.joinable_popup_list {
padding: 0 40rpx;
.joinable_team_item {
display: flex;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f6f6f6;
&:last-child {
border-bottom: none;
}
// 头像叠层
.joinable_avatar_wrap {
position: relative;
width: 80rpx;
height: 56rpx;
flex-shrink: 0;
.joinable_avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
border: 2rpx solid #ffffff;
background-color: #e6e6e6;
position: absolute;
top: 0;
&.avatar_main {
left: 0;
z-index: 2;
}
&.avatar_sub {
left: 24rpx;
z-index: 1;
}
}
}
// 昵称
.joinable_name {
flex: 1;
font-size: 28rpx;
color: #333333;
font-weight: 500;
margin-left: 12rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// 倒计时
.joinable_countdown {
font-size: 22rpx;
color: #999999;
margin: 0 20rpx 0 12rpx;
white-space: nowrap;
flex-shrink: 0;
}
// 去拼团按钮
.joinable_btn {
background: linear-gradient(90deg, #8a3bf8 0%, #742af5 100%);
border-radius: 28rpx;
padding: 0 24rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: opacity 0.2s;
&:active {
opacity: 0.82;
}
.joinable_btn_text {
color: #ffffff;
font-size: 26rpx;
font-weight: bold;
}
}
}
// 空列表占位
.joinable_empty {
padding: 80rpx 0;
display: flex;
align-items: center;
justify-content: center;
.joinable_empty_text {
font-size: 28rpx;
color: #cccccc;
}
}
}
}
}
</style>
File diff suppressed because it is too large Load Diff