Compare commits

...

4 Commits

Author SHA1 Message Date
dzq 4599e5c6ad feat(订单): 添加订单模式字段支持多种业务场景
在订单请求数据类型和结账组件中添加mode字段,用于支持支付、审批、借还等不同业务模式
2025-06-28 14:51:09 +08:00
dzq e83f9750ea feat(租用柜子): 新增租用柜子功能模块
实现租用柜子的完整功能流程,包括:
1. 新增租用柜子容器组件和状态管理
2. 修改商品列表组件支持租用模式
3. 调整结算页面适配租用柜子订单
4. 新增租用购物车逻辑
2025-06-27 15:41:40 +08:00
dzq c43ab6a184 refactor(product): 将商品列表组件拆分为独立组件
将ProductList.vue中的商品展示和购物车逻辑提取到新的ProductContainer组件中,提升代码可维护性
2025-06-26 17:47:58 +08:00
dzq b9e0077b4d feat(智能柜): 添加获取租用中智能柜详情接口及相关类型定义
新增获取租用中智能柜详情的API接口函数getRentingCabinetDetailApi
添加RentingCabinetDetailDTO和CabinetCellEntity类型定义
2025-06-26 11:39:53 +08:00
9 changed files with 1211 additions and 274 deletions

View File

@ -1,5 +1,5 @@
import { request } from '@/http/axios' import { request } from '@/http/axios'
import type { CabinetDetailResponse } from './type' import type { CabinetDetailResponse, RentingCabinetDetailDTO } from './type'
import { OpenCabinetApiData } from '../shop/type' import { OpenCabinetApiData } from '../shop/type'
/** 获取智能柜详情接口 */ /** 获取智能柜详情接口 */
@ -13,6 +13,17 @@ export function getCabinetDetailApi(shopId: number) {
}) })
} }
/** 获取租用中的智能柜详情接口 */
export function getRentingCabinetDetailApi(shopId: number) {
return request<ApiResponseData<RentingCabinetDetailDTO[]>>({
url: 'cabinet/detail/renting',
method: 'get',
params: {
shopId
}
})
}
export function openCabinet(cabinetId: number, pinNo: number, data: OpenCabinetApiData) { export function openCabinet(cabinetId: number, pinNo: number, data: OpenCabinetApiData) {
return request<ApiResponseData<void>>({ return request<ApiResponseData<void>>({
url: `cabinet/openCabinet/${cabinetId}/${pinNo}`, url: `cabinet/openCabinet/${cabinetId}/${pinNo}`,

View File

@ -5,6 +5,46 @@ export interface CabinetDetailDTO {
cells: CellInfoDTO[] cells: CellInfoDTO[]
} }
/** 租用中的智能柜详情DTO */
export interface RentingCabinetDetailDTO {
/** 柜机ID */
cabinetId: number
/** 柜机名称 */
cabinetName: string
/** 锁控编号 */
lockControlNo: number
/** 柜格列表 */
cells: CabinetCellEntity[]
}
/** 智能柜格口实体类 */
export interface CabinetCellEntity {
/** 格口唯一ID */
cellId: number
/** 关联柜机ID */
cabinetId: number
/** 主板ID */
mainboardId?: number
/** 格口号 */
cellNo: number
/** 针脚序号 */
pinNo: number
/** 库存数量 */
stock: number
/** 格口价格 */
cellPrice?: number
/** 是否已租用0-未租用1-已租用 */
isRented: number
/** 格口类型1小格 2中格 3大格 4超大格 */
cellType: number
/** 使用状态1空闲 2已占用 */
usageStatus: number
/** 可用状态1正常 2故障 */
availableStatus: number
/** 商品ID */
goodsId?: number
}
export interface CellInfoDTO { export interface CellInfoDTO {
cellId: number cellId: number
cellNo: number cellNo: number

View File

@ -69,11 +69,15 @@ export interface SubmitOrderRequestData {
/** 是否内部订单 0否 1汇邦云用户 2企业微信用户 */ /** 是否内部订单 0否 1汇邦云用户 2企业微信用户 */
isInternal: number; isInternal: number;
applyRemark: string; applyRemark: string;
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式) */
mode: number;
/** 订单商品明细列表 */ /** 订单商品明细列表 */
goodsList: Array<{ goodsList: Array<{
goodsId: number goodsId?: number
quantity: number quantity: number
cellId: number cellId: number
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式) */
mode: number;
}> }>
} }

View File

@ -13,6 +13,9 @@ import { useWxStore } from "@/pinia/stores/wx"
import { bindQyUserApi } from "@/common/apis/ab98" import { bindQyUserApi } from "@/common/apis/ab98"
import { getShopListApi } from "@/common/apis/shop" import { getShopListApi } from "@/common/apis/shop"
import { ShopEntity } from "@/common/apis/shop/type" import { ShopEntity } from "@/common/apis/shop/type"
import ProductContainer from "./components/ProductContainer.vue"
import RentingCabinetContainer from "./components/RentingCabinetContainer.vue"
import { useRentingCabinetStoreOutside } from "@/pinia/stores/rentingCabinet"
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -20,13 +23,11 @@ const route = useRoute()
const productStore = useProductStore(); const productStore = useProductStore();
const cartStore = useCartStore(); const cartStore = useCartStore();
const wxStore = useWxStore(); const wxStore = useWxStore();
const { cartItems, totalPrice, totalQuantity } = storeToRefs(cartStore); // const rentingCabinetStore = useRentingCabinetStoreOutside();
// store
const { labels, categories } = storeToRefs(productStore);
const { openid, corpidLogin, ab98User, qyUserId } = storeToRefs(wxStore); const { openid, corpidLogin, ab98User, qyUserId } = storeToRefs(wxStore);
//
const activeCategory = ref(0)
// //
const scrollContainer = ref<HTMLElement>() const scrollContainer = ref<HTMLElement>()
// //
@ -40,19 +41,6 @@ const showShopList = ref(true);
const shopList = ref<ShopEntity[]>([]); const shopList = ref<ShopEntity[]>([]);
const shopId = ref<number>(0); const shopId = ref<number>(0);
//
const showDetailPopup = ref(false)
// ID
const currentProductId = ref<number>()
//
const currentProduct = computed(() =>
categories.value.find(p => p.id === currentProductId.value)
)
//
const showCartPopup = ref(false)
const searchQuery = ref('');
const name = ref(''); const name = ref('');
const idNum = ref(''); const idNum = ref('');
const showAb98BindPopup = ref(false); const showAb98BindPopup = ref(false);
@ -62,79 +50,15 @@ function handleShopSelect(selectedShopId: number) {
shopId.value = selectedShopId; shopId.value = selectedShopId;
showShopList.value = false; showShopList.value = false;
productStore.setSelectedShop(shopList.value.find(shop => shop.shopId === selectedShopId)!); productStore.setSelectedShop(shopList.value.find(shop => shop.shopId === selectedShopId)!);
if(productStore.selectedShop?.mode == 3) {
rentingCabinetStore.fetchRentingCabinetDetail(selectedShopId);
} else {
productStore.getGoods(selectedShopId); productStore.getGoods(selectedShopId);
}
cartStore.clearCart(); cartStore.clearCart();
activeCategory.value = 0; rentingCabinetStore.clearRentingCart();
}
function handleBackToShopList() {
showShopList.value = true;
shopId.value = 0;
cartStore.clearCart();
} }
function handleCategoryClick(index: number) {
activeCategory.value = index
}
//
/* const throttledUpdate = throttle(() => {
if (!scrollContainer.value || !categoryRefs.value.length) return
//
const validRefs = categoryRefs.value.filter(Boolean)
if (!validRefs.length) return
const containerTop = scrollContainer.value.getBoundingClientRect().top
const offsets = validRefs.map((el) => {
return el.getBoundingClientRect().top - containerTop
})
let activeIndex = 0
for (let i = offsets.length - 1; i >= 0; i--) {
if (offsets[i] < 100) {
activeIndex = i
break
}
}
// activeCategory.value = activeIndex
}, 100) */
//
const throttledScroll = throttle(() => {
if (!scrollContainer.value) return
const scrollTop = scrollContainer.value.scrollTop
headerHeight.value = Math.max(150 - scrollTop * 0.5, 60)
}, 100)
//
function showProductDetail(productId: number) {
currentProductId.value = productId
showDetailPopup.value = true
}
function handleAddToCart(product: Product) {
cartStore.addToCart(product)
}
function handleRemoveFromCart(product: Product) {
cartStore.removeFromCart(product.cellId)
}
function getCartItemCount(cellId: number) {
const item = cartItems.value.find(item => item.product.cellId === cellId)
return item ? item.quantity : 0
}
function filterProductsByName(products: Product[], query: string) {
if (!query) return products
return products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
)
}
const currentProducts = computed(() => {
const filteredByCategory = categories.value.filter(c => c.label === labels.value[activeCategory.value].id)
return filterProductsByName(filteredByCategory, searchQuery.value)
})
// //
onMounted(() => { onMounted(() => {
@ -153,7 +77,7 @@ onMounted(() => {
} else { } else {
productStore.getGoods(shopId.value); productStore.getGoods(shopId.value);
} }
scrollListener.push(scrollContainer.value?.addEventListener("scroll", throttledScroll)) // scrollListener.push(scrollContainer.value?.addEventListener("scroll", throttledScroll))
// scrollListener.push(scrollContainer.value?.addEventListener("scroll", throttledUpdate)) // scrollListener.push(scrollContainer.value?.addEventListener("scroll", throttledUpdate))
}) })
@ -241,83 +165,16 @@ async function handleAb98Bind() {
<div class="shop-header" :style="{ height: `${headerHeight}px` }"> <div class="shop-header" :style="{ height: `${headerHeight}px` }">
<van-image :src="`${publicPath}cover.png`" class="shop-cover" fit="cover" /> <van-image :src="`${publicPath}cover.png`" class="shop-cover" fit="cover" />
</div> </div>
<div class="product-container"> <template v-if="productStore.selectedShop?.mode === 3">
<!-- 左侧分类导航 --> <RentingCabinetContainer :shop-id="productStore.selectedShop.shopId"
<div> @backToShopList="showShopList = true"
<van-button icon="revoke" type="default" class="showShopListBtn" @click="showShopList = true">重选地址</van-button> @checkoutRenting="handleCheckout"
<van-sidebar v-model="activeCategory" class="category-nav" @change="handleCategoryClick"> />
<van-sidebar-item v-for="label in labels" :key="label.id" :title="label.name" />
</van-sidebar>
</div>
<!-- 右侧商品列表 -->
<div ref="scrollContainer" class="product-list">
<van-search v-model="searchQuery" placeholder="搜索商品名称" shape="round" class="search-box" />
<div class="category-section">
<van-cell v-for="product in currentProducts" :key="product.id" class="product-item">
<template #icon>
<van-image :src="product.image" width="80" height="80" @click.stop="showProductDetail(product.id)"
class="product-image">
<div v-if="product.stock === 0" class="sold-out-overlay">
<span class="sold-out-text">已售罄</span>
</div>
<!-- 真正的图片错误处理 -->
<template #error>
<div class="custom-error">
图片加载失败
</div>
</template> </template>
</van-image> <template v-else>
<ProductContainer @backToShopList="showShopList = true" @checkout="handleCheckout" />
</template> </template>
<div class="product-info">
<div class="product-name van-ellipsis" @click.stop="showProductDetail(product.id)">
{{ product.name }}
</div> </div>
<div class="product-price" @click.stop="showProductDetail(product.id)">
¥{{ product.price.toFixed(2) }}
</div>
<div class="action-row">
<span v-if="product.stock > 0" class="stock-count">
还剩{{ product.stock }}
</span>
<div class="cart-actions">
<van-button v-if="getCartItemCount(product.cellId)" size="mini" icon="minus"
@click.stop="handleRemoveFromCart(product)" />
<span v-if="getCartItemCount(product.cellId)" class="cart-count">{{ getCartItemCount(product.cellId)
}}</span>
<van-button size="mini" type="primary" class="add-cart-btn" icon="plus"
:disabled="product.stock === 0" @click.stop="handleAddToCart(product)" :style="{
opacity: product.stock === 0 ? 0.6 : 1,
}" />
</div>
</div>
</div>
</van-cell>
</div>
</div>
<!-- 底部购物车栏 -->
<div v-if="cartItems.length" class="shopping-cart-bar">
<div class="cart-info">
<van-badge :content="totalQuantity" @click.stop="showCartPopup = true">
<van-icon name="shopping-cart-o" size="24" />
</van-badge>
<div class="total-price">
合计¥{{ totalPrice.toFixed(2) }}
</div>
</div>
<van-button type="primary" size="small" @click="handleCheckout" class="checkout-btn">
去结算
</van-button>
</div>
</div>
</div>
<VanPopup v-model:show="showDetailPopup" position="bottom" :style="{ height: '80%' }" round>
<Detail class="detail-container" @detail-close="showDetailPopup = false" v-if="currentProduct"
:product="currentProduct" />
</VanPopup>
<VanPopup v-model:show="showCartPopup" position="bottom" :style="{ height: '80%' }" round>
<Cart class="detail-container" @cart-close="showCartPopup = false" />
</VanPopup>
<VanPopup v-model:show="showAb98BindPopup" position="center" :style="{ padding: '12px' }" round> <VanPopup v-model:show="showAb98BindPopup" position="center" :style="{ padding: '12px' }" round>
<div style="text-align: center; font-size: 16px; font-weight: bold; margin-bottom: 16px;">请绑定汇邦云账号</div> <div style="text-align: center; font-size: 16px; font-weight: bold; margin-bottom: 16px;">请绑定汇邦云账号</div>
<van-form @submit="handleAb98Bind"> <van-form @submit="handleAb98Bind">
@ -601,6 +458,7 @@ async function handleAb98Bind() {
justify-content: start; justify-content: start;
padding: 12px; padding: 12px;
} }
.shop-cover-img { .shop-cover-img {
width: 100%; width: 100%;
height: 80px; height: 80px;

View File

@ -0,0 +1,397 @@
<!-- ProductList.vue -->
<script setup lang="ts">
import { publicPath } from "@/common/utils/path"
import { useCartStore } from "@/pinia/stores/cart"
import { Product, useProductStore } from "@/pinia/stores/product"
import { throttle } from "lodash-es"
import { storeToRefs } from "pinia"
import VanPopup from "vant/es/popup"
import { computed, onMounted, onBeforeUnmount, ref, watch } from "vue"
import Cart from "./cart.vue" // Cart Detail components
import Detail from "./detail.vue" // Cart Detail components
// Props
// Emit
const emit = defineEmits<{
(e: 'backToShopList'): void;
(e: 'checkout'): void;
}>();
//
const productStore = useProductStore();
const { labels, categories } = storeToRefs(productStore);
const cartStore = useCartStore();
const { cartItems, totalPrice, totalQuantity } = storeToRefs(cartStore);
// props
const activeCategory = ref(0);
//
const scrollContainer = ref<HTMLElement>();
// prop
const headerHeight = ref(150);
let scrollListener: any[] = [];
//
const showDetailPopup = ref(false);
// ID
const currentProductId = ref<number>();
//
const currentProduct = computed(() =>
categories.value.find(p => p.id === currentProductId.value)
);
//
const showCartPopup = ref(false);
const searchQuery = ref('');
//
function handleCategoryClick(index: number) {
activeCategory.value = index;
}
//
/* const throttledScroll = throttle(() => {
if (!scrollContainer.value) return;
const scrollTop = scrollContainer.value.scrollTop;
headerHeight.value = Math.max(150 - scrollTop * 0.5, 60);
}, 100); */
//
function showProductDetail(productId: number) {
currentProductId.value = productId;
showDetailPopup.value = true;
}
function handleAddToCart(product: Product) {
cartStore.addToCart(product);
}
function handleRemoveFromCart(cellId: number) { // cellId
cartStore.removeFromCart(cellId);
}
function getCartItemCount(cellId: number) {
const item = cartItems.value.find(item => item.product.cellId === cellId);
return item ? item.quantity : 0;
}
function filterProductsByName(products: Product[], query: string) {
if (!query) return products;
return products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
}
const currentProducts = computed(() => {
const filteredByCategory = categories.value.filter(c => c.label === labels.value[activeCategory.value]?.id);
return filterProductsByName(filteredByCategory, searchQuery.value);
});
//
onMounted(() => {
// scrollListener.push(scrollContainer.value?.addEventListener("scroll", throttledScroll));
});
//
onBeforeUnmount(() => {
scrollListener.forEach(listener => scrollContainer.value?.removeEventListener("scroll", listener));
scrollListener = [];
});
//
function handleCheckout() {
// emitrouter
// emit
emit('checkout');
}
</script>
<template>
<div class="product-container">
<!-- 左侧分类导航 -->
<div>
<van-button icon="revoke" type="default" class="showShopListBtn" @click="emit('backToShopList')">重选地址</van-button>
<van-sidebar v-model="activeCategory" class="category-nav" @change="handleCategoryClick">
<van-sidebar-item v-for="label in labels" :key="label.id" :title="label.name" />
</van-sidebar>
</div>
<!-- 右侧商品列表 -->
<div ref="scrollContainer" class="product-list">
<van-search v-model="searchQuery" placeholder="搜索商品名称" shape="round" class="search-box" />
<div class="category-section">
<van-cell v-for="product in currentProducts" :key="product.id" class="product-item">
<template #icon>
<van-image :src="product.image" width="80" height="80" @click.stop="showProductDetail(product.id)"
class="product-image">
<div v-if="product.stock === 0" class="sold-out-overlay">
<span class="sold-out-text">已售罄</span>
</div>
<!-- 真正的图片错误处理 -->
<template #error>
<div class="custom-error">
图片加载失败
</div>
</template>
</van-image>
</template>
<div class="product-info">
<div class="product-name van-ellipsis" @click.stop="showProductDetail(product.id)">
{{ product.name }}
</div>
<div class="product-price" @click.stop="showProductDetail(product.id)">
¥{{ product.price.toFixed(2) }}
</div>
<div class="action-row">
<span v-if="product.stock > 0" class="stock-count">
还剩{{ product.stock }}
</span>
<div class="cart-actions">
<van-button v-if="getCartItemCount(product.cellId)" size="mini" icon="minus"
@click.stop="handleRemoveFromCart(product.cellId)" />
<span v-if="getCartItemCount(product.cellId)" class="cart-count">{{ getCartItemCount(product.cellId)
}}</span>
<van-button size="mini" type="primary" class="add-cart-btn" icon="plus" :disabled="product.stock === 0"
@click.stop="handleAddToCart(product)" :style="{
opacity: product.stock === 0 ? 0.6 : 1,
}" />
</div>
</div>
</div>
</van-cell>
</div>
</div>
<!-- 底部购物车栏 -->
<div v-if="cartItems.length" class="shopping-cart-bar">
<div class="cart-info">
<van-badge :content="totalQuantity" @click.stop="showCartPopup = true">
<van-icon name="shopping-cart-o" size="24" />
</van-badge>
<div class="total-price">
合计¥{{ totalPrice.toFixed(2) }}
</div>
</div>
<van-button type="primary" size="small" @click="handleCheckout" class="checkout-btn">
去结算
</van-button>
</div>
</div>
<VanPopup v-model:show="showDetailPopup" position="bottom" :style="{ height: '80%' }" round>
<Detail class="detail-container" @detail-close="showDetailPopup = false" v-if="currentProduct"
:product="currentProduct" />
</VanPopup>
<VanPopup v-model:show="showCartPopup" position="bottom" :style="{ height: '80%' }" round>
<Cart class="detail-container" @cart-close="showCartPopup = false" />
</VanPopup>
</template>
<style scoped lang="scss">
.product-container {
display: flex;
height: 100vh;
background: #f7f8fa;
flex: 1;
min-height: 0;
position: relative;
border-top: 1px solid #e0e0e0;
/* 顶部边框 */
border-top-left-radius: 8px;
/* 左上圆角 */
border-top-right-radius: 8px;
/* 右上圆角 */
overflow: hidden;
/* 确保圆角生效 */
}
.category-nav {
width: 100px;
flex-shrink: 0;
}
.showShopListBtn {
width: 100%;
padding: 0;
border: 0;
}
.product-list {
flex: 1;
overflow-y: auto;
padding: 10px 16px 60px;
background: #ffffff;
}
.category-section {
margin-bottom: 20px;
}
.category-title {
margin: 5px;
padding: 0;
font-size: 16px;
color: #333;
font-weight: bold;
/* background: linear-gradient(to right, #f7f8fa, transparent); */
/* position: sticky; */
top: 0;
z-index: 1;
}
.product-item {
margin-bottom: 10px;
padding: min(2.667vw, 20px) 0;
/* border-radius: 4px; */
/* box-shadow: 0 2px 4px rgba(0,0,0,0.05); */
}
.product-image {
margin-right: 12px;
border-radius: 4px;
overflow: hidden;
}
.product-info {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 80px;
position: relative;
}
.product-name {
font-size: 14px;
color: #333;
line-height: 1.4;
text-align: left;
}
.product-price {
font-size: 16px;
color: #e95d5d;
font-weight: bold;
text-align: left;
}
.add-cart-btn {
align-self: flex-end;
margin-top: 0;
}
.van-sidebar-item--select:before {
background-color: #e95d5d;
}
.van-button--primary {
background-color: #e95d5d;
border: 0;
border-color: #e95d5d;
}
.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;
}
.sold-out-text {
color: #999;
font-size: 14px;
transform: rotate(-15deg);
border: 1px solid #eee;
padding: 2px 8px;
border-radius: 4px;
}
.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;
display: flex;
align-items: flex-end;
height: 100%;
line-height: 1;
}
/* 禁用状态样式 */
.van-button--disabled {
background-color: #ccc !important;
border-color: #ccc !important;
}
/* 商品项置灰 */
.product-item:has(.sold-out-overlay) {
opacity: 0.6;
}
.detail-container {
height: 100%;
}
/* 修改购物车栏样式 */
.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;
}
.total-price {
font-size: 14px;
color: #333;
font-weight: bold;
}
.checkout-btn {
background-color: #e95d5d;
border: none;
border-radius: 16px;
padding: 0 24px;
}
.cart-actions {
margin-left: auto;
align-self: flex-end;
display: flex;
align-items: center;
gap: 4px;
/* margin-left: 8px; */
.cart-count {
font-size: 12px;
min-width: 20px;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,397 @@
<script setup lang="ts">
import { useRentingCabinetStore } from "@/pinia/stores/rentingCabinet"
import { storeToRefs } from "pinia"
import { computed, onMounted, ref } from "vue"
import { publicPath } from "@/common/utils/path";
import VanPopup from "vant/es/popup"
import Cart from "./cart.vue" // Cart Detail components
// RentingDetail RentingCart components
//
// import RentingDetail from "./RentingDetail.vue" //
// import RentingCart from "./RentingCart.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);
(window as any).rentingCartItems = rentingCartItems;
// props
const activeCategory = ref(0); // cabinetName
//
const scrollContainer = ref<HTMLElement>();
//
const showCartPopup = ref(false); //
const searchQuery = ref('');
// cabinetName
function handleCategoryClick(index: number) {
activeCategory.value = index;
}
// URL
const PLACEHOLDER_IMAGE_URL = `${publicPath}` + 'img/product-image.svg'; // URL
//
function handleAddToCart(cabinetCell: any) { // CabinetCellEntity
// 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); // 1
}
// 01
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[] = []; // CabinetCellEntity[]
//
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())) // cell cabinetName
);
});
//
onMounted(() => {
rentingCabinetStore.fetchRentingCabinetDetail(props.shopId);
});
//
function handleCheckout() {
emit('checkoutRenting');
}
</script>
<template>
<div class="product-container">
<!-- 左侧分类导航 (如果需要根据柜机名称进行分类可以改造) -->
<div>
<van-button icon="revoke" type="default" class="showShopListBtn" @click="emit('backToShopList')">重选地址</van-button>
<!-- 简化分类如果不需要按cabinetName分类可以移除van-sidebar -->
<van-sidebar v-model="activeCategory" class="category-nav" @change="handleCategoryClick">
<van-sidebar-item v-for="(cabinet, index) in rentingCabinets" :key="cabinet.cabinetId" :title="cabinet.cabinetName" />
</van-sidebar>
<!-- 暂时保持一个简单的所有可租用格口的逻辑你可以根据实际需求调整分类 -->
<!-- <van-sidebar v-model="activeCategory" class="category-nav">
<van-sidebar-item title="可租用格口" />
</van-sidebar> -->
</div>
<!-- 右侧可租用格口列表 -->
<div ref="scrollContainer" class="product-list">
<van-search v-model="searchQuery" placeholder="搜索格口号或柜机名称" shape="round" class="search-box" />
<div class="category-section">
<van-cell v-for="cell in filteredRentingCells" :key="cell.cellId" class="product-item">
<template #icon>
<van-image :src="PLACEHOLDER_IMAGE_URL" width="80" height="80" class="product-image">
<!-- 已被占用或故障的格口显示 '不可租用' -->
<div v-if="cell.isRented === 1 || cell.usageStatus === 2 || cell.availableStatus === 2" class="sold-out-overlay">
<span class="sold-out-text">不可租用</span>
</div>
<template #error>
<div class="custom-error">
图片加载失败
</div>
</template>
</van-image>
</template>
<div class="product-info">
<div class="product-name van-ellipsis">
格口号: {{ cell.cellNo }}
</div>
<div class="product-price">
<!-- 如果格口有价格属性可以用 cell.cellPrice -->
<!-- 例如租金¥{{ (cell.cellPrice || 0).toFixed(2) }}/ -->
¥{{ (cell.cellPrice || 0).toFixed(2) }}
</div>
<div class="action-row">
<!-- 这里的库存概念是格口是否可用数量只有1 -->
<span v-if="cell.isRented === 0 && cell.usageStatus === 1 && cell.availableStatus === 1" class="stock-count">
可租用
</span>
<div class="cart-actions">
<!-- 数量减按钮如果已在购物车则显示 -->
<van-button v-if="getRentingCartItemCount(cell.cellId) > 0" size="mini" icon="minus"
@click.stop="handleRemoveFromCart(cell.cellId)" />
<!-- 数量显示0或1 -->
<span v-if="getRentingCartItemCount(cell.cellId) > 0" class="cart-count">{{ getRentingCartItemCount(cell.cellId) }}</span>
<!-- 数量加按钮如果未在购物车且可租用则显示 -->
<van-button size="mini" type="primary" class="add-cart-btn" icon="plus"
:disabled="getRentingCartItemCount(cell.cellId) > 0 || cell.isRented === 1 || cell.usageStatus === 2 || cell.availableStatus === 2"
@click.stop="handleAddToCart(cell)"
:style="{ opacity: (getRentingCartItemCount(cell.cellId) > 0 || cell.isRented === 1 || cell.usageStatus === 2 || cell.availableStatus === 2) ? 0.6 : 1 }"
/>
</div>
</div>
</div>
</van-cell>
</div>
</div>
<!-- 底部购物车栏 -->
<div v-if="rentingCartItems.length" class="shopping-cart-bar">
<div class="cart-info">
<van-badge :content="rentingCartTotalQuantity" @click.stop="showCartPopup = true">
<van-icon name="shopping-cart-o" size="24" />
</van-badge>
<div class="total-price">
已选格口数{{ rentingCartTotalQuantity }}
</div>
</div>
<van-button type="primary" size="small" @click="handleCheckout" class="checkout-btn">
去结算
</van-button>
</div>
</div>
<VanPopup v-model:show="showCartPopup" position="bottom" :style="{ height: '80%' }" round>
<Cart class="detail-container" @cart-close="showCartPopup = false" />
</VanPopup>
<!-- 购物车弹窗如果需要 -->
<!-- <VanPopup v-model:show="showCartPopup" position="bottom" :style="{ height: '80%' }" round>
<RentingCart class="detail-container" @cart-close="showCartPopup = false" />
</VanPopup> -->
</template>
<style scoped lang="scss">
/* 导入或复制 ProductContainer.vue 的样式 */
/* 注意:这里可以直接复制 ProductContainer.vue 中的 <style scoped lang="scss"> 的内容 */
/* 或者将公共样式抽离成CSS文件导入 */
.product-container {
display: flex;
height: 100vh;
background: #f7f8fa;
flex: 1;
min-height: 0;
position: relative;
border-top: 1px solid #e0e0e0;
/* 顶部边框 */
border-top-left-radius: 8px;
/* 左上圆角 */
border-top-right-radius: 8px;
/* 右上圆角 */
overflow: hidden;
/* 确保圆角生效 */
}
.category-nav {
width: 100px;
flex-shrink: 0;
}
.showShopListBtn {
width: 100%;
padding: 0;
border: 0;
}
.product-list {
flex: 1;
overflow-y: auto;
padding: 10px 16px 60px;
background: #ffffff;
}
.category-section {
margin-bottom: 20px;
}
.category-title {
margin: 5px;
padding: 0;
font-size: 16px;
color: #333;
font-weight: bold;
top: 0;
z-index: 1;
}
.product-item {
margin-bottom: 10px;
padding: min(2.667vw, 20px) 0;
}
.product-image {
margin-right: 12px;
border-radius: 4px;
overflow: hidden;
}
.product-info {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 80px;
position: relative;
}
.product-name {
font-size: 14px;
color: #333;
line-height: 1.4;
text-align: left;
}
.product-price {
font-size: 16px;
color: #e95d5d;
font-weight: bold;
text-align: left;
}
.add-cart-btn {
align-self: flex-end;
margin-top: 0;
}
.van-sidebar-item--select:before {
background-color: #e95d5d;
}
.van-button--primary {
background-color: #e95d5d;
border: 0;
border-color: #e95d5d;
}
.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;
z-index: 2; /* 确保在图片上层 */
}
.sold-out-text {
color: #999;
font-size: 14px;
transform: rotate(-15deg);
border: 1px solid #eee;
padding: 2px 8px;
border-radius: 4px;
}
.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;
display: flex;
align-items: flex-end;
height: 100%;
line-height: 1;
}
/* 禁用状态样式 */
.van-button--disabled {
background-color: #ccc !important;
border-color: #ccc !important;
}
/* 商品项置灰 */
.product-item:has(.sold-out-overlay) {
opacity: 0.6;
}
.detail-container {
height: 100%;
}
/* 修改购物车栏样式 */
.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;
}
.total-price {
font-size: 14px;
color: #333;
font-weight: bold;
}
.checkout-btn {
background-color: #e95d5d;
border: none;
border-radius: 16px;
padding: 0 24px;
}
.cart-actions {
margin-left: auto;
align-self: flex-end;
display: flex;
align-items: center;
gap: 4px;
.cart-count {
font-size: 12px;
min-width: 20px;
text-align: center;
}
}
</style>

