feat(storage-cells-summary): 重构弹窗状态管理为状态机模式

将 message 弹窗改为 wd-popup 底部弹出,使用状态机管理弹窗流程
新增 usePopupState hook 管理弹窗状态和业务逻辑
移除 useMessage 依赖,实现自定义底部弹窗交互
This commit is contained in:
dzq 2025-12-30 16:24:34 +08:00
parent 364cba07b9
commit 48ece1eeae
4 changed files with 1225 additions and 119 deletions

View File

@ -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
<wd-popup
v-model="showPopup"
position="bottom"
:safe-area-inset-bottom="true"
custom-style="border-top-left-radius: 24rpx; border-top-right-radius: 24rpx; padding: 0; background: #fff; max-height: 90vh;"
@close="handleClose">
<!-- 自定义内容 -->
</wd-popup>
```
## 需求总结
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<PopupState>({ 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
<wd-popup
:model-value="popupVisible"
position="bottom"
:safe-area-inset-bottom="true"
custom-style="border-top-left-radius: 24rpx; border-top-right-radius: 24rpx; padding: 32rpx; background: #fff; max-height: 80vh;"
@update:model-value="handlePopupCancel">
<view class="popup-content">
<!-- 标题 -->
<text class="popup-title">{{ popupTitle }}</text>
<!-- 消息内容(支持换行) -->
<text class="popup-message">{{ popupMessage }}</text>
<!-- 密码输入框(格子模式) -->
<wd-password-input
v-if="currentState.type === 'password-verify' || currentState.type === 'retrieve-input'"
v-model="popupInputValue"
:length="passwordLength"
:gutter="8"
:mask="true"
:focus="true"
class="popup-password-input"
@change="handlePasswordInputChange" />
<!-- 处理中状态提示 -->
<view v-if="currentState.type === 'processing'" class="processing-state">
<wd-loading type="ring" />
<text class="processing-text">
{{ currentState.action === 'deposit' ? '正在存入...' : '正在取出...' }}
</text>
</view>
<view class="popup-actions">
<!-- 取消按钮 -->
<wd-button
v-if="showCancelButton"
type="default"
block
@click="handlePopupCancel">
取消
</wd-button>
<!-- 确认按钮 -->
<wd-button
type="primary"
block
@click="handlePopupConfirm"
:disabled="(currentState.type === 'password-verify' || currentState.type === 'retrieve-input') && popupInputValue.length !== passwordLength">
{{ popupConfirmText }}
</wd-button>
</view>
</view>
</wd-popup>
```
### 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`
- 启用底部安全区适配
- 业务功能完全正常
- 用户体验保持一致

View File

