feat(暂存模式): 新增智能柜暂存模式功能

添加暂存模式(模式5)相关功能实现,包括:
1. 在shop类型中扩展暂存模式定义
2. 新增storageCabinet pinia store管理暂存状态
3. 实现暂存柜格口选择组件
4. 更新支付方式映射关系
5. 添加运行模式文档说明
This commit is contained in:
dzq 2025-12-18 11:52:29 +08:00
parent 4ec292edb3
commit 944ec6b605
10 changed files with 940 additions and 57 deletions

View File

@ -1,30 +0,0 @@
{
"permissions": {
"allow": [
"Bash(tree -L 3 -I 'node_modules' src/)",
"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(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')",
"Bash(ls -la 'E:\\\\code\\\\智柜宝\\\\wx\\\\src\\\\pages\\\\me')",
"Bash(mkdir -p 'E:\\\\code\\\\智柜宝\\\\wx\\\\src\\\\pages\\\\me')",
"Bash(ls -la 'E:\\\\code\\\\智柜宝\\\\wx\\\\static')",
"Bash(ls -la 'E:\\\\code\\\\智柜宝\\\\wx\\\\src\\\\static')",
"Bash(tree -L 3 -I 'node_modules|dist' /E/code/智柜宝/wx)",
"Bash(tree 'E:\\code\\智柜宝\\wx\\src\\pages\\order' -L 3)",
"Bash(pnpm type-check)",
"Bash(ls -la 'E:\\\\code\\\\智柜宝\\\\wx\\\\src\\\\pages\\\\index\\\\components')",
"Bash(ls -la 'E:\\\\code\\\\智柜宝\\\\wx\\\\doc\\\\thirdParty\\\\src\\\\pages\\\\product\\\\components')",
"Bash(mkdir -p \"E:\\code\\智柜宝\\wx\\src\\pages\\rental\")",
"Bash(cat /E/code/智柜宝/wx/README.md)",
"Bash(cat /E/code/智柜宝/wx/package.json)",
"Bash(cat /E/code/智柜宝/wx/manifest.config.ts)",
"Bash(tree:*)"
],
"deny": [],
"ask": []
}
}

1
.gitignore vendored
View File

@ -44,3 +44,4 @@ src/types
# 更新 uni-app 官方版本
# npx @dcloudio/uvm@latest
/.claude

View File

