11 KiB
11 KiB
将 storage-cells-summary 组件的 message 弹窗改为 wd-popup 底部弹出
当前状态分析
组件位置
src/components/storage-cells-summary/index.vue
当前弹窗使用情况
组件使用 useMessage() 调用 wot-design-uni 的 MessageBox 组件进行弹窗交互:
-
存入流程 (
handleDepositFlow):message.alert(): 显示生成的密码(第135-140行)message.prompt(): 密码验证输入(第143-153行)
-
取出流程 (
handleRetrieveFlow):message.prompt(): 输入取出密码(第204-210行)
现有 wd-popup 使用模式
从代码库中找到的示例:
src/components/position-edit/index.vue: 底部弹出 + 安全区配置src/pages/index/components/product-container.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>
需求总结
- 将
message.alert和message.prompt调用改为自定义wd-popup组件 - 使用底部弹出位置 (
position="bottom") - 启用底部安全区 (
safe-area-inset-bottom="true") - 自定义弹窗内容展示
- 保持现有业务逻辑不变
设计方案
方案一:创建独立弹窗子组件
优点: 代码结构清晰,易于维护 缺点: 需要创建新文件,增加组件复杂度
方案二:在当前组件内实现多个弹窗状态
优点: 简单直接,无需新增文件 缺点: 组件内部状态增多,模板复杂度增加
推荐方案二,因为弹窗逻辑相对简单,且与当前组件紧密耦合。
状态机设计
根据用户要求采用状态机模式管理弹窗流程。设计以下状态:
状态定义
type PopupState =
| { type: 'idle' } // 空闲状态
| { type: 'password-show', password: string } // 显示密码
| { type: 'password-verify', correctPassword: string } // 密码验证输入
| { type: 'retrieve-input' } // 取出密码输入
| { type: 'processing', action: 'deposit' | 'retrieve' } // 处理中
状态转换
-
存入流程:
idle→password-show(显示生成的密码)password-show→password-verify(用户点击"已记住")password-verify→processing(验证通过,打开格口)processing→idle(完成)
-
取出流程:
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. 实现状态机
// 状态定义
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. 重构业务流程
// 存入流程(状态机驱动)
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 模板
<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 中添加弹窗相关样式:
.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;
}
}
实施步骤
-
准备阶段
- 备份当前文件
- 分析现有弹窗调用的所有参数和返回值
-
状态变量添加
- 在 script setup 中添加弹窗状态变量
- 创建弹窗控制函数
-
模板修改
- 添加 wd-popup 组件模板
- 设计弹窗内容布局
- 添加输入框和按钮
-
业务流程重构
- 替换
message.alert调用 - 替换
message.prompt调用 - 更新异步流程处理
- 替换
-
样式适配
- 添加弹窗内容样式
- 调整响应式布局
-
测试验证
- 测试存入流程弹窗
- 测试取出流程弹窗
- 验证底部安全区效果
注意事项
- 向后兼容: 保持现有的
emit('deposit')等事件发射 - 错误处理: 保留现有的错误处理逻辑
- 用户体验: 保持相似的交互流程和提示信息
- 性能考虑: 使用
lazy-render(默认启用)避免不必要的渲染
风险与缓解
- 流程中断: 仔细测试每个弹窗转换点,确保业务流程完整
- 样式不一致: 参考现有弹窗样式,保持视觉统一
- 输入验证: 确保密码验证逻辑正确移植
预期结果
- 所有弹窗改为底部弹出的
wd-popup - 启用底部安全区适配
- 业务功能完全正常
- 用户体验保持一致