diff --git a/doc/plans/lively-crafting-taco.md b/doc/plans/lively-crafting-taco.md new file mode 100644 index 0000000..496f585 --- /dev/null +++ b/doc/plans/lively-crafting-taco.md @@ -0,0 +1,416 @@ +# 将 storage-cells-summary 组件的 message 弹窗改为 wd-popup 底部弹出 + +## 当前状态分析 + +### 组件位置 +`src/components/storage-cells-summary/index.vue` + +### 当前弹窗使用情况 +组件使用 `useMessage()` 调用 `wot-design-uni` 的 `MessageBox` 组件进行弹窗交互: + +1. **存入流程** (`handleDepositFlow`): + - `message.alert()`: 显示生成的密码(第135-140行) + - `message.prompt()`: 密码验证输入(第143-153行) + +2. **取出流程** (`handleRetrieveFlow`): + - `message.prompt()`: 输入取出密码(第204-210行) + +### 现有 wd-popup 使用模式 +从代码库中找到的示例: +- `src/components/position-edit/index.vue`: 底部弹出 + 安全区配置 +- `src/pages/index/components/product-container.vue`: 简单底部弹出 + +**关键配置**: +```vue + + + +``` + +## 需求总结 + +1. 将 `message.alert` 和 `message.prompt` 调用改为自定义 `wd-popup` 组件 +2. 使用底部弹出位置 (`position="bottom"`) +3. 启用底部安全区 (`safe-area-inset-bottom="true"`) +4. 自定义弹窗内容展示 +5. 保持现有业务逻辑不变 + +## 设计方案 + +### 方案一:创建独立弹窗子组件 +**优点**: 代码结构清晰,易于维护 +**缺点**: 需要创建新文件,增加组件复杂度 + +### 方案二:在当前组件内实现多个弹窗状态 +**优点**: 简单直接,无需新增文件 +**缺点**: 组件内部状态增多,模板复杂度增加 + +**推荐方案二**,因为弹窗逻辑相对简单,且与当前组件紧密耦合。 + +## 状态机设计 + +根据用户要求采用状态机模式管理弹窗流程。设计以下状态: + +### 状态定义 +```typescript +type PopupState = + | { type: 'idle' } // 空闲状态 + | { type: 'password-show', password: string } // 显示密码 + | { type: 'password-verify', correctPassword: string } // 密码验证输入 + | { type: 'retrieve-input' } // 取出密码输入 + | { type: 'processing', action: 'deposit' | 'retrieve' } // 处理中 +``` + +### 状态转换 +1. **存入流程**: + - `idle` → `password-show` (显示生成的密码) + - `password-show` → `password-verify` (用户点击"已记住") + - `password-verify` → `processing` (验证通过,打开格口) + - `processing` → `idle` (完成) + +2. **取出流程**: + - `idle` → `retrieve-input` (输入取出密码) + - `retrieve-input` → `processing` (输入完成,打开格口) + - `processing` → `idle` (完成) + +### 状态管理实现 +使用 `reactive` 或 `ref` 管理当前状态,通过状态转换函数实现流程控制。 + +## 详细实现计划 + +### 1. 移除 useMessage 导入和声明 +- 删除第5行: `import { useMessage } from 'wot-design-uni'` +- 删除第45行: `const message = useMessage()` +- 注意:其他API导入保持不变 +- wd-password-input 组件通常已全局注册,无需额外导入 + +### 2. 实现状态机 +```typescript +// 状态定义 +type PopupState = + | { type: 'idle' } + | { type: 'password-show', password: string } + | { type: 'password-verify', correctPassword: string } + | { type: 'retrieve-input' } + | { type: 'processing', action: 'deposit' | 'retrieve' } + +// 当前状态 +const currentState = ref({ type: 'idle' }) + +// 弹窗输入值 +const popupInputValue = ref('') +const passwordLength = 4 + +// 状态转换函数 +function transitionTo(state: PopupState) { + currentState.value = state + // 重置输入值当进入需要输入的弹窗 + if (state.type === 'password-verify' || state.type === 'retrieve-input') { + popupInputValue.value = '' + } +} + +// 根据状态计算弹窗显示属性 +const popupVisible = computed(() => currentState.value.type !== 'idle') + +const popupTitle = computed(() => { + const state = currentState.value + switch (state.type) { + case 'password-show': return '密码已生成' + case 'password-verify': return '密码验证' + case 'retrieve-input': return '输入密码' + default: return '' + } +}) + +const popupMessage = computed(() => { + const state = currentState.value + switch (state.type) { + case 'password-show': return `请牢记你的暂存密码为:${state.password}\n请确认已打开的柜子,放置物品后将柜子关闭。` + case 'password-verify': return '请输入刚才显示的密码进行验证' + case 'retrieve-input': return '请输入格口密码' + default: return '' + } +}) + +const popupConfirmText = computed(() => { + const state = currentState.value + switch (state.type) { + case 'password-show': return '已记住' + case 'password-verify': return '验证' + case 'retrieve-input': return '确认' + default: return '确认' + } +}) + +const showCancelButton = computed(() => { + const state = currentState.value + return state.type === 'password-verify' || state.type === 'retrieve-input' +}) +``` + +### 3. 重构业务流程 +```typescript +// 存入流程(状态机驱动) +async function handleDepositFlow() { + try { + depositLoading.value = true + + // 1. 分配格口 + const response = await storeItemApi({ + shopId: props.shopId, + cellType: selectedCellType.value + }) + + const password = response.data?.password || '' + if (!password) { + throw new Error('格口分配失败,未获取到密码') + } + + // 2. 显示密码(状态转换) + transitionTo({ type: 'password-show', password }) + + // 注意:原流程在这里等待用户点击"已记住" + // 现在改为在弹窗确认事件中处理 + + } catch (error) { + // 错误处理 + console.error('存入流程失败:', error) + uni.showToast({ + title: (error as any)?.message || '操作失败', + icon: 'error', + duration: 3000 + }) + transitionTo({ type: 'idle' }) + } finally { + depositLoading.value = false + } +} + +// 取出流程(状态机驱动) +async function handleRetrieveFlow() { + // 直接进入密码输入状态 + transitionTo({ type: 'retrieve-input' }) +} + +// 处理弹窗确认事件 +async function handlePopupConfirm() { + const state = currentState.value + + switch (state.type) { + case 'password-show': + // 点击"已记住",进入密码验证 + transitionTo({ type: 'password-verify', correctPassword: state.password }) + break + + case 'password-verify': + // 验证密码 + if (popupInputValue.value !== state.correctPassword) { + uni.showToast({ title: '密码不正确', icon: 'error' }) + return + } + // 打开格口 + await performOpenByPassword(popupInputValue.value, 'deposit') + break + + case 'retrieve-input': + // 打开格口 + await performOpenByPassword(popupInputValue.value, 'retrieve') + break + } +} + +// 处理弹窗取消/关闭事件 +function handlePopupCancel() { + transitionTo({ type: 'idle' }) +} + +// 统一的打开格口函数 +async function performOpenByPassword(password: string, action: 'deposit' | 'retrieve') { + transitionTo({ type: 'processing', action }) + + try { + await openByPassword({ + shopId: props.shopId, + password: String(password) + }) + + uni.showToast({ + title: '格口已打开', + icon: 'success' + }) + + // 刷新数据 + await refresh() + } catch (error) { + console.error('打开格口失败:', error) + uni.showToast({ + title: (error as any)?.message || '操作失败', + icon: 'error', + duration: 3000 + }) + } finally { + transitionTo({ type: 'idle' }) + } +} +``` + +### 4. 实现 wd-popup 模板 +```vue + + + + + {{ popupTitle }} + + + {{ popupMessage }} + + + + + + + + + {{ currentState.action === 'deposit' ? '正在存入...' : '正在取出...' }} + + + + + + + 取消 + + + + + {{ popupConfirmText }} + + + + +``` + +### 5. 更新样式 +在现有 SCSS 中添加弹窗相关样式: +```scss +.popup-content { + display: flex; + flex-direction: column; + gap: 32rpx; + + .popup-title { + font-size: 36rpx; + font-weight: 600; + color: #333; + text-align: center; + } + + .popup-message { + font-size: 28rpx; + color: #666; + line-height: 1.5; + white-space: pre-line; + text-align: center; + } + + .popup-password-input { + margin: 32rpx 0; + } + + .processing-state { + display: flex; + flex-direction: column; + align-items: center; + gap: 24rpx; + padding: 48rpx 0; + + .processing-text { + font-size: 28rpx; + color: #666; + } + } + + .popup-actions { + display: flex; + gap: 16rpx; + margin-top: 32rpx; + } +} +``` + +## 实施步骤 + +1. **准备阶段** + - 备份当前文件 + - 分析现有弹窗调用的所有参数和返回值 + +2. **状态变量添加** + - 在 script setup 中添加弹窗状态变量 + - 创建弹窗控制函数 + +3. **模板修改** + - 添加 wd-popup 组件模板 + - 设计弹窗内容布局 + - 添加输入框和按钮 + +4. **业务流程重构** + - 替换 `message.alert` 调用 + - 替换 `message.prompt` 调用 + - 更新异步流程处理 + +5. **样式适配** + - 添加弹窗内容样式 + - 调整响应式布局 + +6. **测试验证** + - 测试存入流程弹窗 + - 测试取出流程弹窗 + - 验证底部安全区效果 + +## 注意事项 + +1. **向后兼容**: 保持现有的 `emit('deposit')` 等事件发射 +2. **错误处理**: 保留现有的错误处理逻辑 +3. **用户体验**: 保持相似的交互流程和提示信息 +4. **性能考虑**: 使用 `lazy-render`(默认启用)避免不必要的渲染 + +## 风险与缓解 + +1. **流程中断**: 仔细测试每个弹窗转换点,确保业务流程完整 +2. **样式不一致**: 参考现有弹窗样式,保持视觉统一 +3. **输入验证**: 确保密码验证逻辑正确移植 + +## 预期结果 + +- 所有弹窗改为底部弹出的 `wd-popup` +- 启用底部安全区适配 +- 业务功能完全正常 +- 用户体验保持一致 \ No newline at end of file diff --git a/doc/暂存柜功能文档.md b/doc/暂存柜功能文档.md new file mode 100644 index 0000000..ced1e8c --- /dev/null +++ b/doc/暂存柜功能文档.md @@ -0,0 +1,374 @@ +# 暂存柜功能文档 + +## 概述 + +`storage-cells-summary` 组件是智能柜管理系统中用于暂存柜(临时存储)功能的核心组件。该组件提供以下核心功能: + +1. **可用格口展示** - 显示当前店铺/区域的可用暂存柜格口,按格口类型(小格、中格、大格、超大格)分类统计 +2. **格口选择** - 允许用户选择特定类型的格口进行物品存入 +3. **物品存入流程** - 完整的存入物品流程:分配格口 → 生成密码 → 验证密码 → 打开格口 +4. **物品取出流程** - 通过密码验证取出已暂存的物品 +5. **状态管理** - 使用状态机模式管理弹窗流程,确保业务流程完整性 + +## 功能特性 + +### 1. 数据展示 +- 实时获取并显示可用格口数量 +- 按四种格口类型分类统计:小格(1)、中格(2)、大格(3)、超大格(4) +- 显示每种类型的剩余可用数量 +- 支持刷新数据 + +### 2. 格口选择 +- 可视化格口类型卡片展示 +- 显示每种类型的SVG图标 +- 智能禁用无可用格口的类型 +- 选中状态视觉反馈 + +### 3. 存入流程 +``` +用户点击"物品暂存" → 分配选定类型格口 → 生成4位数字密码 → +显示密码给用户 → 用户确认已记住 → 输入密码验证 → +验证通过打开格口 → 刷新格口状态 +``` + +### 4. 取出流程 +``` +用户点击"物品取出" → 输入4位密码 → 验证密码 → +验证通过打开格口 → 刷新格口状态 +``` + +### 5. 用户交互 +- 加载状态提示 +- 错误状态处理与重试 +- 空状态提示 +- 弹窗式密码输入与验证 +- 数字键盘集成 + +## 组件结构 + +### 文件位置 +``` +src/components/storage-cells-summary/ +├── index.vue # 主组件文件 +└── usePopupState.ts # 弹窗状态管理hook +``` + +### 组件Props +| 属性名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `shopId` | `number` | 必填 | 店铺ID,用于获取可用格口列表 | +| `autoLoad` | `boolean` | `true` | 是否自动加载数据 | +| `showButtons` | `boolean` | `true` | 是否显示操作按钮(存入/取出) | + +### 组件Emits +| 事件名 | 参数 | 说明 | +|--------|------|------| +| `deposit` | 无 | 点击存入按钮时触发(向后兼容) | +| `retrieve` | 无 | 点击取出按钮时触发(向后兼容) | +| `refresh` | 无 | 数据刷新完成时触发 | +| `error` | `Error` | 数据加载失败时触发 | +| `backToAddressSelect` | 无 | 点击"重选地址"按钮时触发 | + +### 核心状态变量 +| 变量名 | 类型 | 说明 | +|--------|------|------| +| `loading` | `Ref` | 数据加载状态 | +| `error` | `Ref` | 错误信息 | +| `cellsData` | `Ref` | 格口数据列表 | +| `selectedCellType` | `Ref` | 当前选中的格口类型 | +| `depositLoading` | `Ref` | 存入操作加载状态 | +| `retrieveLoading` | `Ref` | 取出操作加载状态 | +| `generatedPassword` | `Ref` | 生成的密码(向后兼容) | + +### 计算属性 +| 属性名 | 说明 | +|--------|------| +| `availableCells` | 过滤出无密码的可用格口 | +| `cellTypeStats` | 按类型统计可用格口数量 | +| `hasAvailableCells` | 是否存在可用格口 | + +## 业务流程 + +### 1. 数据初始化流程 +```mermaid +graph TD + A[组件挂载] --> B{autoLoad为true?}; + B -->|是| C[调用refresh函数]; + B -->|否| D[等待手动刷新]; + C --> E[调用availableStorageCells API]; + E --> F[更新cellsData]; + F --> G[触发refresh事件]; +``` + +### 2. 物品存入流程 +```mermaid +graph TD + A[用户点击"物品暂存"] --> B[触发deposit事件]; + B --> C[调用handleDepositFlow]; + C --> D[调用storeItemApi分配格口]; + D --> E{是否获取到密码?}; + E -->|是| F[显示密码弹窗]; + E -->|否| G[显示错误提示]; + F --> H[用户点击"已记住"]; + H --> I[进入密码验证状态]; + I --> J[用户输入密码]; + J --> K[密码验证]; + K --> L{密码正确?}; + L -->|是| M[调用openByPassword打开格口]; + L -->|否| N[显示密码错误提示]; + M --> O[显示成功提示]; + O --> P[刷新格口数据]; +``` + +### 3. 物品取出流程 +```mermaid +graph TD + A[用户点击"物品取出"] --> B[触发retrieve事件]; + B --> C[显示密码输入弹窗]; + C --> D[用户输入密码]; + D --> E[调用openByPassword打开格口]; + E --> F[显示成功提示]; + F --> G[刷新格口数据]; +``` + +## API接口 + +### 依赖的API模块 +```typescript +import { + availableStorageCells, // 获取可用格口列表 + storeItemApi, // 存入物品分配格口 + openByPassword, // 根据密码打开格口 + resetByPassword // 重置格口状态(当前未使用) +} from '@/api/cabinet/index' +``` + +### 1. availableStorageCells +**功能**:获取指定店铺的可用暂存柜格口列表 + +**请求参数**: +```typescript +{ + shopId: number // 店铺ID +} +``` + +**响应数据结构**:`AvailableStorageCellDTO[]` +```typescript +interface AvailableStorageCellDTO { + cellId: number; // 格口ID + cabinetId: number; // 柜机ID + cabinetName: string; // 柜子名称 + mainboardId: number; // 主板ID + cellNo: number; // 格口号 + pinNo: number; // 针脚序号 + stock: number; // 库存数量 + cellPrice: number; // 格口租用价格 + isRented: number; // 是否已租用(0-未租用,1-已租用) + cellType: number; // 格口类型(1-小格,2-中格,3-大格,4-超大格) + usageStatus: number; // 使用状态(1-空闲,2-已占用) + availableStatus: number;// 可用状态(1-正常,2-故障) + hasPassword: boolean; // 是否有密码(true-有密码/已占用,false-无密码/可用) + goodsId: number; // 商品ID + goodsName: string; // 商品名称 + price: number; // 商品价格 + coverImg: string; // 封面图URL +} +``` + +### 2. storeItemApi +**功能**:存入物品并分配格口,生成访问密码 + +**请求参数**:`StoreItemToCellCommand` +```typescript +interface StoreItemToCellCommand { + shopId: number; // 店铺ID + cellType: number; // 格口类型(1-小格,2-中格,3-大格,4-超大格) +} +``` + +**响应数据结构**:`CabinetCellEntity` +```typescript +interface CabinetCellEntity { + cellId: number; // 格口唯一ID + cabinetId: number; // 关联柜机ID + mainboardId?: number; // 主板ID + cellNo: number; // 格口号 + pinNo: number; // 针脚序号 + stock: number; // 库存数量 + cellPrice?: number; // 格口价格 + isRented: number; // 是否已租用:0-未租用,1-已租用 + cellType: number; // 格口类型(1小格 2中格 3大格 4超大格) + usageStatus: number; // 使用状态:1空闲 2已占用 + availableStatus: number;// 可用状态:1正常 2故障 + goodsId?: number; // 商品ID + password?: string; // 密码(暂存模式使用) +} +``` + +### 3. openByPassword +**功能**:根据密码打开对应的格口 + +**请求参数**:`OpenCellByPasswordCommand` +```typescript +interface OpenCellByPasswordCommand { + shopId: number; // 店铺ID + password: string; // 格口密码 +} +``` + +**响应**:无数据,成功时HTTP状态码为200 + +## 状态管理:usePopupState + +### 状态机设计 +弹窗流程采用状态机模式,确保业务流程的完整性和一致性: + +| 状态类型 | 说明 | 显示内容 | +|----------|------|----------| +| `idle` | 空闲状态,无弹窗显示 | 无弹窗 | +| `password-show` | 显示生成的密码 | 密码显示弹窗 | +| `password-verify` | 验证用户输入的密码 | 密码输入框+验证按钮 | +| `retrieve-input` | 输入取出密码 | 密码输入框+确认按钮 | +| `processing` | 显示处理中的加载提示 | 加载动画+处理文本 | + +### 状态转换图 +```mermaid +graph LR + A[idle] -->|存入流程开始| B[password-show]; + B -->|点击"已记住"| C[password-verify]; + C -->|密码验证通过| D[processing]; + C -->|密码验证失败| C; + A -->|取出流程开始| E[retrieve-input]; + E -->|输入密码确认| D[processing]; + D -->|操作完成| A; + B -->|取消| A; + C -->|取消| A; + E -->|取消| A; +``` + +### Hook接口 +```typescript +interface UsePopupStateReturn { + currentState: Ref; // 当前弹窗状态 + popupInputValue: Ref; // 弹窗输入框的值 + passwordLength: number; // 密码长度(固定为4) + popupVisible: ComputedRef; // 弹窗显示状态 + keyboardVisible: Ref; // 键盘显示状态 + popupTitle: ComputedRef; // 弹窗标题 + popupMessage: ComputedRef; // 弹窗消息内容 + popupConfirmText: ComputedRef; // 确认按钮文本 + showCancelButton: ComputedRef; // 是否显示取消按钮 + transitionTo: (state: PopupState) => void; // 状态转换函数 + // ... 其他处理函数 +} +``` + +## 使用方法 + +### 1. 基本使用 +```vue + +``` + +### 2. 仅展示模式 +```vue + +``` + +### 3. 手动控制数据加载 +```vue + + + +``` + +## 样式与UI组件 + +### 使用的UI组件 +- `wd-popup` - 弹窗容器 +- `wd-password-input` - 密码输入框(格子模式) +- `wd-keyboard` - 数字键盘 +- `wd-button` - 按钮 +- `wd-loading` - 加载动画 +- `wd-icon` - 图标 +- `wd-row` / `wd-col` - 栅格布局 + +### 样式特点 +- 使用 `rpx` 单位适配不同屏幕 +- 响应式网格布局(2列) +- 交互状态反馈(点击效果、选中状态) +- 弹窗圆角设计(border-top-left-radius: 24rpx) +- 安全区域适配(safe-area-inset-bottom) + +## 注意事项 + +### 1. 密码安全 +- 密码为4位数字,由系统自动生成 +- 密码仅在存入流程中显示一次,用户需要牢记 +- 密码验证失败会提示错误,但不会泄露正确密码 + +### 2. 格口状态同步 +- 每次成功操作(存入/取出)后会自动刷新格口数据 +- 可用格口数量实时更新 +- 已被占用的格口(hasPassword: true)不会显示在可用列表中 + +### 3. 错误处理 +- 网络请求失败会显示错误提示 +- 用户可点击"重试"按钮重新加载 +- 操作失败会回到初始状态,不会卡在中间状态 + +### 4. 向后兼容性 +- 保留了 `deposit` 和 `retrieve` 事件发射 +- 保留了 `generatedPassword` 状态变量 +- 新增的 `handleDepositFlow` 和 `handleRetrieveFlow` 函数与原有事件处理并行 + +### 5. 性能考虑 +- 按需加载数据(autoLoad控制) +- 计算属性缓存统计结果 +- 避免不必要的重复请求 + +## 相关文件 + +- `src/components/storage-cells-summary/index.vue` - 主组件实现 +- `src/components/storage-cells-summary/usePopupState.ts` - 弹窗状态管理 +- `src/api/cabinet/index.ts` - 柜机相关API +- `src/api/cabinet/types.ts` - 数据类型定义 +- `src/static/svg/` - 格口类型SVG图标 + +--- + +**最后更新**:2025-12-22 +**组件版本**:基于当前代码实现 +**维护者**:项目开发团队 \ No newline at end of file diff --git a/src/components/storage-cells-summary/index.vue b/src/components/storage-cells-summary/index.vue index b142eec..2318cb3 100644 --- a/src/components/storage-cells-summary/index.vue +++ b/src/components/storage-cells-summary/index.vue @@ -2,7 +2,7 @@ import { availableStorageCells, storeItemApi, openByPassword, resetByPassword } from '@/api/cabinet/index' import type { AvailableStorageCellDTO } from '@/api/cabinet/types' import { ref, computed, onMounted } from 'vue' -import { useMessage } from 'wot-design-uni' +import { usePopupState } from './usePopupState' // 格口类型映射 const CELL_TYPE_MAP = { @@ -42,11 +42,29 @@ const error = ref(null) const cellsData = ref([]) const selectedCellType = ref(1) -const message = useMessage() const depositLoading = ref(false) const retrieveLoading = ref(false) const generatedPassword = ref('') +// 弹窗状态管理 +const { + currentState, + popupInputValue, + passwordLength, + popupVisible, + keyboardVisible, + popupTitle, + popupMessage, + popupConfirmText, + showCancelButton, + transitionTo, + handlePasswordInputChange, + handlePopupCancel, + handleKeyboardInput, + handleKeyboardDelete, + handleKeyboardClose +} = usePopupState() + // 统计计算属性 const availableCells = computed(() => cellsData.value.filter(cell => !cell.hasPassword) @@ -116,164 +134,105 @@ function handleBackToAddressSelect() { // 存入流程处理函数 async function handleDepositFlow() { try { - // 1. 调用 storeItemApi 分配格口 depositLoading.value = true + + // 1. 分配格口 const response = await storeItemApi({ shopId: props.shopId, cellType: selectedCellType.value }) - // 保存生成的密码(根据用户确认,接口返回包含 password 字段) - generatedPassword.value = response.data?.password || '' - - // 检查密码是否为空 - if (!generatedPassword.value) { + const password = response.data?.password || '' + if (!password) { throw new Error('格口分配失败,未获取到密码') } - // 2. 显示密码弹窗让用户记住 - await message.alert({ - title: '密码已生成', - msg: `请牢记你的暂存密码为:${generatedPassword.value}\n请确认已打开的柜子,放置物品后将柜子关闭。`, - confirmButtonText: '已记住', - closeOnClickModal: false - }) + // 保存生成的密码以向后兼容 + generatedPassword.value = password - // 3. 密码验证弹窗 - const { value: inputPassword } = await message.prompt({ - title: '密码验证', - msg: '请输入刚才显示的密码进行验证', - inputPlaceholder: '请输入密码', - closeOnClickModal: false, - inputValidate: ((value: string) => { - if (!value) return '请输入密码' - if (value !== generatedPassword.value) return '密码不正确' - return true - }) as any - }) + // 2. 显示密码(状态转换) + transitionTo({ type: 'password-show', password }) - // 4. 打开格口 - await openByPassword({ - shopId: props.shopId, - password: String(inputPassword) - }) - - // 5. 成功提示 - uni.showToast({ - title: '格口已打开', - icon: 'success' - }) - - // 6. 刷新数据 - await refresh() + // 注意:原流程在这里等待用户点击"已记住" + // 现在改为在弹窗确认事件中处理 } catch (error) { - // 处理不同类型的错误 - // 用户取消操作(message.prompt/confirm/alert 返回 'cancel' 字符串或包含 cancel 的错误) - if (error === 'cancel' || (error as any)?.message?.includes?.('cancel')) { - return - } - - // API 错误处理 - const errorMessage = (error as any)?.message || '操作失败' + // 错误处理 console.error('存入流程失败:', error) - - // 显示具体的错误信息 uni.showToast({ - title: errorMessage.length > 20 ? '操作失败' : errorMessage, + title: (error as any)?.message || '操作失败', icon: 'error', duration: 3000 }) + transitionTo({ type: 'idle' }) } finally { depositLoading.value = false } } -// 取出流程处理函数 -async function handleRetrieveFlow() { +// 弹窗相关事件处理 + +async function handlePopupConfirm() { + const state = currentState.value + + switch (state.type) { + case 'password-show': + // 点击"已记住",进入密码验证 + transitionTo({ type: 'password-verify', correctPassword: state.password }) + break + + case 'password-verify': + // 验证密码 + if (popupInputValue.value !== state.correctPassword) { + uni.showToast({ title: '密码不正确', icon: 'error' }) + return + } + // 打开格口 + await performOpenByPassword(popupInputValue.value, 'deposit') + break + + case 'retrieve-input': + // 打开格口 + await performOpenByPassword(popupInputValue.value, 'retrieve') + break + } +} + + +async function performOpenByPassword(password: string, action: 'deposit' | 'retrieve') { + transitionTo({ type: 'processing', action }) + try { - // 1. 确认取出弹窗 - // 注释掉确认弹窗,直接执行取出操作 - /* await message.confirm({ - title: '物品取出', - msg: '确认要取出物品吗?', - closeOnClickModal: false - }) */ - - // 2. 密码输入弹窗 - const { value: password } = await message.prompt({ - title: '输入密码', - msg: '请输入格口密码', - inputPlaceholder: '请输入密码', - closeOnClickModal: false, - inputType: ('password') as any - }) - - // 3. 打开格口 - retrieveLoading.value = true await openByPassword({ shopId: props.shopId, password: String(password) }) - // 4. 成功提示 uni.showToast({ title: '格口已打开', icon: 'success' }) - // 注释掉清空格口部分,打开格口即显示打开成功结束流程 - /* - // 成功提示并询问是否清空 - await message.alert({ - title: '格口已打开', - msg: '柜子已开,请取物品!如不再使用柜子请点击 "清空" 。', - confirmButtonText: '清空', - closeOnClickModal: false - }) - - // 确认清空弹窗 - await message.confirm({ - title: '确认清空', - msg: '清空后密码将不能再次使用,确认清空?', - closeOnClickModal: false - }) - - // 重置格口状态 - await resetByPassword({ - shopId: props.shopId, - password: String(password) - }) - - // 重置成功提示 - uni.showToast({ - title: '格口已清空', - icon: 'success' - }) - */ + // 刷新数据 + await refresh() } catch (error) { - // 错误处理 - // 用户取消操作(message.prompt/confirm/alert 返回 'cancel' 字符串或包含 cancel 的错误) - if (error === 'cancel' || (error as any)?.message?.includes?.('cancel')) { - return - } - - // API 错误处理 - const errorMessage = (error as any)?.message || '操作失败' - console.error('取出流程失败:', error) - - // 显示具体的错误信息 + console.error('打开格口失败:', error) uni.showToast({ - title: errorMessage.length > 20 ? '操作失败' : errorMessage, + title: (error as any)?.message || '操作失败', icon: 'error', duration: 3000 }) } finally { - retrieveLoading.value = false - await refresh() + transitionTo({ type: 'idle' }) } } +// 取出流程处理函数 +async function handleRetrieveFlow() { + // 直接进入密码输入状态 + transitionTo({ type: 'retrieve-input' }) +} + // 格口类型图标映射(需验证图标名称可用性) const CELL_TYPE_ICON_MAP = { 1: 'box', // 小格 @@ -391,8 +350,77 @@ onMounted(() => { - - + + + + + + {{ popupTitle }} + + + {{ popupMessage }} + + + + + + + + + {{ currentState.action === 'deposit' ? '正在存入...' : '正在取出...' }} + + + + + + + 取消 + + + + + {{ popupConfirmText }} + + + + + + + + + + + @@ -562,5 +590,50 @@ onMounted(() => { height: 80rpx; border-radius: 4px; } + + // 弹窗样式 + .popup-content { + display: flex; + flex-direction: column; + gap: 32rpx; + + .popup-title { + font-size: 36rpx; + font-weight: 600; + color: #333; + text-align: center; + } + + .popup-message { + font-size: 28rpx; + color: #666; + line-height: 1.5; + white-space: pre-line; + text-align: center; + } + + .popup-password-input { + margin: 32rpx 0; + } + + .processing-state { + display: flex; + flex-direction: column; + align-items: center; + gap: 24rpx; + padding: 48rpx 0; + + .processing-text { + font-size: 28rpx; + color: #666; + } + } + + .popup-actions { + display: flex; + gap: 16rpx; + margin-top: 32rpx; + } + } } \ No newline at end of file diff --git a/src/components/storage-cells-summary/usePopupState.ts b/src/components/storage-cells-summary/usePopupState.ts new file mode 100644 index 0000000..04a7461 --- /dev/null +++ b/src/components/storage-cells-summary/usePopupState.ts @@ -0,0 +1,243 @@ +/** + * 弹窗状态机管理 hook + * + * 用于管理 storage-cells-summary 组件的弹窗流程,采用状态机模式控制不同弹窗的显示和转换。 + * 支持以下弹窗状态: + * 1. 空闲状态 (idle) - 无弹窗显示 + * 2. 密码显示状态 (password-show) - 显示生成的密码 + * 3. 密码验证状态 (password-verify) - 验证用户输入的密码 + * 4. 取出输入状态 (retrieve-input) - 输入取出密码 + * 5. 处理中状态 (processing) - 显示处理中的加载提示 + * + * 通过状态机模式确保业务流程的完整性,并提供统一的弹窗控制接口。 + */ + +import { ref, computed, watch } from 'vue' + +/** + * 弹窗状态联合类型 + * 定义所有可能的弹窗状态及其携带的数据 + */ +export type PopupState = + | { type: 'idle' } // 空闲状态,无弹窗显示 + | { type: 'password-show', password: string } // 显示生成的密码 + | { type: 'password-verify', correctPassword: string } // 密码验证输入 + | { type: 'retrieve-input' } // 取出密码输入 + | { type: 'processing', action: 'deposit' | 'retrieve' } // 处理中状态 + +/** + * usePopupState hook 返回值接口 + * 定义 hook 返回的所有状态变量和函数的类型 + */ +export interface UsePopupStateReturn { + /** 当前弹窗状态,通过 ref 管理以便响应式更新 */ + currentState: ReturnType> + /** 弹窗输入框的值,用于密码验证和取出流程 */ + popupInputValue: ReturnType> + /** 密码长度,固定为 4 位 */ + passwordLength: number + /** 弹窗显示状态,根据当前状态自动计算 */ + popupVisible: ReturnType> + /** 键盘显示状态 */ + keyboardVisible: ReturnType> + /** 弹窗标题,根据当前状态自动计算 */ + popupTitle: ReturnType> + /** 弹窗消息内容,根据当前状态自动计算 */ + popupMessage: ReturnType> + /** 确认按钮文本,根据当前状态自动计算 */ + popupConfirmText: ReturnType> + /** 是否显示取消按钮,根据当前状态自动计算 */ + showCancelButton: ReturnType> + /** 状态转换函数,用于切换弹窗状态 */ + transitionTo: (state: PopupState) => void + /** 密码输入变化处理函数 */ + handlePasswordInputChange: (value: string) => void + /** 弹窗取消/关闭处理函数 */ + handlePopupCancel: () => void + /** 键盘输入处理函数 */ + handleKeyboardInput: (key: string) => void + /** 键盘删除处理函数 */ + handleKeyboardDelete: () => void + /** 键盘关闭处理函数 */ + handleKeyboardClose: () => void +} + +/** + * 弹窗状态机管理 hook + * + * 提供弹窗状态管理、状态转换、弹窗内容计算等功能 + * 使用状态机模式确保业务流程的完整性和一致性 + * + * @returns {UsePopupStateReturn} 弹窗状态管理对象,包含状态变量和控制函数 + */ +export function usePopupState(): UsePopupStateReturn { + // 当前弹窗状态,初始化为空闲状态 + const currentState = ref({ type: 'idle' }) + // 弹窗输入框的值,用于密码输入验证 + const popupInputValue = ref('') + // 键盘显示状态 + const keyboardVisible = ref(false) + // 密码长度,固定为 4 位数字密码 + const passwordLength = 4 + + /** + * 状态转换函数 + * + * 切换当前弹窗状态,并在需要时重置输入值 + * + * @param {PopupState} state - 要切换到的目标状态 + */ + function transitionTo(state: PopupState) { + currentState.value = state + + // 控制键盘显示状态 + if (state.type === 'password-verify' || state.type === 'retrieve-input') { + // 进入需要输入密码的状态时显示键盘 + keyboardVisible.value = true + // 重置输入值当进入需要输入的弹窗(密码验证或取出输入) + popupInputValue.value = '' + } else { + // 其他状态隐藏键盘 + keyboardVisible.value = false + } + } + + /** + * 弹窗显示状态计算属性 + * + * 只要当前状态不是空闲状态,就显示弹窗 + */ + const popupVisible = computed(() => currentState.value.type !== 'idle') + + /** + * 弹窗标题计算属性 + * + * 根据当前状态返回相应的标题文本 + */ + const popupTitle = computed(() => { + const state = currentState.value + switch (state.type) { + case 'password-show': return '密码已生成' + case 'password-verify': return '密码验证' + case 'retrieve-input': return '输入密码' + default: return '' + } + }) + + /** + * 弹窗消息内容计算属性 + * + * 根据当前状态返回相应的消息文本 + * 支持使用 \n 换行符进行换行 + */ + const popupMessage = computed(() => { + const state = currentState.value + switch (state.type) { + case 'password-show': return `请牢记你的暂存密码为:${state.password}\n请确认已打开的柜子,放置物品后将柜子关闭。` + case 'password-verify': return '请输入刚才显示的密码进行验证' + case 'retrieve-input': return '请输入格口密码' + default: return '' + } + }) + + /** + * 确认按钮文本计算属性 + * + * 根据当前状态返回相应的确认按钮文本 + */ + const popupConfirmText = computed(() => { + const state = currentState.value + switch (state.type) { + case 'password-show': return '已记住' + case 'password-verify': return '验证' + case 'retrieve-input': return '确认' + default: return '确认' + } + }) + + /** + * 显示取消按钮计算属性 + * + * 在需要用户输入的状态(密码验证、取出输入)显示取消按钮 + * 允许用户取消当前操作流程 + */ + const showCancelButton = computed(() => { + const state = currentState.value + return state.type === 'password-verify' || state.type === 'retrieve-input' + }) + + /** + * 密码输入变化处理函数 + * + * 处理密码输入框的变化事件,可以留空或添加额外的业务逻辑 + * 例如:输入时进行实时验证、限制输入格式等 + * + * @param {string} value - 当前输入的密码值 + */ + function handlePasswordInputChange(value: string) { + // 密码输入变化处理,可以留空或添加额外逻辑 + // 例如:实时验证密码格式、限制输入长度等 + } + + /** + * 弹窗取消/关闭处理函数 + * + * 用户点击取消按钮或关闭弹窗时调用 + * 将状态切换回空闲状态,中断当前操作流程 + */ + function handlePopupCancel() { + transitionTo({ type: 'idle' }) + } + + /** + * 键盘输入处理函数 + * + * 处理键盘按键输入,更新输入值,但不超过最大长度 + * + * @param {string} key - 输入的按键值 + */ + function handleKeyboardInput(key: string) { + if (popupInputValue.value.length < passwordLength) { + popupInputValue.value += key + } + } + + /** + * 键盘删除处理函数 + * + * 删除最后一位输入 + */ + function handleKeyboardDelete() { + if (popupInputValue.value.length > 0) { + popupInputValue.value = popupInputValue.value.slice(0, -1) + } + } + + /** + * 键盘关闭处理函数 + * + * 关闭键盘,但保持弹窗显示(用户可能点击外部关闭键盘但不想取消操作) + */ + function handleKeyboardClose() { + keyboardVisible.value = false + } + + // 返回弹窗状态管理对象 + return { + currentState, + popupInputValue, + passwordLength, + popupVisible, + keyboardVisible, + popupTitle, + popupMessage, + popupConfirmText, + showCancelButton, + transitionTo, + handlePasswordInputChange, + handlePopupCancel, + handleKeyboardInput, + handleKeyboardDelete, + handleKeyboardClose + } +} \ No newline at end of file