consumer-app/pages/detail/serviceDetail.vue

737 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="store-page">
<!-- 顶部导航栏 -->
<NavHeader title="店铺详情" />
<!-- 顶部店铺信息点击进入地图页 -->
<view class="store-header" @click="handleGoMap">
<view class="store-brand">
<image
class="brand-image"
:src="storeInfo.coverUrl"
mode="aspectFill"
></image>
</view>
<view class="store-info">
<text class="store-name">{{ storeInfo.name }}</text>
<view class="store-detail">
<text class="detail-text">门店地址: {{ storeInfo.address }}</text>
</view>
<view class="store-detail">
<text class="detail-text">联系电话: {{ storeInfo.phone }}</text>
</view>
<view class="store-tags-row">
<view class="store-tags">
<view class="tag-item tag-pink">会员特惠</view>
<view class="tag-item tag-orange">{{ categoryLabel }}</view>
</view>
<view class="distance-info">
<image
class="location-icon"
src="/static/service/location-icon.png"
mode="aspectFill"
></image>
<text class="distance-text"
>距您 {{ distance || "0" }}km</text
>
</view>
</view>
</view>
</view>
<!-- 中间菜单列表可滑动 -->
<scroll-view class="menu-list" scroll-y="true">
<view class="menu-item" v-for="(item, index) in menuList" :key="index">
<view
class="checkbox"
:class="{ checked: item.selected }"
@click="toggleMenuItem(index)"
>
<text v-if="item.selected" class="checkmark">✓</text>
</view>
<image
class="menu-image"
:src="item.coverUrl || '/static/service/menu-default.png'"
mode="aspectFill"
></image>
<view class="menu-info">
<view class="menu-title-row">
<text class="menu-title">{{ item.name }}</text>
<view class="discount-badge" v-if="item.discount">
{{ item.discount }}折
</view>
</view>
<text class="menu-desc">{{ item.description }}</text>
<view class="menu-price-row">
<view class="current-price">
<view class="price-label">现价¥</view>
<view class="price-text">{{ formatPrice(item.salePrice) }}</view>
</view>
<text class="original-price" v-if="item.originalPrice"
>原价¥{{ formatPrice(item.originalPrice) }}</text
>
<view class="quantity-control">
<view
class="quantity-btn minus"
:class="{
disabled: (item.quantity || 0) <= 0,
}"
@click="decreaseQuantity(index)"
>-</view
>
<text class="quantity-number">{{ item.quantity || 0 }}</text>
<view
class="quantity-btn plus"
@click="increaseQuantity(index)"
>+</view
>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部结算栏(固定) -->
<view class="checkout-footer">
<view class="total-info">
<text class="total-label">总金额¥</text>
<text class="total-amount">{{ totalAmount.toFixed(2) }}</text>
<view class="member-benefit">
<image
class="crown-icon"
src="/static/service/crown-icon.png"
mode="aspectFill"
></image>
<text class="benefit-text">{{ memberLevelName }}</text>
</view>
</view>
<button
class="checkout-btn"
:class="{ disabled: totalAmount <= 0 }"
:disabled="totalAmount <= 0"
@click="handleCheckout"
>
去结算
</button>
</view>
</view>
</template>
<script>
import { getGuildStoreDetail, getGuildCoupon, createGuildCouponWxPay } from "@/api/service";
import NavHeader from "@/components/NavHeader/NavHeader.vue";
export default {
components: {
NavHeader
},
data() {
return {
storeInfo: {},
menuList: [],
categoryLabel: "",
shopId: null,
userInfo: {},
distance: null,
paying: false,
};
},
computed: {
// 计算总金额只计算选中且数量大于0的商品
totalAmount() {
return this.menuList.reduce((total, item) => {
if (item.selected && item.quantity > 0) {
// 价格是以分为单位,需要转换为元
const price = (item.salePrice || 0) / 100;
return total + price * item.quantity;
}
return total;
}, 0);
},
// 是否有会员优惠
hasMemberDiscount() {
return this.menuList.some((item) => item.selected && item.discount);
},
// 获取会员等级名称(安全访问)
memberLevelName() {
return (this.userInfo && this.userInfo.level && this.userInfo.level.name) || '普通会员';
},
},
onLoad(options) {
console.log(options, 111110);
// 从路由参数获取店铺ID和分类标签
this.shopId = options.id;
this.categoryLabel = options.categoryLabel;
// 将距离从米转换为千米保留2位小数
if (options.distance) {
this.distance = (parseFloat(options.distance) / 1000).toFixed(2);
}
this.userInfo = uni.getStorageSync('userInfo') || {};
if (this.shopId) {
this.loadStoreData();
} else {
uni.showToast({
title: "店铺信息错误",
icon: "none",
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
},
onShow() {
// 页面显示时,检查登录状态并更新用户信息
// 如果之前未登录,现在已登录,重新加载数据
const token = uni.getStorageSync('token');
const newUserInfo = uni.getStorageSync('userInfo') || {};
// 如果之前没有用户信息,现在有了(说明刚登录成功),重新加载数据
if (token && (!this.userInfo || !this.userInfo.id) && newUserInfo && newUserInfo.id) {
this.userInfo = newUserInfo;
// 如果有店铺ID重新加载店铺数据特别是菜单数据可能需要登录才能查看
if (this.shopId) {
this.loadStoreData();
}
} else if (token) {
// 如果已登录,更新用户信息(可能用户信息有更新)
this.userInfo = newUserInfo;
}
},
methods: {
// 顶部店铺信息点击:进入地图页
handleGoMap() {
if (!this.shopId) return;
uni.navigateTo({
url: `/pages/detail/mapDetail?id=${this.shopId}`,
});
},
// 加载店铺数据
async loadStoreData() {
try {
// 并行加载店铺详情和菜单
await Promise.all([
this.loadStoreDetail(this.shopId),
this.loadStoreMenu(this.shopId),
]);
} catch (error) {
console.error("加载店铺数据失败:", error);
uni.showToast({
title: "加载店铺信息失败",
icon: "none",
});
}
},
// 加载店铺详情
async loadStoreDetail(shopId) {
try {
const res = await getGuildStoreDetail(shopId);
if (res) {
this.storeInfo = res;
// 如果接口返回的距离是米,转换为千米
if (this.storeInfo.distance && typeof this.storeInfo.distance === 'number') {
this.storeInfo.distance = (this.storeInfo.distance / 1000).toFixed(2);
}
}
} catch (error) {
console.error("加载店铺详情失败:", error);
}
},
// 加载工会优惠券
async loadStoreMenu(shopId) {
try {
const res = await getGuildCoupon({ shopId });
console.log(res, 11119);
if (res && res.list) {
this.menuList = res.list;
}
} catch (error) {
uni.showToast({
title: error,
icon: "none",
});
}
},
// 切换菜单项选择状态
toggleMenuItem(index) {
const item = this.menuList[index];
item.selected = !item.selected;
if (!item.selected) {
item.quantity = 0;
} else if (item.quantity === 0) {
item.quantity = 1;
}
},
// 增加数量
increaseQuantity(index) {
const item = this.menuList[index];
// 如果未选中,先选中
if (!item.selected) {
item.selected = true;
}
// 增加数量
item.quantity = (item.quantity || 0) + 1;
},
// 减少数量
decreaseQuantity(index) {
const item = this.menuList[index];
const currentQuantity = item.quantity || 0;
// 确保数量不能小于0
if (currentQuantity > 0) {
item.quantity = currentQuantity - 1;
// 如果数量减到0自动取消选中状态
if (item.quantity === 0) {
item.selected = false;
}
}
},
// 格式化价格将分转换为元除以100保留两位小数
formatPrice(price) {
if (!price && price !== 0) {
return '0.00';
}
// 将分转换为元
const yuan = price / 100;
// 保留两位小数
return yuan.toFixed(2);
},
// 去结算
async handleCheckout() {
if (this.totalAmount <= 0) {
return;
}
// 获取选中的商品
const selectedItems = this.menuList.filter(
(item) => item.selected && item.quantity > 0
);
if (selectedItems.length === 0) {
uni.showToast({
title: "请选择商品",
icon: "none",
});
return;
}
// #ifndef MP-WEIXIN
uni.showToast({
title: "请在微信小程序内使用微信支付",
icon: "none",
});
return;
// #endif
if (this.paying) return;
this.paying = true;
try {
// 1) 获取小程序登录 code用于后端换取 openid / 下单)
const loginRes = await new Promise((resolve, reject) => {
uni.login({
provider: "weixin",
success: resolve,
fail: reject,
});
});
const loginCode = loginRes && loginRes.code;
if (!loginCode) {
throw new Error("获取微信登录 code 失败");
}
// 2) 调后端:创建订单 + 返回微信支付参数
const payRes = await createGuildCouponWxPay({
shopId: this.shopId,
// 仅传必要字段,避免后端解析困难
items: selectedItems.map((it) => ({
id: it.id,
quantity: it.quantity,
})),
loginCode,
// 金额(单位元),后端应以实际计算为准
amount: this.totalAmount.toFixed(2),
});
// 3) 兼容不同返回结构
const raw = (payRes && (payRes.payParams || payRes.payInfo || payRes)) || {};
const timeStamp = String(raw.timeStamp || raw.timestamp || raw.time_stamp || "");
const nonceStr = raw.nonceStr || raw.nonce_str || "";
const pkg = raw.package || raw.packageValue || raw.package_value || "";
const signType = raw.signType || raw.sign_type || "MD5";
const paySign = raw.paySign || raw.pay_sign || "";
if (!timeStamp || !nonceStr || !pkg || !paySign) {
console.error("支付参数缺失:", raw);
uni.showToast({
title: "支付参数异常,请联系管理员",
icon: "none",
});
return;
}
// 4) 调起微信支付
await new Promise((resolve, reject) => {
uni.requestPayment({
provider: "wxpay",
timeStamp,
nonceStr,
package: pkg,
signType,
paySign,
success: resolve,
fail: reject,
});
});
uni.showToast({
title: "支付成功",
icon: "success",
});
// 可选:跳转到服务记录页(待支付/已完成)
setTimeout(() => {
uni.navigateTo({
url: "/pages/profileSub/serviceRecords",
});
}, 600);
} catch (err) {
console.error("支付失败:", err);
const msg = (err && (err.errMsg || err.message)) || "支付失败/已取消";
uni.showToast({
title: msg.includes("cancel") ? "已取消支付" : "支付失败",
icon: "none",
});
} finally {
this.paying = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.store-page {
min-height: 100vh;
background-color: #e2e8f1;
display: flex;
flex-direction: column;
padding-bottom: 120rpx; // 为底部结算栏留出空间
}
/* 顶部店铺信息 */
.store-header {
background-color: #ffffff;
margin: 14rpx 20rpx;
padding: 40rpx 30rpx 30rpx;
display: flex;
align-items: flex-start;
border-radius: 20rpx;
margin-bottom: 20rpx;
.store-brand {
position: relative;
width: 140rpx;
height: 140rpx;
margin-right: 24rpx;
flex-shrink: 0;
.brand-image {
width: 100%;
height: 100%;
border-radius: 12rpx;
}
}
.store-info {
flex: 1;
display: flex;
flex-direction: column;
.store-name {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 30rpx;
color: #1a1819;
}
.store-detail {
.detail-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 22rpx;
color: #888888;
line-height: 1.5;
}
}
.store-tags-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8rpx;
.store-tags {
display: flex;
gap: 12rpx;
.tag-item {
padding: 6rpx 16rpx;
font-size: 20rpx;
&.tag-pink {
background-color: rgba(213, 28, 60, 0.1);
color: #d51c3c;
}
&.tag-orange {
background-color: rgba(255, 107, 0, 0.1);
color: #ff6b00;
}
}
}
.distance-info {
display: flex;
align-items: center;
.location-icon {
width: 17rpx;
height: 20rpx;
margin-right: 8rpx;
}
.distance-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 22rpx;
color: #004294;
}
}
}
}
}
/* 中间菜单列表 */
.menu-list {
flex: 1;
overflow-y: auto;
width: 95%;
margin: 0 auto;
min-height: 0;
.menu-item {
background-color: #ffffff;
border-radius: 20rpx;
padding: 25rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
position: relative;
.checkbox {
width: 30rpx;
height: 30rpx;
border: 1rpx solid #cccccc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 19rpx;
flex-shrink: 0;
&.checked {
background-color: #004294;
border-color: #004294;
.checkmark {
color: #ffffff;
font-size: 28rpx;
font-weight: bold;
}
}
}
.menu-image {
width: 160rpx;
height: 173rpx;
border-radius: 10rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.menu-info {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
.menu-title-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.menu-title {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 28rpx;
color: #1a1819;
margin-right: 12rpx;
}
.discount-badge {
border: 1rpx solid #d51c3c;
color: #d51c3c;
font-size: 24rpx;
padding: 6rpx 14rpx;
font-weight: bold;
}
}
.menu-desc {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 22rpx;
color: #888888;
line-height: 30rpx;
}
.menu-price-row {
display: flex;
align-items: baseline;
gap: 10rpx;
margin-top: 16rpx;
.current-price {
display: flex;
align-items: baseline;
font-weight: bold;
font-size: 22rpx;
color: #d51c3c;
.price-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 47rpx;
color: #d51c3c;
}
}
.original-price {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 18rpx;
color: #545454;
text-decoration: line-through;
background-color: rgba(115, 115, 115, 0.1);
padding: 7rpx 6rpx;
}
.quantity-control {
display: flex;
align-items: center;
gap: 15rpx;
margin-left: auto;
.quantity-btn {
width: 35rpx;
height: 35rpx;
border: 1rpx solid #888888;
line-height: 35rpx;
display: flex;
align-items: center;
justify-content: center;
color: #333333;
font-weight: 500;
background-color: #ffffff;
border-radius: 4rpx;
}
.quantity-number {
font-family: PingFang-SC, PingFang-SC;
min-width: 40rpx;
text-align: center;
font-weight: 500;
font-size: 30rpx;
color: #333333;
}
}
}
}
}
}
/* 底部结算栏 */
.checkout-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 25rpx 20rpx 25rpx 48rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid #e2e8f1;
z-index: 100;
.total-info {
display: flex;
align-items: baseline;
.total-label {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 30rpx;
color: #d51c3c;
}
.total-amount {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 50rpx;
color: #d51c3c;
}
.member-benefit {
display: flex;
align-items: center;
gap: 8rpx;
margin-left: 12rpx;
background: rgba(255, 107, 0, 0.1);
padding: 4rpx 12rpx;
border-radius: 8rpx;
.crown-icon {
width: 23rpx;
height: 20rpx;
}
.benefit-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 20rpx;
color: #ff6b00;
}
}
}
.checkout-btn {
color: #ffffff;
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 28rpx;
border: none;
background: #004294;
border-radius: 35rpx;
position: absolute;
right: 20rpx;
}
}
</style>