416 lines
11 KiB
Markdown
416 lines
11 KiB
Markdown
# 将 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`
|
||
- 启用底部安全区适配
|
||
- 业务功能完全正常
|
||
- 用户体验保持一致 |