Files
jurong_circle_front_app/pages/home/mallDetail.vue
Sun_sun 1f4c2c75eb 2025-10-23
支付页面
2025-10-23 10:18:06 +08:00

767 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="detail-container">
<u-navbar id="uNavbarId" title="商品详情" :background="{background: 'transparent' }" :border-bottom="false"
back-icon-color="#000" title-color="#000">
<template v-slot:right>
<image class="collection" src="/static/icon/Settings.png" mode=""></image>
</template>
</u-navbar>
<scroll-view scroll-y="true" :style="'height:'+scrollHeight+'px'">
<view class="img-banner">
<u-swiper height="400" img-mode="aspectFit" mode="none" bg-color="transparent" :list="dataInfo.banner"
@change="handleChangeBanner" :current="currentBanner"></u-swiper>
</view>
<scroll-view scroll-x="true" class="img-sub-banner">
<image v-for="(item,index) in dataInfo.banner" :src="item" class="img-item"
:class="{active: currentBanner==index}" mode="aspectFit" @click="handleChangeBanner(index)">
</image>
</scroll-view>
<view class="mall-info">
<view class="name">
{{dataInfo.name}}
</view>
<view class="description">
{{dataInfo.description}}
</view>
<view class="price">
<view>
<view class="mall-price">
<image src="/static/icon/rongdou.png" class="icon" mode=""></image>
{{dataInfo.rongdou_price}}
</view>
<view class="mall-price">
<image src="/static/icon/jifen.png" class="icon" mode=""></image>
{{dataInfo.points_price}}
</view>
<view v-if="dataInfo.discount" class="discount-info">
<u-tag type="error" text="1折优惠" />
</view>
</view>
<view>
<view>已售{{dataInfo.sales}}</view>
<view style="color: red;">剩余{{dataInfo.stock}}</view>
</view>
</view>
<view class="rating">
评分<u-rate :disabled="true" :count="5" v-model="dataInfo.rating" active-color="#f7ba2a"></u-rate>
</view>
<view class="detail">
<h3>具体描述</h3>
<view v-html="dataInfo.details"></view>
</view>
<view class="recommend">
<h3>推荐商品</h3>
<view class="mall-list">
<u-waterfall v-model="recommendedList" ref="mallListRef">
<template v-slot:left="{leftList}">
<view class="mall-item u-m-r-10" v-for="(item, index) in leftList" :key="index"
@click="handleCheck(item)">
<u-lazy-load threshold="-450" border-radius="10" :image="getImageUrl(item.image)"
:index="index"></u-lazy-load>
<view class="mall-title u-m-l-5 u-m-r-5">
{{item.name}}
</view>
<view class="mall-price u-m-l-5 u-m-r-5">
<image src="/static/icon/rongdou.png" class="icon" mode=""></image>
{{item.price}}
</view>
<view class="mall-price u-m-l-5 u-m-r-5">
<image src="/static/icon/jifen.png" class="icon" mode=""></image>
{{item.points}}
</view>
</view>
</template>
<template v-slot:right="{rightList}">
<view class="mall-item u-m-l-10" v-for="(item, index) in rightList" :key="index"
@click="handleCheck(item)">
<u-lazy-load threshold="-450" border-radius="10" :image="getImageUrl(item.image)"
:index="index"></u-lazy-load>
<view class="mall-title u-m-l-5 u-m-r-5">
{{item.name}}
</view>
<view class="mall-price u-m-l-5 u-m-r-5">
<image src="/static/icon/rongdou.png" class="icon" mode=""></image>
{{item.price}}
</view>
<view class="mall-price u-m-l-5 u-m-r-5">
<image src="/static/icon/jifen.png" class="icon" mode=""></image>
{{item.points}}
</view>
</view>
</template>
</u-waterfall>
</view>
</view>
</view>
</scroll-view>
<view class="bottom-view" id="bottomViewId">
<view class="icon-btn">
<!-- <view class="item">
<u-image width="100%" :fade="false" src="/static/mall/Home.png" mode="widthFix"></u-image>
店铺
</view> -->
<view class="item">
<u-image width="100%" :fade="false" src="/static/mall/Twitch.png" mode="widthFix"></u-image>
客服
</view>
<view class="item">
<u-image width="100%" :fade="false" src="/static/mall/Star.png" mode="widthFix"></u-image>
收藏
</view>
</view>
<view class="text-btn">
<u-button class="add-car common" :hair-line="false" hover-class="none"
@click="handleBtn(0)">加入购物车</u-button>
<u-button class="buy common" :hair-line="false" hover-class="none" @click="handleBtn(1)">领券购买</u-button>
</view>
</view>
<u-popup v-model="showSure" mode="bottom" length="70%" :closeable="true" blur="90%">
<scroll-view scroll-y="true" style="height: 100%;">
<view class="sure-popup">
<view class="title">{{popTitle}}</view>
<view class="address">
<view class="text">
<image style="width: 40rpx;height: 40rpx;" src="/static/icon/Map pin2.png" mode=""></image>
<view class="u-m-l-10">张三 | </view>
<view class="u-m-l-10">浙江省 宁波市 海曙区</view>
</view>
<view class="right-icon">
<image style="width: 100%;height: 100%;" src="/static/icon/Chevron right Menu.png" mode="">
</image>
</view>
</view>
<view class="count-select u-p-l-10 u-p-r-10">
<view class="pre-view">
<u-image :src="getImageUrl(dataInfo.image_url)" height="100%" width="100%">
<template v-slot:error>
<view style="font-size: 24rpx;">暂无图片</view>
</template>
</u-image>
</view>
<view class="text">
<view>
实付<image src="/static/icon/rongdou.png" class="icon" mode=""></image>
{{dataInfo.price * order.count}}
<view>
<u-icon name="integral"></u-icon>
{{dataInfo.points * order.count}}
</view>
</view>
<view>
<u-number-box v-model="order.count" :max="dataInfo.stock" :min="1"></u-number-box>
</view>
</view>
</view>
<view class="spec-option" v-for="item in specNames">
<view class="title">
{{item + "("+specOptions.get(item).size+")"}}
</view>
<view class="option-list">
<view class="option-item" v-for="key in specOptions.get(item).keys()" :key="key"
:class="{active: isChose(key), unactive: !canChose(key)}"
@click="handleChangeSpec(key, item)">
{{specOptions.get(item).get(key)}}
</view>
</view>
</view>
<view class="spec-option">
<view class="title">
订单备注
</view>
<view class="">
<u-input type="textarea" :border="true" :height="100" v-model="order.orderNote"></u-input>
</view>
</view>
<view class="spec-option">
<u-button @click="handleSubmit">{{popTitle}}</u-button>
</view>
</view>
</scroll-view>
</u-popup>
<u-toast ref="msgRef" />
</view>
</template>
<script setup>
import {
onMounted,
ref,
getCurrentInstance
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import {
mallAPI
} from '../../api/mall';
import {
getImageUrl, arrayContainsAll
} from '../../util/common.js';
const instance = getCurrentInstance();
const scrollHeight = ref(0)
const loadHeight = () => {
uni.getSystemInfo({
success(res) {
let screenHeight = res.screenHeight
uni.createSelectorQuery().in(instance.proxy).select("#uNavbarId").boundingClientRect((
data) => {
scrollHeight.value = screenHeight - data.height
}).exec()
uni.createSelectorQuery().in(instance.proxy).select("#bottomViewId").boundingClientRect((
data) => {
scrollHeight.value = scrollHeight.value - data.height
}).exec()
}
})
}
const currentBanner = ref(0)
const handleChangeBanner = (index) => {
currentBanner.value = index
}
const handleCheck = (item) => {
uni.redirectTo({
url: '/pages/home/mallDetail?id=' + item.id
})
}
const msgRef = ref()
// 切换 加入购物车/确认购买
const showSure = ref(false)
const popTitle = ref('')
const flag = ref(0)
const handleBtn = (sign) => {
if (sign == 0) {
popTitle.value = '加入购物车'
flag.value = 0
} else if (sign == 1) {
popTitle.value = '确认购买'
flag.value = 1
}
showSure.value = true
}
const order = ref({
count: 1
})
const choseKeys = ref([])
const handleChangeSpec = (key, allKey) => {
if (!canChose(key)) return
if (choseKeys.value.includes(key)) {
// 取消选中
choseKeys.value = choseKeys.value.filter(item => item != key)
} else {
// console.log(choseKeys.value);
// console.log(specOptions.value.get(allKey));
// 清除选项
const keysArray = Array.from(specOptions.value.get(allKey).keys())
choseKeys.value = choseKeys.value.filter(item => {
if (keysArray.includes(item)) return false
return true
})
// 选中
choseKeys.value.push(key)
}
}
// 判断当前key是否可选
const canChose = (key) => {
// console.log("当前规格ID:", key);
// console.log("规格组合列表:", activeKeys.value);
// console.log("已选择的规格ID列表:", choseKeys.value);
// console.log("规格选项:", specOptions.value);
// 如果已经选中,肯定可选(因为可以取消选中)
if (isChose(key)) {
return true;
}
// 构建临时选择的key列表
let tempChoseKeys = [...choseKeys.value];
// 找到当前key所属的规格类别
let specCategory = '';
for (const [category, options] of specOptions.value.entries()) {
if (options.has(key)) {
specCategory = category;
break;
}
}
// 如果找到了所属规格类别,需要先移除同类别下已选的其他选项
if (specCategory) {
const sameCategoryKeys = Array.from(specOptions.value.get(specCategory).keys());
tempChoseKeys = tempChoseKeys.filter(item => !sameCategoryKeys.includes(item));
}
// 添加当前要判断的key
tempChoseKeys.push(key);
// 检查是否存在包含所有临时选择key的有效规格组合
const hasValidCombination = activeKeys.value.some(combination => {
return arrayContainsAll(combination, tempChoseKeys);
});
return hasValidCombination;
}
// 是否被选中
const isChose = (key) => {
return choseKeys.value.includes(key)
}
const handleSubmit = () => {
// 匹配规格
let specification = dataInfo.value.specifications.filter(item => {
let group = item.combination_key.split('-').map(Number)
if (arraysEqualUnordered(group, choseKeys.value)) return true
return false
})
let specificationId = null
if (specification.length != 0) {
specificationId = specification[0].id
} else {
msgRef.value.show({
title: '请选择规格',
type: 'warning'
})
return
}
// 立即购买
const cartItem = {
productId: dataInfo.value.id,
quantity: order.value.count,
specificationId: specificationId,
points: dataInfo.value.points,
name: dataInfo.value.name,
image: dataInfo.value.image,
stock: dataInfo.value.stock
}
if (flag.value == 0) {
// 加入购物车
mallAPI.addCart(cartItem).then(res => {
if (res.success) {
msgRef.value.show({
title: '已加入购物车',
type: 'error'
})
} else {
msgRef.value.show({
title: res.data.message || '添加到购物车失败',
type: 'error'
})
}
}).finally(() => {
order.value.count = 1
order.value.orderNote = ''
choseKeys.value = []
showSure.value = false
})
} else if (flag.value == 1) {
mallAPI.addCart(cartItem).then(res => {
if (res.success) {
const cartItemId = res.data?.cart_item_id || res.data?.id || res.data
?.cartItemId || res.id
if (!cartItemId) {
msgRef.value.show({
title: '无法获取购物车项ID',
type: 'error'
})
} else {
mallAPI.createOrder({
cart_item_ids: [cartItemId]
}).then(response => {
if (response.success) {
showSure.value = false
// 进入支付页面
uni.navigateTo({
url: '/pages/home/pay?preOrderId=' + response.data
.preOrderId
})
} else {
msgRef.value.show({
title: '操作失败',
type: 'error'
})
}
})
}
} else {
msgRef.value.show({
title: res.data.message || '添加到购物车失败',
type: 'error'
})
}
})
}
}
// 判断两个数组值是否相等
const arraysEqualUnordered = (arr1, arr2) => {
if (arr1.length !== arr2.length) return false;
const sorted1 = [...arr1].sort();
const sorted2 = [...arr2].sort();
return sorted1.every((item, index) => item === sorted2[index]);
}
const dataId = ref()
const dataInfo = ref({})
const recommendedList = ref([])
const specNames = ref(new Set())
const specOptions = ref(new Map())
const activeKeys = ref([])
const loadData = () => {
mallAPI.getMallDetail(dataId.value).then(res => {
dataInfo.value = res.data.product
dataInfo.value.banner = []
if (dataInfo.value.images.length != 0) {
dataInfo.value.images.forEach(item => {
dataInfo.value.banner.push(getImageUrl(item))
})
}
loadSpec()
})
mallAPI.getRecommended(dataId.value).then(res => {
recommendedList.value = res.data.products
})
}
const loadSpec = () => {
dataInfo.value.specifications.forEach(specification => {
activeKeys.value.push(specification.combination_key)
specification.spec_details.forEach(detail => {
specNames.value.add(detail.spec_display_name)
let data = specOptions.value.get(detail.spec_display_name)
if (data == undefined) {
data = new Map()
data.set(detail.id, detail.display_value)
specOptions.value.set(detail.spec_display_name, data)
} else {
data.set(detail.id, detail.display_value)
specOptions.value.set(detail.spec_display_name, data)
}
})
})
}
onLoad((val) => {
dataId.value = val.id
})
onMounted(() => {
loadHeight()
loadData()
})
</script>
<style scoped lang="scss">
.detail-container {
width: 100%;
height: 100vh;
background: linear-gradient(180deg, #2F75F9 0%, #F0F3FF 34.13%);
.collection {
width: 48rpx;
height: 48rpx;
margin-right: 24rpx;
}
.img-sub-banner {
padding: 12rpx 0;
background: #fff;
white-space: nowrap;
.img-item {
border-radius: 20rpx;
width: 128rpx;
height: 128rpx;
margin: 0 4rpx;
}
.active {
border: 1px solid #189eff;
}
}
.mall-info {
padding: 20rpx 34rpx;
.name {
font-weight: 650;
font-size: 20px;
leading-trim: NONE;
line-height: 46rpx;
letter-spacing: 0%;
}
.price {
display: flex;
justify-content: space-between;
align-items: center;
.mall-price {
font-size: 30rpx;
color: #305DEF;
margin-top: 10rpx;
.icon {
height: 30rpx;
width: 30rpx;
}
}
}
.detail {
margin-top: 20rpx;
border: 1rpx solid #fff;
padding: 10rpx 20rpx;
box-shadow: 1px 1px 2px 2px rgba(0, 0, 255, 0.2);
}
.recommend {
margin-top: 20rpx;
.mall-list {
margin-top: 20rpx;
.mall-item {
border-radius: 16rpx;
background: #F0F5FF;
position: relative;
margin-top: 20rpx;
box-shadow: 0px 4px 4px 0px #00000040;
padding-bottom: 10rpx;
.mall-title {
font-size: 30rpx;
margin-top: 10rpx;
color: $u-main-color;
}
.mall-price {
font-size: 30rpx;
color: $u-type-error;
margin-top: 10rpx;
.icon {
height: 30rpx;
width: 30rpx;
}
}
.mall-tag {
display: flex;
margin-top: 5px;
.mall-tag-owner {
background-color: $u-type-error;
color: #FFFFFF;
display: flex;
align-items: center;
padding: 4rpx 14rpx;
border-radius: 50rpx;
font-size: 20rpx;
line-height: 1;
}
.mall-tag-text {
margin-right: 10px;
border: 1px solid $u-type-primary;
color: $u-type-primary;
border-radius: 50rpx;
line-height: 1;
padding: 4rpx 14rpx;
display: flex;
align-items: center;
border-radius: 50rpx;
font-size: 20rpx;
}
}
.mall-shop {
font-size: 22rpx;
color: $u-tips-color;
margin-top: 5px;
}
}
}
}
}
.bottom-view {
position: absolute;
bottom: 0;
width: 100%;
height: 116rpx;
background: #F5F8FF;
padding: 0 29rpx;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
.icon-btn {
display: flex;
width: 228rpx;
width: 30%;
flex-wrap: nowrap;
.item {
margin-right: 20rpx;
white-space: nowrap;
font-family: Work Sans;
font-weight: 400;
font-style: Regular;
font-size: 26rpx;
leading-trim: NONE;
line-height: 100%;
letter-spacing: -2%;
}
}
.text-btn {
display: flex;
flex: 1;
justify-content: flex-end;
.common {
border: none;
width: 200rpx;
height: 70rpx;
margin: 0;
color: #fff;
font-family: Work Sans;
font-weight: 400;
font-style: Regular;
font-size: 26rpx;
leading-trim: NONE;
line-height: 100%;
letter-spacing: -2%;
}
.buy {
background: #6287FF;
border-top-right-radius: 12rpx;
border-bottom-right-radius: 12rpx;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.add-car {
background: #A8BCFF;
border-top-right-radius: 0rpx;
border-bottom-right-radius: 0rpx;
border-top-left-radius: 12rpx;
border-bottom-left-radius: 12rpx;
}
}
}
}
.sure-popup {
background: #F5F8FF;
padding-bottom: 20rpx;
.title {
font-weight: 274;
font-style: Light;
font-size: 32rpx;
leading-trim: NONE;
line-height: 48rpx;
letter-spacing: 0%;
text-align: center;
}
.address {
display: flex;
align-items: center;
margin-top: 30rpx;
padding: 20rpx 10rpx;
justify-content: space-between;
.text {
display: flex;
align-items: center;
}
.right-icon {
width: 40rpx;
height: 40rpx;
}
}
.count-select {
display: flex;
margin-top: 10rpx;
.pre-view {
width: 160rpx;
height: 160rpx;
}
.text {
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.icon {
height: 30rpx;
width: 30rpx;
}
}
.spec-option {
margin: 20rpx 0;
padding: 0 20rpx;
.title {
font-weight: 700;
font-style: Bold;
font-size: 32rpx;
leading-trim: NONE;
line-height: 48rpx;
letter-spacing: 0%;
text-align: left;
}
.option-list {
display: flex;
.option-item {
background: #f2f3f5;
padding: 10rpx 20rpx;
margin: 20rpx 20rpx 20rpx 0;
}
.active {
border: 1rpx solid #ff6b35;
color: #ff6b35;
}
.unactive {
background: #D9D9D9;
}
}
}
}
</style>