feat(暂存模式): 新增智能柜暂存模式功能
添加暂存模式(模式5)相关功能实现,包括: 1. 在shop类型中扩展暂存模式定义 2. 新增storageCabinet pinia store管理暂存状态 3. 实现暂存柜格口选择组件 4. 更新支付方式映射关系 5. 添加运行模式文档说明
This commit is contained in:
parent
4ec292edb3
commit
944ec6b605
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
@ -44,3 +44,4 @@ src/types
|
|||
|
||||
# 更新 uni-app 官方版本
|
||||
# npx @dcloudio/uvm@latest
|
||||
/.claude
|
||||
|
|
|
|||
|
|
@ -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*
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
/** 获取出租中的智能柜详情接口 */
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
// 获取购物车中某个格口的数量(应为0或1)
|
||||
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>
|
||||
|
|
@ -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) {
|
||||
// 借还柜:显示mode不为5的shop
|
||||
return shopList.value.filter(shop => shop.mode !== 5)
|
||||
} else {
|
||||
// 暂存柜:显示mode为5的shop
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -11,4 +11,5 @@ export const modeToPaymentMethodMap: Record<number, number[]> = {
|
|||
2: [0, 1],
|
||||
3: [0],
|
||||
4: [2],
|
||||
5: [],
|
||||
};
|
||||
Loading…
Reference in New Issue