@ -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<boolean>` | 数据加载状态 |
| `error` | `Ref<Error \| null>` | 错误信息 |
| `cellsData` | `Ref<AvailableStorageCellDTO[]>` | 格口数据列表 |
| `selectedCellType` | `Ref<number>` | 当前选中的格口类型 |
| `depositLoading` | `Ref<boolean>` | 存入操作加载状态 |
| `retrieveLoading` | `Ref<boolean>` | 取出操作加载状态 |
| `generatedPassword` | `Ref<string>` | 生成的密码(向后兼容) |
### 计算属性
| 属性名 | 说明 |
|--------|------|
| `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<PopupState>; // 当前弹窗状态
popupInputValue: Ref<string>; // 弹窗输入框的值
passwordLength: number; // 密码长度固定为4
popupVisible: ComputedRef<boolean>; // 弹窗显示状态
keyboardVisible: Ref<boolean>; // 键盘显示状态
popupTitle: ComputedRef<string>; // 弹窗标题
popupMessage: ComputedRef<string>; // 弹窗消息内容
popupConfirmText: ComputedRef<string>; // 确认按钮文本
showCancelButton: ComputedRef<boolean>; // 是否显示取消按钮
transitionTo: (state: PopupState) => void; // 状态转换函数
// ... 其他处理函数
}
```
## 使用方法
### 1. 基本使用
```vue
<template>
<storage-cells-summary
:shop-id="currentShopId"
:auto-load="true"
:show-buttons="true"
@deposit="handleDeposit"
@retrieve="handleRetrieve"
@refresh="handleRefresh"
@error="handleError"
@back-to-address-select="handleBack"
/>
</template>
```
### 2. 仅展示模式
```vue
<template>
<storage-cells-summary
:shop-id="currentShopId"
:show-buttons="false"
/>
</template>
```
### 3. 手动控制数据加载
```vue
<template>
<storage-cells-summary
ref="storageRef"
:shop-id="currentShopId"
:auto-load="false"
/>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const storageRef = ref()
// 手动刷新数据
function refreshData() {
storageRef.value?.refresh()
}
</script>
```
## 样式与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
**组件版本**:基于当前代码实现
**维护者**:项目开发团队

View File

@ -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<Error | null>(null)
const cellsData = ref<AvailableStorageCellDTO[]>([])
const selectedCellType = ref<number>(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(() => {
</wd-row>
</view>
<!-- MessageBox 组件 -->
<wd-message-box />
<!-- wd-popup 弹窗 -->
<wd-popup
:model-value="popupVisible"
position="bottom"
:safe-area-inset-bottom="true"
custom-style="border-top-left-radius: 24rpx; border-top-right-radius: 24rpx; padding: 32rpx; background: #fff; max-height: 80vh;"
@update:model-value="handlePopupCancel">
<view class="popup-content">
<!-- 标题 -->
<text class="popup-title">{{ popupTitle }}</text>
<!-- 消息内容支持换行 -->
<text class="popup-message">{{ popupMessage }}</text>
<!-- 密码输入框格子模式 -->
<wd-password-input
v-if="currentState.type === 'password-verify' || currentState.type === 'retrieve-input'"
v-model="popupInputValue"
:length="passwordLength"
:gutter="8"
:mask="true"
:focused="keyboardVisible"
class="popup-password-input"
@focus="keyboardVisible = true"
@change="handlePasswordInputChange" />
<!-- 处理中状态提示 -->
<view v-if="currentState.type === 'processing'" class="processing-state">
<wd-loading type="ring" />
<text class="processing-text">
{{ currentState.action === 'deposit' ? '正在存入...' : '正在取出...' }}
</text>
</view>
<view class="popup-actions">
<!-- 取消按钮 -->
<wd-button
v-if="showCancelButton"
type="default"
block
@click="handlePopupCancel">
取消
</wd-button>
<!-- 确认按钮 -->
<wd-button
type="primary"
block
@click="handlePopupConfirm"
:disabled="(currentState.type === 'password-verify' || currentState.type === 'retrieve-input') && popupInputValue.length !== passwordLength">
{{ popupConfirmText }}
</wd-button>
</view>
<!-- 数字键盘显示时增加底部间距 -->
<wd-gap v-if="keyboardVisible" safe-area-bottom :height="'300rpx'" />
<!-- 数字键盘 -->
<wd-keyboard
v-model="popupInputValue"
v-model:visible="keyboardVisible"
:maxlength="passwordLength"
:modal="false"
:hide-on-click-outside="false"
:safe-area-inset-bottom="true"
close-text="完成"
/>
</view>
</wd-popup>
</view>
</template>
@ -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;
}
}
}
</style>

View File

@ -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<typeof ref<PopupState>>
/** 弹窗输入框的值,用于密码验证和取出流程 */
popupInputValue: ReturnType<typeof ref<string>>
/** 密码长度,固定为 4 位 */
passwordLength: number
/** 弹窗显示状态,根据当前状态自动计算 */
popupVisible: ReturnType<typeof computed<boolean>>
/** 键盘显示状态 */
keyboardVisible: ReturnType<typeof ref<boolean>>
/** 弹窗标题,根据当前状态自动计算 */
popupTitle: ReturnType<typeof computed<string>>
/** 弹窗消息内容,根据当前状态自动计算 */
popupMessage: ReturnType<typeof computed<string>>
/** 确认按钮文本,根据当前状态自动计算 */
popupConfirmText: ReturnType<typeof computed<string>>
/** 是否显示取消按钮,根据当前状态自动计算 */
showCancelButton: ReturnType<typeof computed<boolean>>
/** 状态转换函数,用于切换弹窗状态 */
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<PopupState>({ 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
}
}