consumer-app/pages/detail/serviceDetail.vue

721 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">
<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"
>距您 {{ storeInfo.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, getPaySign } from "@/api/service";
import NavHeader from "@/components/NavHeader/NavHeader.vue";
export default {
components: {
NavHeader,
},
data() {
return {
storeInfo: {},
menuList: [],
categoryLabel: "",
shopId: null,
userInfo: {},
hasCheckedLogin: false, // 是否已经检查过登录状态
isLoadingAfterLogin: false, // 是否正在登录后重新加载
};
},
computed: {
// 计算总金额只计算选中且数量大于0的商品
totalAmount() {
return this.menuList.reduce((total, item) => {
if (item.selected && item.quantity > 0) {
// 价格是以分为单位,需要转换为元
const price =
(item.salePrice || item.currentPrice || item.price || 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) {
// 从路由参数获取店铺ID和分类标签
this.shopId = options.id;
this.categoryLabel = options.categoryLabel;
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;
this.isLoadingAfterLogin = true; // 标记正在登录后重新加载
// 设置一个全局标记,表示刚刚登录返回,用于防止 401 弹窗
uni.setStorageSync("justBackFromLogin", true);
// 5秒后清除标记
setTimeout(() => {
uni.removeStorageSync("justBackFromLogin");
}, 5000);
// 延迟一下,确保 token 已经保存并生效,避免与之前的请求冲突
setTimeout(() => {
// 如果有店铺ID重新加载店铺数据特别是菜单数据可能需要登录才能查看
if (this.shopId) {
this.loadStoreData();
}
// 加载完成后清除标记
setTimeout(() => {
this.isLoadingAfterLogin = false;
}, 1000);
}, 500); // 增加延迟时间,确保之前的请求已经完成
} else if (token) {
// 如果已登录,更新用户信息(可能用户信息有更新)
this.userInfo = newUserInfo;
}
// 标记已经检查过登录状态
this.hasCheckedLogin = true;
},
methods: {
// 加载店铺数据
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;
}
} 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;
}
//
// 把选中的数据拼接成这样的数据 couponId:num;couponId:num 例如 2324:1;2325:2
const couponStr = selectedItems
.map((item) => `${item.couponId}:${item.quantity}`)
.join(";");
// this.totalAmount 是元, 要转换成分
const trxamt = (this.totalAmount * 100).toFixed(0);
},
// 支付调用
async handlePay(){
// 测试支付进行
const reqsn =
"mini" + Date.now() + "" + Math.floor(Math.random() * 10000);
const randomstr = Math.floor(Math.random() * 10000000) + "";
// 定义请求参数
// 仅排序,不修改任何字段值,参考对象中没有的字段放在最后
// 注意参数 所有参数的类型用STring 不然会造成意外的报错,
let params = {
appid: "00390105", // 平台分配的appid
body: "body订单标题", //
cusid: "56479107531MPMN", // 平台分配的商户号
notify_url:
"http://e989c692.natappfree.cc/admin-api/member/labor-union-coupon-purchase/tlNotice", // 保持原值不变
orgid: "56479107392N35H",
paytype: "W06",
randomstr: randomstr,
remark: "remark备注",
reqsn: reqsn,
sign: "",
signtype: "RSA",
trxamt: "1",
version: "12",
//"asinfo":"56479107392MP4J:01:0.01" //(后台已经设置固定比例, 会自动分账) 分账, 第一个是分账商户号,第二个是01 类型, 根据金额分账,0.01 元
};
const sign = await getPaySign(params);
console.log("测试返回的签名:" + sign);
params["sign"] = sign;
console.log(params);
// params = this.sortObjectByKey(params);
console.log(params);
// 统一使用 navigateToMiniProgram 跳转小程序
uni.navigateToMiniProgram({
appId: "wxef277996acc166c3", // 目标小程序appid
extraData: params, // 传递给目标小程序的参数
success(res) {
console.log("小程序跳转成功", res);
},
fail(err) {
console.error("小程序跳转失败", err);
// 可根据需要添加失败后的提示或兜底逻辑
uni.showToast({
title: "跳转失败,请稍后重试",
icon: "none",
});
},
complete() {
// 无论成功失败都会执行的逻辑(可选)
},
});
//---- 支付
}
},
};
</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: 24rpx;
color: #d51c3c;
.price-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 48rpx;
color: #d51c3c;
}
}
.original-price {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 20rpx;
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: 20rpx;
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>