shop-wx/doc/plans/lively-crafting-taco.md

11 KiB
Raw Permalink Blame History

将 storage-cells-summary 组件的 message 弹窗改为 wd-popup 底部弹出

当前状态分析

组件位置

src/components/storage-cells-summary/index.vue

当前弹窗使用情况

组件使用 useMessage() 调用 wot-design-uniMessageBox 组件进行弹窗交互:

  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: 简单底部弹出

关键配置:

<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.alertmessage.prompt 调用改为自定义 wd-popup 组件
  2. 使用底部弹出位置 (position="bottom")
  3. 启用底部安全区 (safe-area-inset-bottom="true")
  4. 自定义弹窗内容展示
  5. 保持现有业务逻辑不变

设计方案

方案一:创建独立弹窗子组件

优点: 代码结构清晰,易于维护 缺点: 需要创建新文件,增加组件复杂度

方案二:在当前组件内实现多个弹窗状态

优点: 简单直接,无需新增文件 缺点: 组件内部状态增多,模板复杂度增加

推荐方案二,因为弹窗逻辑相对简单,且与当前组件紧密耦合。

状态机设计

根据用户要求采用状态机模式管理弹窗流程。设计以下状态:

状态定义

type PopupState =
  | { type: 'idle' }  // 空闲状态
  | { type: 'password-show', password: string }  // 显示密码
  | { type: 'password-verify', correctPassword: string }  // 密码验证输入
  | { type: 'retrieve-input' }  // 取出密码输入
  | { type: 'processing', action: 'deposit' | 'retrieve' }  // 处理中

状态转换

  1. 存入流程:

    • idlepassword-show (显示生成的密码)
    • password-showpassword-verify (用户点击"已记住")
    • password-verifyprocessing (验证通过,打开格口)
    • processingidle (完成)
  2. 取出流程:

    • idleretrieve-input (输入取出密码)
    • retrieve-inputprocessing (输入完成,打开格口)
    • processingidle (完成)

状态管理实现

使用 reactiveref 管理当前状态,通过状态转换函数实现流程控制。

详细实现计划

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;
  }
}

实施步骤

  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
  • 启用底部安全区适配
  • 业务功能完全正常
  • 用户体验保持一致