@ -0,0 +1,240 @@
# Shop运行模式文档
本文档详细说明项目中Shop的mode运行模式相关处理逻辑。运行模式决定了商店的业务流程、支付方式以及用户交互方式。
## 运行模式定义
`src/api/shop/types.ts` 中定义了Shop实体和运行模式
```typescript
export interface ShopEntity {
/** 主键ID */
shopId: number;
/** 商店名称 */
shopName: string;
/** 企业微信id */
corpid: string;
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式) */
mode?: number;
/** 借呗支付1-正常使用 0-禁止使用) */
balanceEnable?: number;
/** 封面图URL */
coverImg?: string;
}
```
运行模式的具体含义:
| 模式值 | 模式名称 | 说明 |
|--------|----------|------|
| 0 | 支付模式 | 用户直接支付购买商品 |
| 1 | 审批模式 | 需要审批流程后才能购买 |
| 2 | 借还模式 | 借还物品模式 |
| 3 | 会员模式 | 会员租用模式 |
| 4 | 耗材模式 | 耗材领用模式 |
## 支付方式映射
不同运行模式支持不同的支付方式,映射关系定义在 `src/utils/maps/payment.ts`
```typescript
export const modeToPaymentMethodMap: Record<number, number[]> = {
0: [0], // 支付模式:微信支付
1: [0, 1], // 审批模式:微信支付、借呗支付
2: [0, 1], // 借还模式:微信支付、借呗支付
3: [0], // 会员模式:微信支付
4: [2], // 耗材模式:要呗支付
};
```
支付方式的枚举值:
- `0`: 微信支付
- `1`: 借呗支付
- `2`: 要呗支付
## 业务逻辑处理
### 1. 首页模式切换 (`pages/index/index.vue`)
首页根据选中的商店模式决定显示内容:
```typescript
// 当选择商店时触发
if (selectedShop.mode == 3) {
// 会员模式:进入租用模式
rentingCabinetStore.fetchRentingCabinetDetail(selectedShopId);
} else {
// 其他模式:获取普通商品列表
productStore.getGoods(selectedShopId);
}
```
### 2. 结账页面处理 (`pages/index/checkout.vue`)
结账页面使用 `isRentingMode` 计算属性区分模式:
```typescript
// 是否为租用模式(会员模式)
const isRentingMode = computed(() => {
return selectedShop.value?.mode === 3;
});
```
根据模式渲染不同的商品列表:
- 普通模式:显示普通购物车商品
- 租用模式:显示租用购物车商品
支付方式动态确定:
```typescript
// 根据模式获取支持的支付方式
const paymentMethodList = computed(() => {
const mode = selectedShop.value?.mode || 0;
const methods = modeToPaymentMethodMap[mode] || [0];
return methods;
});
```
订单提交时传递模式参数:
```typescript
// 在 SubmitOrderRequestData 中包含 mode 字段
const submitData: SubmitOrderRequestData = {
// ... 其他字段
mode: selectedShop.value?.mode || 0,
goodsList: [
{
// ... 商品信息
mode: selectedShop.value?.mode || 0, // 每个商品项也包含 mode
}
]
};
```
### 3. 购物车管理 (`pages/index/components/cart.vue`)
购物车组件支持两种模式:
- 普通购物车:用于模式 0,1,2,4
- 租用购物车:用于模式 3
提供统一的购物车操作方法,内部根据模式区分处理。
### 4. 租用模式专门处理 (`pages/index/components/renting-cabinet-container.vue`)
专门为会员模式(租用模式)设计的组件,提供:
- 租用格口的选择界面
- 不同尺寸格口的展示使用SVG图标
- 加入租用购物车功能
- 租用时长和费用计算
## 相关API端点
### 获取店铺列表
```
GET /shop/list
```
支持根据mode参数过滤店铺。
### 提交订单
```
POST /order/submit
```
请求体需要包含 `mode` 字段,表示订单的运行模式。
### 订单数据模型
`SubmitOrderRequestData` 中:
```typescript
export interface SubmitOrderRequestData {
// ... 其他字段
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式) */
mode: number;
/** 订单商品明细列表 */
goodsList: Array<{
// ... 商品信息
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式) */
mode: number;
}>;
}
```
## 组件和页面
### 主要页面
1. **首页 (`pages/index/index.vue`)**
- 模式切换和渲染
- 根据模式调用不同的数据获取方法
2. **结账页面 (`pages/index/checkout.vue`)**
- 模式支付处理
- 订单提交逻辑
### 主要组件
1. **购物车组件 (`pages/index/components/cart.vue`)**
- 模式购物车管理
- 统一的操作接口
2. **租用柜格容器组件 (`pages/index/components/renting-cabinet-container.vue`)**
- 租用模式专门UI
- 格口选择和租用逻辑
3. **商品容器组件 (`pages/index/components/product-container.vue`)**
- 普通商品展示
- 适用于非租用模式
## 模式处理流程图
```mermaid
graph TD
A[用户访问首页] --> B{选择商店}
B --> C[获取商店信息]
C --> D{检查商店模式}
D -->|模式=3| E[进入租用模式]
D -->|模式≠3| F[进入普通模式]
E --> E1[显示租用柜格容器]
E --> E2[使用租用购物车]
E --> E3[租用结算流程]
F --> F1[显示商品容器]
F --> F2[使用普通购物车]
F --> F3[普通结算流程]
E3 --> G[提交订单]
F3 --> G
G --> H{根据模式确定支付方式}
H --> I[完成订单]
```
## 开发注意事项
1. **模式判断一致性**
- 始终使用 `selectedShop.value?.mode` 获取当前模式
- 模式判断应集中在业务逻辑入口处
2. **支付方式配置**
- 修改支付方式映射时更新 `payment.ts` 文件
- 确保前端支付方式与后端一致
3. **租用模式特殊处理**
- 模式3有专门的组件和流程
- 租用购物车与普通购物车数据分离
4. **扩展新模式**
- 在 `types.ts` 中更新模式注释
- 更新 `payment.ts` 中的支付方式映射
- 根据需要添加新的业务逻辑分支
## 常见问题
**Q: 如何添加新的运行模式?**
A: 1. 在 `types.ts` 中更新模式定义注释2. 在 `payment.ts` 中添加支付方式映射3. 在业务逻辑中添加新的处理分支。
**Q: 模式如何影响支付方式?**
A: 通过 `modeToPaymentMethodMap` 映射关系确定,前端根据当前模式过滤可用的支付方式。
**Q: 租用模式与其他模式的主要区别?**
A: 租用模式使用独立的购物车系统、专门的UI组件和不同的结算逻辑。
---
*文档最后更新2025-12-15*
*维护者Claude Code*

