feat: 添加订单结算页面及相关功能
实现订单结算页面,包括: 1. 新增结算页面路由配置 2. 创建结算组件处理普通商品和租用商品的结算逻辑 3. 添加租用机柜容器组件 4. 更新购物车跳转逻辑指向新结算页面 5. 添加产品图片占位SVG 6. 更新迁移文档说明
This commit is contained in:
parent
80307e6992
commit
cb445624e6
|
|
@ -5,7 +5,10 @@
|
|||
"Bash(mkdir -p 'E:\\code\\智柜宝\\wx\\src\\api\\shop')",
|
||||
"Bash(tree 'E:\\code\\智柜宝\\wx\\src\\api' -I 'foo|goods|layout|login|me|order|system|types' -L 2)",
|
||||
"Bash(mkdir -p 'E:\\code\\智柜宝\\wx\\src\\pages\\home')",
|
||||
"Bash(mkdir -p 'E:\\code\\智柜宝\\wx\\src\\pages\\home\\components')"
|
||||
"Bash(mkdir -p 'E:\\code\\智柜宝\\wx\\src\\pages\\home\\components')",
|
||||
"Bash(cp 'E:\\code\\智柜宝\\wx\\doc\\thirdParty\\src\\pages\\product\\components\\checkout.vue' 'E:\\code\\智柜宝\\wx\\src\\pages\\index\\components\\checkout.vue')",
|
||||
"Bash(cp 'E:\\code\\智柜宝\\wx\\doc\\thirdParty\\src\\pages\\product\\components\\RentingCabinetContainer.vue' 'E:\\code\\智柜宝\\wx\\src\\pages\\index\\components\\renting-cabinet-container.vue')",
|
||||
"Bash(test -f 'E:\\code\\智柜宝\\wx\\src\\pages\\index\\components\\product-container.vue')"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
@doc\thirdParty\ 文件夹下是旧H5项目的代码,现在已经将其api部分的代码移植到了本微信小程序项目。现在需要移植页面,改为
|
||||
微信小程序uni-app的写法,第一步先移植首页 @doc\thirdParty\src\pages\product\ 内的页面和全部组件,小程序写法参考
|
||||
@src\pages\index\index.vue
|
||||
|
||||
。仅参考语法,api需要使用原ProductList.vue中已经移植到本项目的相应api,stores也需要使用移植后的pinia。生成的代码写到
|
||||
@src\pages\home\ 文件夹下
|
||||
|
||||
参考已迁移至本项目的代码 @src\pages\index\ 。将 @doc\thirdParty\src\pages\product\components\
|
||||
下的另外两个component也迁移到本项目。注意thirdParty下的是H5项目,现在需要改为微信小程序uni-app。api需要使用原Product
|
||||
List.vue中已经移植到本项目的相应api,stores也需要使用移植后的pinia。
|
||||
|
|
@ -61,6 +61,13 @@
|
|||
},
|
||||
"excludeLoginPath": false
|
||||
},
|
||||
{
|
||||
"path": "pages/index/checkout",
|
||||
"type": "page",
|
||||
"style": {
|
||||
"navigationBarTitleText": "订单结算"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/faceLogin",
|
||||
"type": "page",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,495 @@
|
|||
<script setup lang="ts">
|
||||
import { useCartStore } from "@/pinia/stores/cart";
|
||||
import { useWxStore } from "@/pinia/stores/wx";
|
||||
import { useAb98UserStore } from '@/pinia/stores/ab98-user';
|
||||
import { storeToRefs } from "pinia";
|
||||
import { submitOrderApi } from "@/api/shop";
|
||||
import type { SubmitOrderRequestData } from "@/api/shop/types";
|
||||
import { useProductStore } from "@/pinia/stores/product";
|
||||
import { useRentingCabinetStore } from "@/pinia/stores/rentingCabinet";
|
||||
// 导入支付方式映射
|
||||
import { paymentMethodOptions, modeToPaymentMethodMap } from "@/utils/maps/payment";
|
||||
|
||||
const cartStore = useCartStore();
|
||||
const { cartItems, totalPrice } = storeToRefs(cartStore);
|
||||
|
||||
// 引入 rentingCabinet store 数据
|
||||
const rentingCabinetStore = useRentingCabinetStore();
|
||||
const { rentingCartItems, rentingCartTotalPrice } = storeToRefs(rentingCabinetStore);
|
||||
|
||||
const wxStore = useWxStore();
|
||||
const { openid, balance, corpidLogin, userid: qyUserid, name: qyName } = storeToRefs(wxStore);
|
||||
|
||||
const ab98UserStore = useAb98UserStore();
|
||||
const { tel, userid: ab98Userid, name } = storeToRefs(ab98UserStore);
|
||||
|
||||
const productStore = useProductStore();
|
||||
|
||||
// 判断当前是否为租用模式 (mode 为 3)
|
||||
const isRentingMode = computed(() => productStore.selectedShop?.mode === 3);
|
||||
|
||||
const currentTotalPrice = computed(() => {
|
||||
return isRentingMode.value ? rentingCartTotalPrice.value : totalPrice.value;
|
||||
});
|
||||
|
||||
// 根据店铺模式计算支持的支付方式
|
||||
const supportedPayments = computed(() => {
|
||||
const shopMode = productStore.selectedShop?.mode || 0;
|
||||
const allowedValues = modeToPaymentMethodMap[shopMode] || [];
|
||||
// 筛选条件:
|
||||
// 1. 支付方式值必须在店铺模式允许的列表中
|
||||
// 2. 微信支付(值为0)仅当非企业微信登录时才显示
|
||||
return paymentMethodOptions.filter(option => allowedValues.includes(option.value)
|
||||
&& (option.value !== 0 || !corpidLogin.value));
|
||||
});
|
||||
|
||||
// 初始化选中的支付方式为第一个支持的选项
|
||||
const selectedPayment = ref<string>(supportedPayments.value[0]?.value.toString() || '0');
|
||||
const applyRemark = ref("");
|
||||
const submitting = ref(false);
|
||||
|
||||
// 判断是否需要审批
|
||||
const isApproval = computed(() => {
|
||||
// 对于普通商品,判断是否需要审批 (belongType == 1)
|
||||
// 对于租用机柜,通常没有审批概念,这里默认不认为是审批模式
|
||||
if (isRentingMode.value) {
|
||||
return false; // 租用模式下通常不走审批流程
|
||||
} else {
|
||||
return cartItems.value.some(item => item.product.belongType == 1);
|
||||
}
|
||||
});
|
||||
|
||||
// 支付方式value到类型的映射
|
||||
const paymentValueToType: Record<string, 'wechat' | 'balance' | 'approval'> = {
|
||||
'0': 'wechat',
|
||||
'1': 'balance',
|
||||
'2': 'approval',
|
||||
'3': 'balance'
|
||||
};
|
||||
|
||||
// uni-app支付调用
|
||||
async function callUniPay(paymentInfo: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// uni-app支付API
|
||||
/* uni.requestPayment({
|
||||
provider: 'wxpay',
|
||||
timeStamp: paymentInfo.timeStamp,
|
||||
nonceStr: paymentInfo.nonceStr,
|
||||
package: paymentInfo.package.startsWith('prepay_id=')
|
||||
? paymentInfo.package
|
||||
: `prepay_id=${paymentInfo.package}`,
|
||||
signType: paymentInfo.signType,
|
||||
paySign: paymentInfo.paySign,
|
||||
success: () => {
|
||||
resolve(true);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '支付未完成'));
|
||||
}
|
||||
}); */
|
||||
});
|
||||
}
|
||||
|
||||
// 显示提示信息
|
||||
function showToast(title: string) {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (isRentingMode.value) {
|
||||
if (!rentingCartItems.value.length) {
|
||||
return showToast('请先选择租用商品后再结算');
|
||||
}
|
||||
} else {
|
||||
if (!cartItems.value.length) {
|
||||
return showToast('请先选择商品后再结算');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有可用支付方式
|
||||
if (supportedPayments.value.length === 0) {
|
||||
return showToast('没有支持的支付方法,请从微信进入智借还');
|
||||
}
|
||||
|
||||
if (!openid.value && !wxStore.isFakeQyLogin) {
|
||||
return showToast('请从微信中打开');
|
||||
}
|
||||
|
||||
// 验证手机号
|
||||
if (!/^1[3-9]\d{9}$/.test(tel.value)) {
|
||||
return showToast('请输入有效的手机号码');
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
// 判断用户类型:
|
||||
// 2 - 企业微信用户
|
||||
// 1 - 汇邦云用户
|
||||
// 0 - 外部用户
|
||||
const isInternal = corpidLogin.value ? 2 : ab98Userid.value ? 1 : 0;
|
||||
|
||||
// 根据模式组织 goodsList
|
||||
let goodsListToSend;
|
||||
if (isRentingMode.value) {
|
||||
goodsListToSend = rentingCartItems.value.map(item => ({
|
||||
quantity: item.quantity,
|
||||
cellId: item.cabinetCell.cellId, // 租用模式下,cellId
|
||||
mode: productStore.selectedShop?.mode || 0,
|
||||
}));
|
||||
} else {
|
||||
goodsListToSend = cartItems.value.map(item => ({
|
||||
goodsId: item.product.id,
|
||||
quantity: item.quantity,
|
||||
cellId: item.product.cellId,
|
||||
mode: productStore.selectedShop?.mode || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
const requestData: SubmitOrderRequestData = {
|
||||
openid: openid.value,
|
||||
userid: wxStore.userid,
|
||||
corpid: wxStore.corpid,
|
||||
goodsList: goodsListToSend,
|
||||
// 根据选中的支付方式value获取对应的类型
|
||||
paymentType: paymentValueToType[selectedPayment.value] || 'wechat',
|
||||
mobile: tel.value,
|
||||
name: isInternal === 2 ? qyName.value : name.value,
|
||||
applyRemark: applyRemark.value,
|
||||
qyUserid: wxStore.userid,
|
||||
mode: productStore.selectedShop?.mode || 0,
|
||||
isInternal: isInternal
|
||||
};
|
||||
|
||||
const { code, data } = await submitOrderApi(requestData);
|
||||
|
||||
if (code !== 0) {
|
||||
throw new Error("订单提交失败");
|
||||
}
|
||||
|
||||
if (selectedPayment.value == '0') { // 微信支付
|
||||
if (data.paymentInfo) {
|
||||
await callUniPay(data.paymentInfo);
|
||||
}
|
||||
} else if (selectedPayment.value == '1' || selectedPayment.value == '3') { // 借呗支付 或 余额支付
|
||||
wxStore.setBalance(data.newBalance || 0);
|
||||
showToast(`${selectedPayment.value === '1' ? '借呗' : '余额'}支付成功,剩余余额:¥${data.newBalance?.toFixed(2)}`);
|
||||
} else if (selectedPayment.value == '2') { // 要呗支付 (审批模式)
|
||||
showToast('提交领用申请成功,请等待管理员审批');
|
||||
}
|
||||
|
||||
// 跳转到订单成功页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/index/order-success?orderId=${data.orderId}`
|
||||
});
|
||||
|
||||
// 根据模式清空对应的购物车
|
||||
if (isRentingMode.value) {
|
||||
rentingCabinetStore.clearRentingCart();
|
||||
} else {
|
||||
cartStore.clearCart();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'user_cancel') {
|
||||
showToast(error instanceof Error ? error.message : "支付流程中断");
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="checkout-container">
|
||||
<!-- <uni-nav-bar title="结算页面" left-text="返回" left-arrow @clickLeft="$router.go(-1)" /> -->
|
||||
|
||||
<view class="content-wrapper">
|
||||
<!-- 根据 isRentingMode 动态渲染列表 -->
|
||||
<view class="product-list">
|
||||
<view v-if="isRentingMode">
|
||||
<view v-for="item in rentingCartItems" :key="item.cabinetCell.cellId" class="product-item">
|
||||
<view class="product-info-wrapper">
|
||||
<view class="product-image-wrapper">
|
||||
<image src="/static/svg/product-image.svg" class="product-image" />
|
||||
</view>
|
||||
<view class="product-info">
|
||||
<view class="product-name">
|
||||
{{ `格口号 ${item.cabinetCell.cellNo}` }}
|
||||
</view>
|
||||
<view class="price-row">
|
||||
<text class="product-price">
|
||||
¥{{ item.cabinetCell.cellPrice?.toFixed(2) }}
|
||||
</text>
|
||||
<text class="quantity">
|
||||
×{{ item.quantity }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else>
|
||||
<view v-for="item in cartItems" :key="item.product.id" class="product-item">
|
||||
<view class="product-info-wrapper">
|
||||
<view class="product-image-wrapper">
|
||||
<image :src="item.product.image" class="product-image" />
|
||||
</view>
|
||||
<view class="product-info">
|
||||
<view class="product-name">
|
||||
{{ item.product.name }}
|
||||
</view>
|
||||
<view class="price-row">
|
||||
<text class="product-price">
|
||||
¥{{ item.product.price.toFixed(2) }}
|
||||
</text>
|
||||
<text class="quantity">
|
||||
×{{ item.quantity }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.product.usageInstruction" class="usage-instruction">
|
||||
<view class="instruction-content">
|
||||
使用说明:{{ item.product.usageInstruction }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tel-input-wrapper">
|
||||
<view class="tel-label">
|
||||
<text class="required-star">*</text>
|
||||
<text class="label-text">手机号</text>
|
||||
</view>
|
||||
<input v-model="tel" placeholder="请输入手机号" class="tel-input" type="number" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式 -->
|
||||
<view v-if="!isApproval" class="contact-form">
|
||||
<view v-for="method in supportedPayments" :key="method.value"
|
||||
:class="['payment-option', { selected: selectedPayment === method.value.toString(), disabled: method.value === 1 && (balance < currentTotalPrice) }]"
|
||||
@click="selectedPayment = method.value.toString()">
|
||||
<text class="method-label">
|
||||
{{ method.label }}
|
||||
<text v-if="method.value === 1" class="balance-amount">(当前:¥{{ balance.toFixed(2) }})</text>
|
||||
</text>
|
||||
<view class="empty"></view>
|
||||
<text v-if="selectedPayment === method.value.toString()" class="check-icon">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交订单栏 -->
|
||||
<view class="submit-bar">
|
||||
<view class="total-price">
|
||||
合计:¥{{ currentTotalPrice.toFixed(2) }}
|
||||
</view>
|
||||
<button type="primary" size="default" :loading="submitting" loading-text="提交中..."
|
||||
class="submit-button" @click="handleSubmit">
|
||||
提交订单
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.checkout-container {
|
||||
padding: 12px 16px 80px;
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.product-list {
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.product-info-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.product-image-wrapper {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
color: #e95d5d;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.usage-instruction {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.instruction-content {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tel-input-wrapper {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tel-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.required-star {
|
||||
color: #e95d5d;
|
||||
margin-right: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tel-input {
|
||||
background-color: #f5f5f5;
|
||||
margin-left: 12px;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.submit-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 16px;
|
||||
color: #e95d5d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #e95d5d;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
margin-right: 0;
|
||||
padding: 0 24px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.payment-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin: 8px 0;
|
||||
padding: 12px;
|
||||
border: 1px solid #ebedf0;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.payment-option.selected {
|
||||
border-color: #07c160;
|
||||
background-color: #f6ffed;
|
||||
}
|
||||
|
||||
.payment-option.disabled {
|
||||
opacity: 0.6;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.method-label {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
flex-grow: 1;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #07c160;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -68,9 +68,10 @@ function handleClearCartOrRentingCart() {
|
|||
|
||||
// 结算方法
|
||||
function handleCheckout() {
|
||||
handleClose()
|
||||
// emit事件由父组件处理
|
||||
// router.push("/product/checkout")
|
||||
handleClose();
|
||||
uni.navigateTo({
|
||||
url: '/pages/index/checkout'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,435 @@
|
|||
<script setup lang="ts">
|
||||
import { useRentingCabinetStore } from "@/pinia/stores/rentingCabinet"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { computed, onMounted, ref } from "vue"
|
||||
|
||||
// 定义Props
|
||||
const props = defineProps<{
|
||||
shopId: number; // 接收店铺ID,用于获取数据
|
||||
}>();
|
||||
|
||||
// 定义Emit事件
|
||||
const emit = defineEmits<{
|
||||
(e: 'backToShopList'): void;
|
||||
(e: 'checkoutRenting'): void; // 用于租用结算
|
||||
}>();
|
||||
|
||||
// 状态管理
|
||||
const rentingCabinetStore = useRentingCabinetStore();
|
||||
const { rentingCabinets, rentingCartItems, rentingCartTotalQuantity } = storeToRefs(rentingCabinetStore);
|
||||
|
||||
// 从props接收的数据
|
||||
const activeCategory = ref(0); // 这里的分类可以根据 cabinetName 来划分,也可以简化为所有可租用格口
|
||||
|
||||
// 购物车弹窗控制
|
||||
const showCartPopup = ref(false);
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
// 点击分类导航
|
||||
function handleCategoryClick(index: number) {
|
||||
activeCategory.value = index;
|
||||
}
|
||||
|
||||
// 占位图片 URL
|
||||
const PLACEHOLDER_IMAGE_URL = '/static/svg/product-image.svg';
|
||||
|
||||
// 添加到购物车
|
||||
function handleAddToCart(cabinetCell: any) {
|
||||
// 每个格口只能加入一个,且无法增减,直接确保数量为1
|
||||
const existingItem = rentingCartItems.value.find(item => item.cabinetCell.cellId === cabinetCell.cellId);
|
||||
if (!existingItem) {
|
||||
rentingCabinetStore.addToRentingCart(cabinetCell, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 从购物车移除
|
||||
function handleRemoveFromCart(cellId: number) {
|
||||
rentingCabinetStore.removeFromRentingCart(cellId, 1);
|
||||
}
|
||||
|
||||
// 获取购物车中某个格口的数量(应为0或1)
|
||||
function getRentingCartItemCount(cellId: number) {
|
||||
const item = rentingCartItems.value.find(item => item.cabinetCell.cellId === cellId);
|
||||
return item ? item.quantity : 0;
|
||||
}
|
||||
|
||||
// 过滤格口列表,搜索框功能
|
||||
const filteredRentingCells = computed(() => {
|
||||
let cells: any[] = [];
|
||||
// 如果有分类,则按分类过滤
|
||||
if (rentingCabinets.value.length > 0) {
|
||||
// Flattern all cells from all cabinets
|
||||
cells = rentingCabinets.value.flatMap(cabinet => cabinet.cells);
|
||||
}
|
||||
|
||||
// 过滤掉已租用 (isRented = 1) 和已占用 (usageStatus = 2) 的格口
|
||||
const availableCells = cells.filter(cell => cell.isRented === 0 && cell.usageStatus === 1);
|
||||
|
||||
// 搜索过滤
|
||||
if (!searchQuery.value) {
|
||||
return availableCells;
|
||||
}
|
||||
// 假设搜索是针对格口号 (cellNo) 或其他标识信息
|
||||
return availableCells.filter(cell =>
|
||||
String(cell.cellNo).includes(searchQuery.value) ||
|
||||
(cell.cabinetName && cell.cabinetName.toLowerCase().includes(searchQuery.value.toLowerCase()))
|
||||
);
|
||||
});
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
rentingCabinetStore.fetchRentingCabinetDetail(props.shopId);
|
||||
});
|
||||
|
||||
// 结算方法
|
||||
function handleCheckout() {
|
||||
emit('checkoutRenting');
|
||||
}
|
||||
|
||||
// 显示购物车详情
|
||||
function showCartDetail() {
|
||||
showCartPopup.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="product-container">
|
||||
<!-- 左侧分类导航 -->
|
||||
<view class="category-nav-wrapper">
|
||||
<button type="default" class="showShopListBtn" @click="emit('backToShopList')">重选地址</button>
|
||||
<scroll-view scroll-y class="category-nav">
|
||||
<view v-for="(cabinet, index) in rentingCabinets" :key="cabinet.cabinetId" :class="['category-item', { active: activeCategory === index }]" @click="handleCategoryClick(index)">
|
||||
{{ cabinet.cabinetName }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧可租用格口列表 -->
|
||||
<view class="product-list">
|
||||
<view class="search-wrapper">
|
||||
<input v-model="searchQuery" placeholder="搜索格口号或柜机名称" class="search-box" />
|
||||
</view>
|
||||
<scroll-view scroll-y class="category-section">
|
||||
<view v-for="cell in filteredRentingCells" :key="cell.cellId" class="product-item">
|
||||
<view class="product-info-wrapper">
|
||||
<view class="product-image-wrapper">
|
||||
<image :src="PLACEHOLDER_IMAGE_URL" class="product-image">
|
||||
<!-- 已被占用或故障的格口显示 '不可租用' -->
|
||||
<view v-if="cell.isRented === 1 || cell.usageStatus === 2 || cell.availableStatus === 2" class="sold-out-overlay">
|
||||
<text class="sold-out-text">不可租用</text>
|
||||
</view>
|
||||
</image>
|
||||
</view>
|
||||
<view class="product-info">
|
||||
<view class="product-name">
|
||||
格口号: {{ cell.cellNo }}
|
||||
</view>
|
||||
<view class="product-price">
|
||||
¥{{ (cell.cellPrice || 0).toFixed(2) }}
|
||||
</view>
|
||||
<view class="action-row">
|
||||
<view v-if="cell.isRented === 0 && cell.usageStatus === 1 && cell.availableStatus === 1" class="stock-count">
|
||||
可租用
|
||||
</view>
|
||||
<view class="cart-actions">
|
||||
<!-- 数量减按钮:如果已在购物车,则显示 -->
|
||||
<button v-if="getRentingCartItemCount(cell.cellId) > 0" size="mini" class="cart-btn minus-btn" @click.stop="handleRemoveFromCart(cell.cellId)">-</button>
|
||||
<!-- 数量显示:0或1 -->
|
||||
<text v-if="getRentingCartItemCount(cell.cellId) > 0" class="cart-count">{{ getRentingCartItemCount(cell.cellId) }}</text>
|
||||
<!-- 数量加按钮:如果未在购物车且可租用,则显示 -->
|
||||
<button size="mini" type="primary" class="add-cart-btn"
|
||||
:disabled="getRentingCartItemCount(cell.cellId) > 0 || cell.isRented === 1 || cell.usageStatus === 2 || cell.availableStatus === 2"
|
||||
@click.stop="handleAddToCart(cell)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部购物车栏 -->
|
||||
<view v-if="rentingCartItems.length" class="shopping-cart-bar" @click="showCartDetail">
|
||||
<view class="cart-info">
|
||||
<view class="badge-wrapper">
|
||||
<text class="cart-icon">🛒</text>
|
||||
<view class="badge">{{ rentingCartTotalQuantity }}</view>
|
||||
</view>
|
||||
<view class="total-price">
|
||||
已选格口数:{{ rentingCartTotalQuantity }}
|
||||
</view>
|
||||
</view>
|
||||
<button type="primary" size="default" @click.stop="handleCheckout" class="checkout-btn">
|
||||
去结算
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.product-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background: #f7f8fa;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-nav-wrapper {
|
||||
width: 100px;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.showShopListBtn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
background: #f7f8fa;
|
||||
color: #e95d5d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
margin-bottom: 10px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.product-info-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.product-image-wrapper {
|
||||
position: relative;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.sold-out-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sold-out-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
transform: rotate(-15deg);
|
||||
border: 1px solid #eee;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 16px;
|
||||
color: #e95d5d;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.stock-count {
|
||||
font-size: 11px;
|
||||
color: #bbbbbb;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.cart-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
|
||||
.cart-count {
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
button {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.minus-btn {
|
||||
background: #fff;
|
||||
color: #e95d5d;
|
||||
border: 1px solid #e95d5d;
|
||||
}
|
||||
|
||||
.add-cart-btn {
|
||||
background: #e95d5d;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
background: #ccc !important;
|
||||
border-color: #ccc !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 商品项置灰
|
||||
.product-item:has(.sold-out-overlay) {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 修改购物车栏样式 */
|
||||
.shopping-cart-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.cart-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.badge-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -8px;
|
||||
background: #e95d5d;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 0 6px;
|
||||
font-size: 10px;
|
||||
line-height: 18px;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.checkout-btn {
|
||||
background-color: #e95d5d;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
padding: 0 24px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -34,7 +34,7 @@ onMounted(async () => {
|
|||
if (res?.code === 0 && res?.data?.length > 0) {
|
||||
shopList.value = res.data;
|
||||
|
||||
shopList.value = [...shopList.value, ...res.data, ...res.data, ...res.data];
|
||||
// shopList.value = [...shopList.value, ...res.data, ...res.data, ...res.data];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取店铺列表失败:', error)
|
||||
|
|
@ -70,7 +70,7 @@ function backToShopList() {
|
|||
// 结算方法
|
||||
function handleCheckout() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/index/shopping-cart/index'
|
||||
url: '/pages/index/checkout'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="80" height="80" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5" y="5" width="90" height="90" rx="10" fill="#E8F5E9" stroke="#81C784"
|
||||
stroke-width="2" />
|
||||
<text x="50" y="60" font-family="Arial, sans-serif" font-size="24" font-weight="bold"
|
||||
fill="#2E7D32" text-anchor="middle">空闲</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 473 B |
Loading…
Reference in New Issue