View File

@ -1,45 +1,95 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCartStore } from "@/pinia/stores/cart" import { useCartStore } from "@/pinia/stores/cart";
import { Product } from "@/pinia/stores/product" import { Product } from "@/pinia/stores/product";
import { storeToRefs } from "pinia" import { storeToRefs } from "pinia";
import { showConfirmDialog } from "vant" import { showConfirmDialog } from "vant";
import { useRouter } from "vue-router" import { useRouter } from "vue-router";
import { useProductStore } from "@/pinia/stores/product"; // product store
// import { useRentingCabinetStore } from "@/pinia/stores/rentingCabinet"; // rentingCabinet store
import type { CabinetCellEntity } from "@/common/apis/cabinet/type"; //
import { publicPath } from "@/common/utils/path";
// //
const emit = defineEmits(["cartClose"]) const emit = defineEmits(["cartClose"]);
const router = useRouter() const router = useRouter();
// //
const cartStore = useCartStore() const cartStore = useCartStore();
const { cartItems, totalPrice, totalQuantity } = storeToRefs(cartStore)// const { cartItems, totalPrice, totalQuantity } = storeToRefs(cartStore);
// productStore selectedShop
const productStore = useProductStore();
const { selectedShop } = storeToRefs(productStore);
// rentingCabinetStore
const rentingCabinetStore = useRentingCabinetStore();
const { rentingCartItems, rentingCartTotalQuantity } = storeToRefs(rentingCabinetStore);
// (mode 3)
const isRentingMode = computed(() => selectedShop.value?.mode === 3);
//
const currentItems = computed(() => {
return isRentingMode.value ? rentingCartItems.value : cartItems.value;
});
//
const currentTotalQuantity = computed(() => {
return isRentingMode.value ? rentingCartTotalQuantity.value : totalQuantity.value;
});
//
function handleClose() { function handleClose() {
emit("cartClose") emit("cartClose");
}
function handleAddToCart(product: Product) {
cartStore.addToCart(product)
} }
function handleRemoveFromCart(product: Product) { //
cartStore.removeFromCart(product.cellId) function handleAddItem(item: Product | { cabinetCell: CabinetCellEntity }) {
if (isRentingMode.value) {
if ('cabinetCell' in item) {
rentingCabinetStore.addToRentingCart(item.cabinetCell);
}
} else {
if ('id' in item) { // Product
cartStore.addToCart(item);
}
}
} }
function handleClearCart() { //
function handleRemoveItem(item: Product | { cabinetCell: CabinetCellEntity }) {
if (isRentingMode.value) {
if ('cabinetCell' in item) {
rentingCabinetStore.removeFromRentingCart(item.cabinetCell.cellId);
}
} else {
if ('id' in item) { // Product
cartStore.removeFromCart(item.cellId);
}
}
}
//
function handleClearCartOrRentingCart() {
showConfirmDialog({ showConfirmDialog({
title: "确认清空购物车吗?" title: `确认清空${isRentingMode.value ? '租用' : ''}购物车吗?`
}) })
.then(() => { .then(() => {
cartStore.clearCart() if (isRentingMode.value) {
rentingCabinetStore.clearRentingCart();
} else {
cartStore.clearCart();
}
}) })
.catch(() => { .catch(() => {
// on cancel // on cancel
}) });
} }
// //
function handleCheckout() { function handleCheckout() {
handleClose() handleClose();
router.push("/product/checkout") router.push("/product/checkout");
} }
</script> </script>
@ -50,17 +100,51 @@ function handleCheckout() {
<div class="clear-header"> <div class="clear-header">
<van-button <van-button
size="small" type="primary" class="clear-btn" plain size="small" type="primary" class="clear-btn" plain
@click="handleClearCart" @click="handleClearCartOrRentingCart"
> >
清空购物车 清空购物车
</van-button> </van-button>
</div> </div>
</div> </div>
<div class="product-list"> <div class="product-list">
<!-- 根据 isRentingMode 动态渲染列表 -->
<template v-if="isRentingMode">
<van-cell v-for="item in rentingCartItems" :key="item.cabinetCell.cellId" class="product-item">
<template #icon>
<!-- 租用柜子通常没有图片或者默认图片 -->
<van-image :src="`${publicPath}` + 'img/product-image.svg'" width="80" height="80" class="product-image">
<template #error>
<div class="custom-error">
图片加载失败
</div>
</template>
</van-image>
</template>
<div class="product-info">
<div class="product-name van-ellipsis">
<!-- 租用柜子的名称可以根据 cellNo 或其他属性生成 -->
{{ `格口号 ${item.cabinetCell.cellNo}` }}
</div>
<div class="product-price">
¥{{ item.cabinetCell.cellPrice?.toFixed(2) }}
</div>
<div class="action-row">
<span class="stock-count">
<!-- 租用柜子可能没有库存概念或者显示可用数量 -->
<!-- 如果需要展示库存item.cabinetCell 需包含 stock 字段 -->
</span>
<div class="cart-actions">
<van-button size="mini" icon="minus" @click.stop="handleRemoveItem(item)" />
<span class="cart-count">{{ item.quantity }}</span>
</div>
</div>
</div>
</van-cell>
</template>
<template v-else>
<van-cell v-for="item in cartItems" :key="item.product.id" class="product-item"> <van-cell v-for="item in cartItems" :key="item.product.id" class="product-item">
<template #icon> <template #icon>
<van-image :src="item.product.image" width="80" height="80" class="product-image"> <van-image :src="item.product.image" width="80" height="80" class="product-image">
<!-- 真正的图片错误处理 -->
<template #error> <template #error>
<div class="custom-error"> <div class="custom-error">
图片加载失败 图片加载失败
@ -80,24 +164,27 @@ function handleCheckout() {
还剩{{ item.product.stock }} 还剩{{ item.product.stock }}
</span> </span>
<div class="cart-actions"> <div class="cart-actions">
<van-button size="mini" icon="minus" @click.stop="handleRemoveFromCart(item.product)" /> <van-button size="mini" icon="minus" @click.stop="handleRemoveItem(item.product)" />
<span class="cart-count">{{ item.quantity }}</span> <span class="cart-count">{{ item.quantity }}</span>
<van-button <van-button
size="mini" type="primary" class="add-cart-btn" icon="plus" size="mini" type="primary" class="add-cart-btn" icon="plus"
@click.stop="handleAddToCart(item.product)" @click.stop="handleAddItem(item.product)"
/> />
</div> </div>
</div> </div>
</div> </div>
</van-cell> </van-cell>
<!-- 底部购物车栏 --> </template>
<div v-if="cartItems.length" class="shopping-cart-bar">
<!-- 底部购物车栏根据 currentItems 的长度判断是否显示 (通用逻辑) -->
<div v-if="currentItems.length" class="shopping-cart-bar">
<div class="cart-info"> <div class="cart-info">
<van-badge :content="totalQuantity" @click.stop="handleClose()"> <van-badge :content="currentTotalQuantity" @click.stop="handleClose()">
<van-icon name="shopping-cart-o" size="24" /> <van-icon name="shopping-cart-o" size="24" />
</van-badge> </van-badge>
<div class="total-price"> <div class="total-price">
合计¥{{ totalPrice.toFixed(2) }} <!-- 总价可能在租用模式下不适用或显示其他信息, 这里简化处理 -->
合计¥{{ isRentingMode ? rentingCabinetStore.rentingCartTotalPrice : totalPrice.toFixed(2) }}
</div> </div>
</div> </div>
<van-button type="primary" size="small" @click="handleCheckout" class="checkout-btn"> <van-button type="primary" size="small" @click="handleCheckout" class="checkout-btn">
@ -109,6 +196,7 @@ function handleCheckout() {
</template> </template>
<style scoped> <style scoped>
/* 保持所有样式不变因为DOM结构尽量保持一致 */
.product-list { .product-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;

View File

@ -8,6 +8,7 @@ import { submitOrderApi } from "@/common/apis/shop";
import type { SubmitOrderRequestData, WxJsApiPreCreateResponse } from "@/common/apis/shop/type"; import type { SubmitOrderRequestData, WxJsApiPreCreateResponse } from "@/common/apis/shop/type";
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useProductStore } from "@/pinia/stores/product"; import { useProductStore } from "@/pinia/stores/product";
import { useRentingCabinetStore } from "@/pinia/stores/rentingCabinet"; // <<<<<< rentingCabinet store
// //
import { paymentMethodOptions, modeToPaymentMethodMap } from "@/common/utils/maps/payment"; import { paymentMethodOptions, modeToPaymentMethodMap } from "@/common/utils/maps/payment";
@ -16,6 +17,10 @@ const router = useRouter();
const cartStore = useCartStore(); const cartStore = useCartStore();
const { cartItems, totalPrice } = storeToRefs(cartStore); const { cartItems, totalPrice } = storeToRefs(cartStore);
// <<<<<< rentingCabinet store
const rentingCabinetStore = useRentingCabinetStore();
const { rentingCartItems, rentingCartTotalPrice } = storeToRefs(rentingCabinetStore);
const wxStore = useWxStore(); const wxStore = useWxStore();
const { openid, balance, corpidLogin, userid: qyUserid, name: qyName } = storeToRefs(wxStore); const { openid, balance, corpidLogin, userid: qyUserid, name: qyName } = storeToRefs(wxStore);
@ -24,6 +29,13 @@ const { tel, userid: ab98Userid, name } = storeToRefs(ab98UserStore);
const productStore = useProductStore(); 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 supportedPayments = computed(() => {
const shopMode = productStore.selectedShop?.mode || 0; const shopMode = productStore.selectedShop?.mode || 0;
@ -37,12 +49,18 @@ const supportedPayments = computed(() => {
// //
const selectedPayment = ref<string>(supportedPayments.value[0]?.value.toString() || '0'); const selectedPayment = ref<string>(supportedPayments.value[0]?.value.toString() || '0');
const contact = ref("");
const applyRemark = ref(""); const applyRemark = ref("");
const submitting = ref(false); const submitting = ref(false);
// <<<<<< isApproval currentItems
const isApproval = computed(() => { const isApproval = computed(() => {
// (belongType == 1)
//
if (isRentingMode.value) {
return false; //
} else {
return cartItems.value.some(item => item.product.belongType == 1); return cartItems.value.some(item => item.product.belongType == 1);
}
}); });
// value // value
@ -87,11 +105,20 @@ function callWxJsApi(paymentInfo: WxJsApiPreCreateResponse) {
} }
async function handleSubmit() { async function handleSubmit() {
if (isRentingMode.value) {
if (!rentingCartItems.value.length) {
return showConfirmDialog({
title: "提示",
message: `请先选择租用商品后再结算`
});
}
} else {
if (!cartItems.value.length) { if (!cartItems.value.length) {
return showConfirmDialog({ return showConfirmDialog({
title: "提示", title: "提示",
message: "请先选择商品后再结算" message: `请先选择商品后再结算`
}) });
}
} }
if (!openid.value) { if (!openid.value) {
@ -120,21 +147,39 @@ async function handleSubmit() {
console.log('qyUserid', qyUserid.value) console.log('qyUserid', qyUserid.value)
console.log("isInternal", isInternal) console.log("isInternal", isInternal)
// <<<<<< goodsList
let goodsListToSend;
if (isRentingMode.value) {
goodsListToSend = rentingCartItems.value.map(item => ({
quantity: item.quantity,
cellId: item.cabinetCell.cellId, // cellId
mode: productStore.selectedShop?.mode || 0, // 0
//
// goodsId/cellId
// priceprice: item.cabinetCell.cellPrice,
// price goodsId
}));
} else {
goodsListToSend = cartItems.value.map(item => ({
goodsId: item.product.id,
quantity: item.quantity,
cellId: item.product.cellId,
mode: productStore.selectedShop?.mode || 0, // 0
}));
}
const requestData: SubmitOrderRequestData = { const requestData: SubmitOrderRequestData = {
openid: openid.value, openid: openid.value,
userid: wxStore.userid, userid: wxStore.userid,
corpid: wxStore.corpid, corpid: wxStore.corpid,
goodsList: cartItems.value.map(item => ({ goodsList: goodsListToSend, // <<<<<< 使 goodsListToSend
goodsId: item.product.id,
quantity: item.quantity,
cellId: item.product.cellId,
})),
// value // value
paymentType: paymentValueToType[selectedPayment.value] || 'wechat', paymentType: paymentValueToType[selectedPayment.value] || 'wechat',
mobile: tel.value, mobile: tel.value,
name: isInternal === 2 ? qyName.value : name.value, name: isInternal === 2 ? qyName.value : name.value,
applyRemark: applyRemark.value, applyRemark: applyRemark.value,
qyUserid: wxStore.userid, qyUserid: wxStore.userid,
mode: productStore.selectedShop?.mode || 0, // 0
isInternal: isInternal isInternal: isInternal
} }
@ -148,29 +193,21 @@ async function handleSubmit() {
if (data.paymentInfo) { if (data.paymentInfo) {
await callWxJsApi(data.paymentInfo); await callWxJsApi(data.paymentInfo);
} }
} else if (selectedPayment.value == '1') { // } else if (selectedPayment.value == '1' || selectedPayment.value == '3') { // ()
wxStore.setBalance(data.newBalance || 0); wxStore.setBalance(data.newBalance || 0);
try { try {
await showConfirmDialog({ await showConfirmDialog({
title: "支付成功", title: "支付成功",
message: `借呗支付成功,剩余余额:¥${data.newBalance?.toFixed(2)}` message: `${selectedPayment.value === '1' ? '借呗' : '余额'}支付成功,剩余余额:¥${data.newBalance?.toFixed(2)}`
}) })
} catch (error) { } } catch (error) { }
} else if (selectedPayment.value == '2') { // } else if (selectedPayment.value == '2') { // ()
try { try {
await showConfirmDialog({ await showConfirmDialog({
title: "提交领用申请成功", title: "提交领用申请成功",
message: `请等待管理员审批` message: `请等待管理员审批`
}) })
} catch (error) { } } catch (error) { }
} else if (selectedPayment.value == '3') { //
wxStore.setBalance(data.newBalance || 0);
try {
await showConfirmDialog({
title: "支付成功",
message: `余额支付成功,剩余余额:¥${data.newBalance?.toFixed(2)}`
})
} catch (error) {}
} else { // } else { //
// //
} }
@ -179,7 +216,13 @@ async function handleSubmit() {
path: '/order-success', path: '/order-success',
query: { orderId: data.orderId } query: { orderId: data.orderId }
}); });
cartStore.clearCart()
// <<<<<<
if (isRentingMode.value) {
rentingCabinetStore.clearRentingCart();
} else {
cartStore.clearCart();
}
} catch (error) { } catch (error) {
if (error !== 'user_cancel') { if (error !== 'user_cancel') {
showConfirmDialog({ showConfirmDialog({
@ -198,8 +241,31 @@ async function handleSubmit() {
<van-nav-bar title="结算页面" left-text="返回" left-arrow fixed @click-left="() => $router.go(-1)" /> <van-nav-bar title="结算页面" left-text="返回" left-arrow fixed @click-left="() => $router.go(-1)" />
<div class="content-wrapper"> <div class="content-wrapper">
<!-- 原有商品列表等代码保持不动 --> <!-- 根据 isRentingMode 动态渲染列表 -->
<van-cell-group class="product-list"> <van-cell-group class="product-list">
<!-- <<<<<< 修改根据 isRentingMode 渲染不同商品列表 -->
<template v-if="isRentingMode">
<van-cell v-for="item in rentingCartItems" :key="item.cabinetCell.cellId" class="product-item">
<template #icon>
<!-- 租用机柜可以放一个默认图片或特定图标 -->
<van-image src="/img/product-image.svg" width="60" height="60" class="product-image" />
</template>
<div class="product-info">
<div class="product-name van-ellipsis">
{{ `格口号 ${item.cabinetCell.cellNo}` }}
</div>
<div class="price-row">
<span class="product-price">
¥{{ item.cabinetCell.cellPrice?.toFixed(2) }}
</span>
<span class="quantity">
×{{ item.quantity }}
</span>
</div>
</div>
</van-cell>
</template>
<template v-else>
<template v-for="item in cartItems" :key="item.product.id"> <template v-for="item in cartItems" :key="item.product.id">
<van-cell class="product-item"> <van-cell class="product-item">
<template #icon> <template #icon>
@ -226,6 +292,7 @@ async function handleSubmit() {
</div> </div>
</van-cell> </van-cell>
</template> </template>
</template>
<van-cell> <van-cell>
<van-field v-model="tel" label="手机号" placeholder="请输入联系电话" required class="tel-input" /> <van-field v-model="tel" label="手机号" placeholder="请输入联系电话" required class="tel-input" />
</van-cell> </van-cell>
@ -236,19 +303,17 @@ async function handleSubmit() {
</van-cell-group> --> </van-cell-group> -->
<!-- 支付方式 --> <!-- 支付方式 -->
<!-- <<<<<< 修改isApproval -->
<van-cell-group v-if="!isApproval" class="contact-form"> <van-cell-group v-if="!isApproval" class="contact-form">
<van-cell <van-cell v-for="method in supportedPayments" :key="method.value"
v-for="method in supportedPayments" :class="['payment-option', { selected: selectedPayment === method.value.toString(), disabled: method.value === 1 && (balance < currentTotalPrice) }]"
:key="method.value" @click="selectedPayment = method.value.toString()">
:class="['payment-option', { selected: selectedPayment === method.value.toString(), disabled: method.value === 1 && (balance < totalPrice) }]"
@click="selectedPayment = method.value.toString()"
>
<van-icon <van-icon
:name="method.value === 0 ? 'wechat' : method.value === 1 ? 'balance-o' : method.value === 2 ? 'credit-card' : 'wallet'" :name="method.value === 0 ? 'wechat' : method.value === 1 ? 'balance-o' : method.value === 2 ? 'credit-card' : 'wallet'"
class="method-icon" class="method-icon" />
/>
<span class="method-label"> <span class="method-label">
{{ method.label }} {{ method.label }}
<!-- <<<<<< 修改balance < currentTotalPrice -->
<span v-if="method.value === 1" class="balance-amount">当前¥{{ balance.toFixed(2) }}</span> <span v-if="method.value === 1" class="balance-amount">当前¥{{ balance.toFixed(2) }}</span>
</span> </span>
<div class="empty"></div> <div class="empty"></div>
@ -259,7 +324,7 @@ async function handleSubmit() {
<!-- 提交订单栏 --> <!-- 提交订单栏 -->
<div class="submit-bar"> <div class="submit-bar">
<div class="total-price"> <div class="total-price">
合计¥{{ totalPrice.toFixed(2) }} 合计¥{{ currentTotalPrice.toFixed(2) }} <!-- <<<<<< 修改使用 currentTotalPrice -->
</div> </div>
<van-button type="primary" size="large" :loading="submitting" loading-text="提交中..." @click="handleSubmit"> <van-button type="primary" size="large" :loading="submitting" loading-text="提交中..." @click="handleSubmit">
提交订单 提交订单
@ -270,6 +335,7 @@ async function handleSubmit() {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// cart.vue
.van-nav-bar { .van-nav-bar {
position: fixed; position: fixed;
top: 0; top: 0;

View File

@ -0,0 +1,76 @@
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { ref } from 'vue'
import { getRentingCabinetDetailApi } from '@/common/apis/cabinet/index'
import type { CabinetCellEntity, RentingCabinetDetailDTO } from '@/common/apis/cabinet/type'
export const useRentingCabinetStore = defineStore('rentingCabinet', () => {
const rentingCabinets = ref<RentingCabinetDetailDTO[]>([])
// 租用购物车列表
const rentingCartItems = ref<Array<{ cabinetCell: CabinetCellEntity; quantity: number }>>([])
const fetchRentingCabinetDetail = async (shopId: number) => {
const res = await getRentingCabinetDetailApi(shopId);
console.log(res)
if (res.code === 0 && res.data) {
rentingCabinets.value = res.data
}
}
// 添加到租用购物车
const addToRentingCart = (cabinetCell: CabinetCellEntity, quantity: number = 1): boolean => {
if (quantity <= 0) return false
const existingItem = rentingCartItems.value.find(item => item.cabinetCell.cellId === cabinetCell.cellId)
if (existingItem) {
existingItem.quantity += quantity
} else {
rentingCartItems.value.push({ cabinetCell, quantity })
}
return true
}
// 从购物车移除
const removeFromRentingCart = (cellId: number, quantity: number = 1) => {
const index = rentingCartItems.value.findIndex(item => item.cabinetCell.cellId === cellId)
if (index !== -1) {
if (rentingCartItems.value[index].quantity <= quantity) {
rentingCartItems.value.splice(index, 1)
} else {
rentingCartItems.value[index].quantity -= quantity
}
}
}
// 计算租用总数
const rentingCartTotalQuantity = computed(() => {
return rentingCartItems.value.reduce((total, item) => total + item.quantity, 0)
})
// 计算租用总价
const rentingCartTotalPrice = computed(() => {
return rentingCartItems.value.reduce((sum, item) => {
return sum + (item.cabinetCell.cellPrice || 0) * item.quantity
}, 0)
})
// 清空租用购物车
const clearRentingCart = () => {
rentingCartItems.value = []
}
return {
rentingCabinets,
rentingCartItems,
addToRentingCart,
removeFromRentingCart,
rentingCartTotalQuantity,
rentingCartTotalPrice,
clearRentingCart,
fetchRentingCabinetDetail
}
})
export function useRentingCabinetStoreOutside() {
return useRentingCabinetStore()
}