View File

@ -1,10 +1,10 @@
import { http } from "@/http/http";
import type { CabinetDetailResponse, RentingCabinetDetailDTO } from './types';
import type { CabinetDetailDTO, RentingCabinetDetailDTO } from './types';
import type { OpenCabinetApiData } from '@/api/shop/types';
/** 获取智能柜详情接口 */
export async function getCabinetDetailApi(shopId: number) {
return await http.get<CabinetDetailResponse>("cabinet/detail", { shopId });
return await http.get<CabinetDetailDTO[]>("cabinet/detail", { shopId });
}
/** 获取出租中的智能柜详情接口 */

View File

@ -48,6 +48,8 @@ export interface CabinetCellEntity {
availableStatus: number
/** 商品ID */
goodsId?: number
/** 密码(暂存模式使用) */
password?: string
}
export interface CellInfoDTO {
@ -63,8 +65,4 @@ export interface ProductInfoDTO {
goodsName: string
price: number
coverImg: string
}
export type CabinetDetailResponse = {
data: CabinetDetailDTO[]
}

View File

@ -71,14 +71,14 @@ export interface SubmitOrderRequestData {
/** 是否内部订单 0否 1汇邦云用户 2企业微信用户 */
isInternal: number;
applyRemark: string;
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式 */
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式 5-暂存模式 */
mode: number;
/** 订单商品明细列表 */
goodsList: Array<{
goodsId?: number
quantity: number
cellId: number
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式 */
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式 5-暂存模式 */
mode: number;
}>;
/** 是否微信小程序订单 0否 1是 */
@ -230,7 +230,7 @@ export interface ShopEntity {
shopName: string;
/** 企业微信id */
corpid: string;
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式 */
/** 运行模式0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式 5-暂存模式 */
mode?: number;
/** 借呗支付1-正常使用 0-禁止使用) */
balanceEnable?: number;

View File

@ -0,0 +1,500 @@
<script setup lang="ts">
import { useStorageCabinetStore } from "@/pinia/stores/storageCabinet"
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: 'checkoutStorage'): void; //
}>();
//
const storageCabinetStore = useStorageCabinetStore();
const { storageCabinets, storageCartItems, storagePassword, storageCartTotalQuantity, storageCartTotalPrice } = storeToRefs(storageCabinetStore);
// 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 = storageCartItems.value.find(item => item.cabinetCell.cellId === cabinetCell.cellId);
if (!existingItem) {
storageCabinetStore.addToStorageCart(cabinetCell, 1);
}
}
//
function handleRemoveFromCart(cellId: number) {
storageCabinetStore.removeFromStorageCart(cellId, 1);
}
// 01
function getStorageCartItemCount(cellId: number) {
const item = storageCartItems.value.find(item => item.cabinetCell.cellId === cellId);
return item ? item.quantity : 0;
}
//
const filteredStorageCells = computed(() => {
let cells: any[] = [];
//
if (storageCabinets.value.length > 0) {
if (activeCategory.value >= 0 && activeCategory.value < storageCabinets.value.length) {
//
cells = storageCabinets.value[activeCategory.value].cells || [];
} else {
//
cells = storageCabinets.value.flatMap(cabinet => cabinet.cells || []);
}
}
// (isRented = 1) (usageStatus = 2) (availableStatus = 2)
/* const availableCells = cells.filter(cell =>
cell.isRented === 0 && cell.usageStatus === 1 && cell.availableStatus === 1
); */
const availableCells = cells;
//
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()))
);
});
//
function copyPassword() {
if (!storagePassword.value) return;
uni.setClipboardData({
data: storagePassword.value,
success: () => {
uni.showToast({ title: '密码已复制', icon: 'success' });
},
fail: () => {
uni.showToast({ title: '复制失败', icon: 'error' });
}
});
}
//
onMounted(() => {
storageCabinetStore.fetchStorageCabinetDetail(props.shopId);
});
//
function handleCheckout() {
emit('checkoutStorage');
}
//
function showCartDetail() {
showCartPopup.value = true;
}
//
function switchCellImage(cellType: number) {
switch (cellType) {
case 1: return '/static/svg/product-image-small.svg';
case 2: return '/static/svg/product-image-medium.svg';
case 3: return '/static/svg/product-image-large.svg';
case 4: return '/static/svg/product-image-super-large.svg';
default: return '/static/svg/product-image-unknow.svg';
}
}
</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 storageCabinets" :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>
<!-- 密码显示区域如果有密码 -->
<view v-if="storagePassword" class="password-display">
<view class="password-title">暂存密码</view>
<view class="password-value">{{ storagePassword }}</view>
<button size="mini" type="primary" @click="copyPassword" class="copy-btn">复制</button>
<view class="password-tip">请记住此密码取件时需输入</view>
</view>
<scroll-view scroll-y class="category-section">
<view v-for="cell in filteredStorageCells" :key="cell.cellId" class="product-item">
<view class="product-info-wrapper">
<view class="product-image-wrapper">
<image :src="switchCellImage(cell.cellType)" 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">
免费
</view>
<view class="action-row">
<view v-if="cell.isRented === 0 && cell.usageStatus === 1 && cell.availableStatus === 1" class="stock-count">
可暂存
</view>
<view v-else class="stock-count">
不可暂存
</view>
<view v-if="cell.isRented === 0 && cell.usageStatus === 1 && cell.availableStatus === 1" class="cart-actions">
<!-- 数量减按钮如果已在购物车则显示 -->
<button v-if="getStorageCartItemCount(cell.cellId) > 0" size="mini" class="cart-btn minus-btn" @click.stop="handleRemoveFromCart(cell.cellId)">-</button>
<!-- 数量显示0或1 -->
<text v-if="getStorageCartItemCount(cell.cellId) > 0" class="cart-count">{{ getStorageCartItemCount(cell.cellId) }}</text>
<!-- 数量加按钮如果未在购物车且可暂存则显示 -->
<button size="mini" type="primary" class="add-cart-btn"
:disabled="getStorageCartItemCount(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="storageCartItems.length" class="shopping-cart-bar" @click="showCartDetail">
<view class="cart-info">
<wd-badge :model-value="storageCartTotalQuantity" right="0" top="0">
<wd-icon name="cart" size="24px"></wd-icon>
</wd-badge>
<text class="total-price">
合计{{ storageCartTotalQuantity }} 免费
</text>
</view>
<view class="checkout-btn" @click.stop="handleCheckout">
确认暂存
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.product-container {
display: flex;
height: calc(100vh - 150px);
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;
position: relative;
}
.search-wrapper {
padding: 10px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.search-box {
padding: 8px 12px;
background: #f5f5f5;
border-radius: 20px;
font-size: 14px;
text-align: center;
}
/* 密码显示区域 */
.password-display {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 10px;
padding: 16px;
border-radius: 12px;
color: white;
text-align: center;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
.password-title {
font-size: 14px;
opacity: 0.9;
margin-bottom: 8px;
}
.password-value {
font-size: 28px;
font-weight: bold;
letter-spacing: 4px;
margin: 12px 0;
font-family: monospace;
background: rgba(255, 255, 255, 0.1);
padding: 8px 16px;
border-radius: 8px;
display: inline-block;
}
.copy-btn {
margin: 8px auto;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
}
.password-tip {
font-size: 12px;
opacity: 0.8;
margin-top: 8px;
}
}
.category-section {
flex: 1;
overflow-y: auto;
padding: 10px;
padding-bottom: 55px;
}
.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;
}
.total-price {
font-size: 14px;
color: #333;
font-weight: bold;
}
.checkout-btn {
background-color: #F56C6C;
border: none;
border-radius: 16px;
padding: 0 24px;
height: 36px;
line-height: 36px;
color: #fff;
font-size: 14px;
}
</style>

View File

@ -8,11 +8,13 @@ import { getShopListApi } from '@/api/shop'
import type { ShopEntity } from '@/api/shop/types'
import ProductContainer from './components/product-container.vue';
import RentingCabinetContainer from './components/renting-cabinet-container.vue';
import StorageCabinetContainer from './components/storage-cabinet-container.vue';
import { generateDynamicCode, getWxUserByOpenid, mpCodeToOpenId } from '@/api/users'
import { toHttpsUrl, uniLogin } from '@/utils'
import { useAb98UserStore } from '@/pinia/stores/ab98-user'
import { storeToRefs } from 'pinia'
import { useWxParamsStore } from '@/pinia/stores/wx-params'
import { useStorageCabinetStore } from '@/pinia/stores/storageCabinet'
definePage({
style: {
@ -25,6 +27,7 @@ const wxStore = useWxStore()
const productStore = useProductStore()
const cartStore = useCartStore()
const rentingCabinetStore = useRentingCabinetStore()
const storageCabinetStore = useStorageCabinetStore()
const ab98UserStore = useAb98UserStore()
//
@ -32,6 +35,26 @@ const showShopList = ref<boolean>(true)
const shopList = ref<ShopEntity[]>([])
const shopId = ref<number>(0)
// tabs
const activeTab = ref<number>(0)
const tabs = [
{ title: '借还柜', value: 0 },
{ title: '暂存柜', value: 1 }
]
// activeTab
const filteredShopList = computed(() => {
if (!shopList.value.length) return []
if (activeTab.value === 0) {
// mode5shop
return shopList.value.filter(shop => shop.mode !== 5)
} else {
// mode5shop
return shopList.value.filter(shop => shop.mode === 5)
}
})
//
const selectedShopMode = computed(() => {
if (!shopId.value) return 0
@ -41,6 +64,8 @@ const selectedShopMode = computed(() => {
//
const isRentingMode = computed(() => selectedShopMode.value === 3)
//
const isStorageMode = computed(() => selectedShopMode.value === 5)
@ -58,11 +83,15 @@ function handleShopSelect(selectedShopId: number) {
if (selectedShop.mode == 3) {
//
rentingCabinetStore.fetchRentingCabinetDetail(selectedShopId)
} else if (selectedShop.mode == 5) {
//
storageCabinetStore.fetchStorageCabinetDetail(selectedShopId)
} else {
productStore.getGoods(selectedShopId)
}
cartStore.clearCart()
rentingCabinetStore.clearRentingCart()
storageCabinetStore.clearStorageCart()
}
}
@ -73,6 +102,7 @@ function backToShopList() {
productStore.categories = []
cartStore.clearCart()
rentingCabinetStore.clearRentingCart()
storageCabinetStore.clearStorageCart()
}
//
@ -89,6 +119,13 @@ function handleCheckoutRenting() {
})
}
//
function handleCheckoutStorage() {
uni.navigateTo({
url: '/pages/index/checkout'
})
}
onLoad(async (query) => {
const wxParamsStore = useWxParamsStore();
const { wxUserDTO } = storeToRefs(wxStore);
@ -169,27 +206,40 @@ onShow(async () => {
<!-- 店铺选择列表内容 -->
<view v-if="showShopList" class="shop-list-content">
<view class="shop-prompt">
<!-- tabs切换 -->
<wd-tabs v-model="activeTab" class="shop-tabs">
<wd-tab v-for="tab in tabs" :key="tab.value" :title="tab.title" :name="tab.value"></wd-tab>
</wd-tabs>
<!-- <view class="shop-prompt">
<view class="prompt-text">请选择机柜地址</view>
</view>
</view> -->
<view class="shop-row">
<view v-for="shop in shopList" :key="shop.shopId" class="shop-col" @click="handleShopSelect(shop.shopId)">
<view class="shop-item">
<wd-img
:src="toHttpsUrl(shop.coverImg) || '/static/product-image.png'"
width="100%"
height="80"
mode="aspectFill"
></wd-img>
<view class="shop-info">
<view class="shop-name">
<wd-icon name="shop" size="16px" color="#666"></wd-icon>
<text>{{ shop.shopName }}</text>
<template v-if="filteredShopList.length > 0">
<view v-for="shop in filteredShopList" :key="shop.shopId" class="shop-col" @click="handleShopSelect(shop.shopId)">
<view class="shop-item">
<wd-img
:src="toHttpsUrl(shop.coverImg) || '/static/product-image.png'"
width="100%"
height="80"
mode="aspectFill"
></wd-img>
<view class="shop-info">
<view class="shop-name">
<wd-icon name="shop" size="16px" color="#666"></wd-icon>
<text>{{ shop.shopName }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<template v-else>
<view class="empty-state">
<wd-icon name="info" size="48px" color="#ccc"></wd-icon>
<text class="empty-text">暂无店铺</text>
</view>
</template>
</view>
</view>
@ -200,12 +250,21 @@ onShow(async () => {
@backToShopList="backToShopList"
@checkoutRenting="handleCheckoutRenting"
/>
<!-- 暂存格口列表 -->
<StorageCabinetContainer
v-else-if="!showShopList && isStorageMode"
:shop-id="shopId"
@backToShopList="backToShopList"
@checkoutStorage="handleCheckoutStorage"
/>
<!-- 商品列表 -->
<ProductContainer
v-else-if="!showShopList && !isRentingMode"
v-else-if="!showShopList"
@backToShopList="backToShopList"
@checkout="handleCheckout"
/>
<wd-gap safe-area-bottom height="20"></wd-gap>
</view>
</template>
@ -230,6 +289,13 @@ onShow(async () => {
height: calc(100vh - 150px);
}
.shop-tabs {
margin: 8px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.shop-prompt {
margin: 8px;
padding: 12px 16px;
@ -247,12 +313,27 @@ onShow(async () => {
.shop-row {
overflow-y: auto;
overflow-x: hidden;
padding: 0 8px;
padding: 10px 8px 0 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
flex: 1;
height: calc(100vh - 150px - 60px);
height: calc(100vh - 150px - 52px);
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 200px;
.empty-text {
margin-top: 16px;
font-size: 14px;
color: #999;
}
}
}
.shop-col {

View File

@ -0,0 +1,92 @@
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { ref } from 'vue'
import { getCabinetDetailApi } from '@/api/cabinet'
import type { CabinetCellEntity, CabinetDetailDTO } from '@/api/cabinet/types'
export const useStorageCabinetStore = defineStore('storageCabinet', () => {
const storageCabinets = ref<CabinetDetailDTO[]>([])
// 暂存购物车列表
const storageCartItems = ref<Array<{ cabinetCell: CabinetCellEntity; quantity: number }>>([])
// 存储从后端接收的密码
const storagePassword = ref<string>('')
const fetchStorageCabinetDetail = async (shopId: number) => {
const res = await getCabinetDetailApi(shopId)
console.log(res)
if (res.code === 0 && res.data) {
storageCabinets.value = res.data
}
}
// 添加到暂存购物车
const addToStorageCart = (cabinetCell: CabinetCellEntity, quantity: number = 1): boolean => {
if (quantity <= 0) return false
const existingItem = storageCartItems.value.find(item => item.cabinetCell.cellId === cabinetCell.cellId)
if (existingItem) {
existingItem.quantity += quantity
} else {
storageCartItems.value.push({ cabinetCell, quantity })
}
return true
}
// 从购物车移除
const removeFromStorageCart = (cellId: number, quantity: number = 1) => {
const index = storageCartItems.value.findIndex(item => item.cabinetCell.cellId === cellId)
if (index !== -1) {
if (storageCartItems.value[index].quantity <= quantity) {
storageCartItems.value.splice(index, 1)
} else {
storageCartItems.value[index].quantity -= quantity
}
}
}
// 计算暂存总数
const storageCartTotalQuantity = computed(() => {
return storageCartItems.value.reduce((total, item) => total + item.quantity, 0)
})
// 暂存模式价格为0
const storageCartTotalPrice = computed(() => {
return 0
})
// 设置密码
const setStoragePassword = (password: string) => {
storagePassword.value = password
}
// 清空暂存购物车
const clearStorageCart = () => {
storageCartItems.value = []
}
// 重置store用于退出暂存模式时
const resetStore = () => {
storageCabinets.value = []
storageCartItems.value = []
storagePassword.value = ''
}
return {
storageCabinets,
storageCartItems,
storagePassword,
addToStorageCart,
removeFromStorageCart,
storageCartTotalQuantity,
storageCartTotalPrice,
setStoragePassword,
clearStorageCart,
resetStore,
fetchStorageCabinetDetail
}
})
export function useStorageCabinetStoreOutside() {
return useStorageCabinetStore()
}

View File

@ -11,4 +11,5 @@ export const modeToPaymentMethodMap: Record<number, number[]> = {
2: [0, 1],
3: [0],
4: [2],
5: [],
};