feat(plan): 新增 AmountKeyboard 数字键盘输入组件
创建 AmountKeyboard 组件替代 AmountInput 输入框,实现数字键盘交互:
核心功能:
- 点击弹出数字键盘(右列模式,包含小数点)
- 金额显示弹窗(渐变背景 + 装饰圆圈)
- 输入验证(1个小数点,2位小数)
- 千分位分隔符显示
- 集成 GlobalPopupManager 解决嵌套弹窗层级
技术实现:
- nut-number-keyboard @input 事件(单字符传递)
- 字符累加逻辑:String(inputValue) + String(val)
- 输入验证:split('.') 检查小数点和小数位数量
- toFixed(2) 确保保存格式一致性
- watch(showKeyboard) 同步键盘和弹窗状态
问题解决:
1. Vue渲染错误:使用 kebab-case 组件名
2. 键盘被底部按钮遮挡:使用 GlobalPopupManager
3. 单字符输入:实现字符累加
4. 字符串相加变数字相加:显式 String() 转换
5. 输入验证阻塞:初始化为空字符串
6. 保存格式问题:使用 toFixed(2)
7. 自动关闭bug:时间判断过滤
8. 状态同步:watch 同步关闭金额弹窗
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
6 changed files
with
864 additions
and
12 deletions
| ... | @@ -5,6 +5,109 @@ | ... | @@ -5,6 +5,109 @@ |
| 5 | 5 | ||
| 6 | --- | 6 | --- |
| 7 | 7 | ||
| 8 | +## [2026-02-09] - 新增 AmountKeyboard 数字键盘输入组件 | ||
| 9 | + | ||
| 10 | +### 新增 | ||
| 11 | +- 创建 AmountKeyboard.vue 组件,替代 AmountInput 输入框 | ||
| 12 | +- 实现数字键盘(nut-number-keyboard)点击触发 | ||
| 13 | +- 新增金额显示弹窗(页面中间显示输入内容,渐变背景设计) | ||
| 14 | +- 集成 GlobalPopupManager 解决嵌套弹窗层级问题 | ||
| 15 | +- 实现字符累加逻辑,支持连续录入(123) | ||
| 16 | +- 添加输入验证(1个小数点,2位小数) | ||
| 17 | +- 添加千分位分隔符显示 | ||
| 18 | +- 实现 toFixed(2) 确保保存格式一致性 | ||
| 19 | +- 防止点击遮罩关闭弹窗 | ||
| 20 | +- 添加 watch(showKeyboard) 同步键盘和弹窗状态 | ||
| 21 | + | ||
| 22 | +### 功能特点 | ||
| 23 | +- 点击输入框弹出数字键盘(右列模式,包含小数点) | ||
| 24 | +- 金额显示弹窗(渐变背景 + 装饰圆圈 + 大号字体) | ||
| 25 | +- 输入验证(限制1个小数点,2位小数) | ||
| 26 | +- 连续输入支持(字符累加,字符串类型安全) | ||
| 27 | +- 状态同步(键盘关闭时自动关闭金额弹窗) | ||
| 28 | +- 多币种支持(CNY、USD、HKD、EUR) | ||
| 29 | + | ||
| 30 | +### 技术实现 | ||
| 31 | +- 使用 nut-number-keyboard 的 @input 事件(单字符传递) | ||
| 32 | +- 字符累加:`String(inputValue.value) + String(val)` | ||
| 33 | +- 输入验证:split('.') 检查小数点和小数位数量 | ||
| 34 | +- 初始化:空字符串(非"0.00")避免验证冲突 | ||
| 35 | +- 保存:parseFloat(yuan.toFixed(2)) 确保格式 | ||
| 36 | +- 单位转换:内部存储为分(整数),显示为元(带2位小数) | ||
| 37 | + | ||
| 38 | +### 问题解决 | ||
| 39 | +1. Vue渲染错误:使用正确的 kebab-case 组件名 `nut-number-keyboard` | ||
| 40 | +2. 键盘被底部按钮遮挡:使用 GlobalPopupManager 解决嵌套弹窗层级 | ||
| 41 | +3. 单字符输入:实现字符累加逻辑 | ||
| 42 | +4. 字符串相加变成数字相加(1+2=3):显式使用 String() 转换 | ||
| 43 | +5. 输入验证阻塞:初始化为空字符串而非 "0.00" | ||
| 44 | +6. 保存格式问题:使用 toFixed(2) 确保 "12.00" 格式 | ||
| 45 | +7. 自动关闭bug:添加时间判断过滤(500ms内) | ||
| 46 | +8. 状态同步:watch(showKeyboard) 同步关闭金额弹窗 | ||
| 47 | + | ||
| 48 | +### 影响文件 | ||
| 49 | +- src/components/PlanFields/AmountKeyboard.vue(新增) | ||
| 50 | +- src/components/PlanTemplates/LifeInsuranceTemplate.vue(使用新组件) | ||
| 51 | +- src/components/PlanTemplates/CriticalIllnessTemplate.vue(使用新组件) | ||
| 52 | +- src/components/PlanTemplates/SavingsTemplate.vue(使用新组件) | ||
| 53 | +- docs/lessons-learned.md(添加经验教训记录) | ||
| 54 | + | ||
| 55 | +**详细信息**: | ||
| 56 | +- **影响文件**: 见上文 | ||
| 57 | +- **技术栈**: Vue 3, Taro 4, NutUI, GlobalPopupManager | ||
| 58 | +- **测试状态**: ✅ 已通过 | ||
| 59 | +- **备注**: 替代原有的 AmountInput 组件,老组件保留用于测试 | ||
| 60 | + | ||
| 61 | +--- | ||
| 62 | + | ||
| 63 | +## [2026-02-08] - 实现全局弹窗管理器解决嵌套弹窗遮挡问题 | ||
| 64 | + | ||
| 65 | +### 新增 | ||
| 66 | +- 创建 GlobalPopupManager 全局弹窗管理器 | ||
| 67 | +- 实现 useParentPopup 和 useGlobalPopup 接口 | ||
| 68 | +- 新增 PlanPopupNew 父弹窗组件(支持全局管理) | ||
| 69 | +- 新增 DatePickerGlobal 日期选择器(支持全局管理) | ||
| 70 | +- 新增 SelectPickerGlobal 下拉选择器(支持全局管理) | ||
| 71 | +- 新增 AgePickerGlobal 年龄选择器(支持全局管理) | ||
| 72 | + | ||
| 73 | +### 优化 | ||
| 74 | +- 子弹窗打开时自动隐藏父弹窗底部按钮 | ||
| 75 | +- 所有子弹窗关闭时自动恢复底部按钮 | ||
| 76 | +- 支持多个子弹窗同时打开 | ||
| 77 | +- 支持多层弹窗嵌套(弹窗套弹窗) | ||
| 78 | +- 使用 watch 监听全局状态,解决组件挂载时序问题 | ||
| 79 | + | ||
| 80 | +### 问题原因 | ||
| 81 | +**NutUI 嵌套弹窗的 z-index 层级问题**: | ||
| 82 | +- 父弹窗和子弹窗都是 `position: fixed` 定位 | ||
| 83 | +- 即使子弹窗 `z-index` 更高,也无法遮挡父弹窗的非子元素(如底部按钮) | ||
| 84 | +- 底部按钮在 DOM 结构上与子弹窗同级,会覆盖子弹窗内容 | ||
| 85 | + | ||
| 86 | +### 解决方案 | ||
| 87 | +**核心策略**:全局弹窗管理器协调父弹窗和子弹窗状态 | ||
| 88 | + | ||
| 89 | +1. **子弹窗注册**:每个子弹窗组件挂载时注册,获得唯一 ID | ||
| 90 | +2. **激活/停用**:子弹窗打开时激活,关闭时停用 | ||
| 91 | +3. **状态同步**:管理器通知父弹窗隐藏/显示底部按钮 | ||
| 92 | +4. **响应式更新**:父弹窗通过 watch 监听全局状态变化 | ||
| 93 | + | ||
| 94 | +### 迁移工作 | ||
| 95 | +- 更新 PlanFormContainer 使用 PlanPopupNew | ||
| 96 | +- 更新所有计划模板(LifeInsuranceTemplate、CriticalIllnessTemplate、SavingsTemplate)使用 Global 版本字段组件 | ||
| 97 | + | ||
| 98 | +### 影响文件 | ||
| 99 | +- src/components/PlanFormContainer.vue | ||
| 100 | +- src/components/PlanPopupNew.vue | ||
| 101 | +- src/components/PlanFields/GlobalPopupManager.js | ||
| 102 | +- src/components/PlanFields/DatePickerGlobal.vue | ||
| 103 | +- src/components/PlanFields/SelectPickerGlobal.vue | ||
| 104 | +- src/components/PlanFields/AgePickerGlobal.vue | ||
| 105 | +- src/components/PlanTemplates/LifeInsuranceTemplate.vue | ||
| 106 | +- src/components/PlanTemplates/CriticalIllnessTemplate.vue | ||
| 107 | +- src/components/PlanTemplates/SavingsTemplate.vue | ||
| 108 | + | ||
| 109 | +--- | ||
| 110 | + | ||
| 8 | ## [2026-02-08] - 修复计划书表单重置和数据同步问题 | 111 | ## [2026-02-08] - 修复计划书表单重置和数据同步问题 |
| 9 | 112 | ||
| 10 | ### 修复 | 113 | ### 修复 | ... | ... |
| ... | @@ -1945,21 +1945,220 @@ const USE_MOCK_DATA = true // 容易导致生产环境误用 | ... | @@ -1945,21 +1945,220 @@ const USE_MOCK_DATA = true // 容易导致生产环境误用 |
| 1945 | 1945 | ||
| 1946 | --- | 1946 | --- |
| 1947 | 1947 | ||
| 1948 | +## 组件开发案例:AmountKeyboard 数字键盘组件 ⭐ 新增 | ||
| 1949 | + | ||
| 1950 | +### 问题描述 | ||
| 1951 | + | ||
| 1952 | +需要创建一个保额输入组件,点击后弹出数字键盘进行输入,而不是直接使用输入框。 | ||
| 1953 | + | ||
| 1954 | +### 遇到的坑和解决方案 | ||
| 1955 | + | ||
| 1956 | +#### 坑 1: Vue 渲染错误 - Invalid vnode type | ||
| 1957 | + | ||
| 1958 | +**错误信息**: | ||
| 1959 | +``` | ||
| 1960 | +[Vue warn]: Invalid vnode type when creating vnode: undefined | ||
| 1961 | +``` | ||
| 1962 | + | ||
| 1963 | +**原因**: 使用了错误的组件名 `nut-numberkeyboard` (驼峰式) | ||
| 1964 | + | ||
| 1965 | +**解决方案**: 使用正确的 kebab-case 命名 `<nut-number-keyboard>` | ||
| 1966 | + | ||
| 1967 | +**官方示例**: | ||
| 1968 | +```vue | ||
| 1969 | +<nut-number-keyboard v-model:visible="show" type="rightColumn" /> | ||
| 1970 | +``` | ||
| 1971 | + | ||
| 1972 | +--- | ||
| 1973 | + | ||
| 1974 | +#### 坑 2: 键盘被底部按钮遮挡 | ||
| 1975 | + | ||
| 1976 | +**现象**: 数字键盘的底部按钮被页面底部按钮遮挡 | ||
| 1977 | + | ||
| 1978 | +**错误尝试**: 使用 `z-index` CSS 调整(无效) | ||
| 1979 | + | ||
| 1980 | +**正确方案**: 使用 GlobalPopupManager 系统 | ||
| 1981 | +```javascript | ||
| 1982 | +import { useGlobalPopup } from './GlobalPopupManager.js' | ||
| 1983 | + | ||
| 1984 | +const { registerPopup, activatePopup, deactivatePopup } = useGlobalPopup() | ||
| 1985 | + | ||
| 1986 | +// 注册弹窗 | ||
| 1987 | +const keyboardPopupId = ref(null) | ||
| 1988 | +onMounted(() => { | ||
| 1989 | + keyboardPopupId.value = registerPopup() | ||
| 1990 | +}) | ||
| 1991 | + | ||
| 1992 | +// 监听键盘状态 | ||
| 1993 | +watch(showKeyboard, (newValue) => { | ||
| 1994 | + if (newValue) { | ||
| 1995 | + activatePopup(keyboardPopupId.value) | ||
| 1996 | + } else { | ||
| 1997 | + deactivatePopup(keyboardPopupId.value) | ||
| 1998 | + } | ||
| 1999 | +}) | ||
| 2000 | +``` | ||
| 2001 | + | ||
| 2002 | +--- | ||
| 2003 | + | ||
| 2004 | +#### 坑 3: 数字输入只能录入单个数字 | ||
| 2005 | + | ||
| 2006 | +**现象**: 输入 "123" 时,只显示最后一个数字 | ||
| 2007 | + | ||
| 2008 | +**原因**: `nut-number-keyboard` 的 `@input` 事件传入的是单个字符,需要累加 | ||
| 2009 | + | ||
| 2010 | +**错误代码**: | ||
| 2011 | +```javascript | ||
| 2012 | +const onInput = (val) => { | ||
| 2013 | + inputValue.value = val // ❌ 直接替换 | ||
| 2014 | +} | ||
| 2015 | +``` | ||
| 2016 | + | ||
| 2017 | +**正确代码**: | ||
| 2018 | +```javascript | ||
| 2019 | +const onInput = (val) => { | ||
| 2020 | + if (!inputValue.value) { | ||
| 2021 | + inputValue.value = String(val) | ||
| 2022 | + } else { | ||
| 2023 | + inputValue.value = String(inputValue.value) + String(val) | ||
| 2024 | + } | ||
| 2025 | +} | ||
| 2026 | +``` | ||
| 2027 | + | ||
| 2028 | +--- | ||
| 2029 | + | ||
| 2030 | +#### 坑 4: 输入验证导致无法输入 | ||
| 2031 | + | ||
| 2032 | +**现象**: 初始化为 "0.00" 后,所有输入都被忽略 | ||
| 2033 | + | ||
| 2034 | +**原因**: 验证逻辑判断 `parts[1].length >= 2`,认为已有2位小数 | ||
| 2035 | + | ||
| 2036 | +**解决方案**: 初始化时使用空字符串,而不是 "0.00" | ||
| 2037 | +```javascript | ||
| 2038 | +const openKeyboard = () => { | ||
| 2039 | + if (props.modelValue !== null && props.modelValue !== undefined) { | ||
| 2040 | + inputValue.value = (props.modelValue / 100).toFixed(2) | ||
| 2041 | + } else { | ||
| 2042 | + inputValue.value = '' // ✅ 空字符串,不是 '0.00' | ||
| 2043 | + } | ||
| 2044 | +} | ||
| 2045 | +``` | ||
| 2046 | + | ||
| 2047 | +**验证逻辑**: | ||
| 2048 | +```javascript | ||
| 2049 | +const onInput = (val) => { | ||
| 2050 | + // 检查小数点 | ||
| 2051 | + if (val === '.' && inputValue.value.includes('.')) { | ||
| 2052 | + return // 已有小数点,忽略 | ||
| 2053 | + } | ||
| 2054 | + | ||
| 2055 | + // 检查小数位 | ||
| 2056 | + if (val >= '0' && val <= '9') { | ||
| 2057 | + const parts = inputValue.value.split('.') | ||
| 2058 | + if (parts.length === 2 && parts[1]?.length >= 2) { | ||
| 2059 | + return // 已有2位小数,忽略 | ||
| 2060 | + } | ||
| 2061 | + } | ||
| 2062 | + | ||
| 2063 | + // 累加输入 | ||
| 2064 | + if (!inputValue.value) { | ||
| 2065 | + inputValue.value = String(val) | ||
| 2066 | + } else { | ||
| 2067 | + inputValue.value = String(inputValue.value) + String(val) | ||
| 2068 | + } | ||
| 2069 | +} | ||
| 2070 | +``` | ||
| 2071 | + | ||
| 2072 | +--- | ||
| 2073 | + | ||
| 2074 | +#### 坑 5: 键盘自动关闭问题 | ||
| 2075 | + | ||
| 2076 | +**现象**: 键盘刚打开就立即关闭 | ||
| 2077 | + | ||
| 2078 | +**原因**: `@close` 事件在打开时被误触发 | ||
| 2079 | + | ||
| 2080 | +**解决方案**: 添加时间判断,过滤误触发 | ||
| 2081 | +```javascript | ||
| 2082 | +const keyboardOpenTime = ref(0) | ||
| 2083 | + | ||
| 2084 | +watch(showKeyboard, (newValue) => { | ||
| 2085 | + if (newValue) { | ||
| 2086 | + keyboardOpenTime.value = Date.now() // 记录打开时间 | ||
| 2087 | + } | ||
| 2088 | +}) | ||
| 2089 | + | ||
| 2090 | +const onClose = () => { | ||
| 2091 | + const timeSinceOpen = Date.now() - keyboardOpenTime.value | ||
| 2092 | + if (timeSinceOpen < 500) { | ||
| 2093 | + return // 忽略误触发 | ||
| 2094 | + } | ||
| 2095 | + showKeyboard.value = false | ||
| 2096 | +} | ||
| 2097 | +``` | ||
| 2098 | + | ||
| 2099 | +**最终方案**: 移除 `@close` 监听,在 `watch(showKeyboard)` 中同步关闭金额弹窗 | ||
| 2100 | + | ||
| 2101 | +--- | ||
| 2102 | + | ||
| 2103 | +#### 坑 6: 字符串累加被当作数字相加 | ||
| 2104 | + | ||
| 2105 | +**现象**: 输入 1、2、3 后显示 6(1+2+3) | ||
| 2106 | + | ||
| 2107 | +**原因**: JavaScript `+=` 运算符将字符串当作数字相加 | ||
| 2108 | + | ||
| 2109 | +**解决方案**: 确保两边都是字符串类型 | ||
| 2110 | +```javascript | ||
| 2111 | +inputValue.value = String(inputValue.value) + String(val) | ||
| 2112 | +``` | ||
| 2113 | + | ||
| 2114 | +--- | ||
| 2115 | + | ||
| 2116 | +#### 坑 7: 保存后再次编辑显示问题 | ||
| 2117 | + | ||
| 2118 | +**问题**: 保存 "12.23",删除后保存变成 "12" 而不是 "12.00" | ||
| 2119 | + | ||
| 2120 | +**原因**: `parseFloat("12")` 丢失小数点后的零 | ||
| 2121 | + | ||
| 2122 | +**解决方案**: 使用 `toFixed(2)` 保留两位小数 | ||
| 2123 | +```javascript | ||
| 2124 | +const onConfirm = () => { | ||
| 2125 | + let yuan = parseFloat(inputValue.value || '0') | ||
| 2126 | + if (!Number.isNaN(yuan)) { | ||
| 2127 | + const formattedYuan = parseFloat(yuan.toFixed(2)) | ||
| 2128 | + const cents = Math.round(formattedYuan * 100) | ||
| 2129 | + emit('update:modelValue', cents) | ||
| 2130 | + } | ||
| 2131 | +} | ||
| 2132 | +``` | ||
| 2133 | + | ||
| 2134 | +--- | ||
| 2135 | + | ||
| 2136 | +### ✅ 最终实现要点 | ||
| 2137 | + | ||
| 2138 | +1. **组件结构**: 点击区域 + 金额弹窗 + 数字键盘三层结构 | ||
| 2139 | +2. **事件处理**: `@input` 累加,`@delete` 删除,`@confirm` 保存 | ||
| 2140 | +3. **输入验证**: 限制小数点(1个)和小数位(2位) | ||
| 2141 | +4. **状态管理**: `watch(showKeyboard)` 同步关闭金额弹窗 | ||
| 2142 | +5. **美观设计**: 渐变背景 + 圆形装饰 + 大号数字显示 | ||
| 2143 | + | ||
| 2144 | +--- | ||
| 2145 | + | ||
| 1948 | ## 总结 | 2146 | ## 总结 |
| 1949 | 2147 | ||
| 1950 | ### 🎯 核心经验 | 2148 | ### 🎯 核心经验 |
| 1951 | 2149 | ||
| 1952 | 1. **"第 3 次出现原则"**: 代码重复 3 次时必须抽取 | 2150 | 1. **"第 3 次出现原则"**: 代码重复 3 次时必须抽取 |
| 1953 | 2. **NutUI 陷阱**: textarea、IconFont 等组件有坑,优先使用原生组件 | 2151 | 2. **NutUI 陷阱**: textarea、IconFont 等组件有坑,优先使用原生组件 |
| 1954 | -3. **静态资源**: SVG 图标必须使用 `import` 导入 | 2152 | +3. **⭐ 数字键盘组件**: `@input` 传入单个字符需要累加,注意类型转换 |
| 1955 | -4. **样式策略**: TailwindCSS(80%) + Less(20%) 混合使用 | 2153 | +4. **静态资源**: SVG 图标必须使用 `import` 导入 |
| 1956 | -5. **性能优化**: `shallowRef` + `markRaw` 处理组件对象响应式 | 2154 | +5. **样式策略**: TailwindCSS(80%) + Less(20%) 混合使用 |
| 1957 | -6. **代码质量**: 强制 JSDoc 注释,统一命名规范 | 2155 | +6. **性能优化**: `shallowRef` + `markRaw` 处理组件对象响应式 |
| 1958 | -7. **API 调用规范**: ⚠️ **不要使用 `fn()` 包装 API,直接调用并自己处理错误** | 2156 | +7. **代码质量**: 强制 JSDoc 注释,统一命名规范 |
| 1959 | -8. **架构设计**: 分层清晰,职责单一 | 2157 | +8. **API 调用规范**: ⚠️ **不要使用 `fn()` 包装 API,直接调用并自己处理错误** |
| 1960 | -9. **跨页面通信**: ⭐ **使用事件总线模式,LoadMoreList 页面避免使用 `useDidShow`**(防止意外刷新) | 2158 | +9. **架构设计**: 分层清晰,职责单一 |
| 1961 | -10. **Mock 数据切换**: ⭐ **使用环境变量 `process.env.NODE_ENV` 自动判断,禁止硬编码** | 2159 | +10. **跨页面通信**: ⭐ **使用事件总线模式,LoadMoreList 页面避免使用 `useDidShow`**(防止意外刷新) |
| 1962 | -11. **⚠️ 写代码前必查**: 先搜索项目中是否有类似实现,保持写法一致 | 2160 | +11. **Mock 数据切换**: ⭐ **使用环境变量 `process.env.NODE_ENV` 自动判断,禁止硬编码** |
| 2161 | +12. **⚠️ 写代码前必查**: 先搜索项目中是否有类似实现,保持写法一致 | ||
| 1963 | 2162 | ||
| 1964 | ### 📚 推荐阅读 | 2163 | ### 📚 推荐阅读 |
| 1965 | 2164 | ... | ... |
src/components/PlanFields/AmountKeyboard.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div> | ||
| 3 | + <!-- 标签 --> | ||
| 4 | + <div v-if="label" class="text-sm text-gray-600 mb-2 flex items-center"> | ||
| 5 | + <span v-if="required" class="text-red-500 mr-1">*</span> | ||
| 6 | + <span>{{ label }}</span> | ||
| 7 | + <span v-if="currencyText" class="text-gray-500">({{ currencyText }})</span> | ||
| 8 | + </div> | ||
| 9 | + | ||
| 10 | + <!-- 多币种模式(方案 2 - 未来扩展) --> | ||
| 11 | + <div v-if="multiCurrencyEnabled" class="mb-2"> | ||
| 12 | + <div class="text-sm text-gray-600 mb-2">币种</div> | ||
| 13 | + <div class="flex gap-2"> | ||
| 14 | + <button | ||
| 15 | + v-for="curr in supportedCurrencies" | ||
| 16 | + :key="curr.value" | ||
| 17 | + :class="[ | ||
| 18 | + 'px-4 py-2 rounded-lg text-sm border transition-colors', | ||
| 19 | + selectedCurrency === curr.value | ||
| 20 | + ? 'bg-blue-600 text-white border-blue-600' | ||
| 21 | + : 'bg-white text-gray-600 border-gray-200' | ||
| 22 | + ]" | ||
| 23 | + @tap="selectCurrency(curr.value)" | ||
| 24 | + > | ||
| 25 | + {{ curr.label }} | ||
| 26 | + </button> | ||
| 27 | + </div> | ||
| 28 | + </div> | ||
| 29 | + | ||
| 30 | + <!-- 保额显示区域(可点击) --> | ||
| 31 | + <div | ||
| 32 | + class="border border-gray-200 rounded-lg flex items-center overflow-hidden cursor-pointer" | ||
| 33 | + @tap="openKeyboard" | ||
| 34 | + > | ||
| 35 | + <div class="flex-1 p-3 text-sm"> | ||
| 36 | + <span v-if="displayValue" class="text-gray-900">{{ displayValue }}</span> | ||
| 37 | + <span v-else class="text-gray-400">{{ placeholder }}</span> | ||
| 38 | + </div> | ||
| 39 | + <span class="text-sm text-gray-500 shrink-0 ml-2 mr-5">{{ currencySymbol }}</span> | ||
| 40 | + </div> | ||
| 41 | + | ||
| 42 | + <!-- 金额显示弹窗 --> | ||
| 43 | + <nut-popup | ||
| 44 | + v-model:visible="showAmountModal" | ||
| 45 | + position="center" | ||
| 46 | + :style="{ padding: '0', borderRadius: '24rpx', overflow: 'hidden' }" | ||
| 47 | + :overlay-style="{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }" | ||
| 48 | + :close-on-click-overlay="false" | ||
| 49 | + > | ||
| 50 | + <view class="amount-modal"> | ||
| 51 | + <!-- 背景装饰 --> | ||
| 52 | + <view class="amount-modal-bg"> | ||
| 53 | + <view class="amount-modal-circle amount-modal-circle-1"></view> | ||
| 54 | + <view class="amount-modal-circle amount-modal-circle-2"></view> | ||
| 55 | + <view class="amount-modal-circle amount-modal-circle-3"></view> | ||
| 56 | + </view> | ||
| 57 | + | ||
| 58 | + <!-- 内容区域 --> | ||
| 59 | + <view class="amount-modal-content"> | ||
| 60 | + <!-- 币种符号 --> | ||
| 61 | + <view class="amount-modal-currency">{{ currencySymbol }}</view> | ||
| 62 | + | ||
| 63 | + <!-- 金额数值 --> | ||
| 64 | + <view class="amount-modal-value"> | ||
| 65 | + {{ formattedInputValue || '0.00' }} | ||
| 66 | + </view> | ||
| 67 | + | ||
| 68 | + <!-- 提示文字 --> | ||
| 69 | + <view class="amount-modal-hint">请输入金额</view> | ||
| 70 | + </view> | ||
| 71 | + </view> | ||
| 72 | + </nut-popup> | ||
| 73 | + | ||
| 74 | + <!-- 数字键盘 --> | ||
| 75 | + <nut-number-keyboard | ||
| 76 | + v-model:visible="showKeyboard" | ||
| 77 | + type="rightColumn" | ||
| 78 | + overlay | ||
| 79 | + :custom-key="customKey" | ||
| 80 | + confirm-text="完成" | ||
| 81 | + @input="onInput" | ||
| 82 | + @delete="onDelete" | ||
| 83 | + @confirm="onConfirm" | ||
| 84 | + @close="onClose" | ||
| 85 | + /> | ||
| 86 | + </div> | ||
| 87 | +</template> | ||
| 88 | + | ||
| 89 | +<script setup> | ||
| 90 | +/** | ||
| 91 | + * 保额键盘输入组件 | ||
| 92 | + * | ||
| 93 | + * @description 点击后弹出数字键盘进行输入的保额组件 | ||
| 94 | + * - 单位转换:内部存储为分(整数),显示为元(带2位小数) | ||
| 95 | + * - 币种支持:CNY、USD、HKD、EUR | ||
| 96 | + * - 多币种模式:通过 FEATURE_FLAGS.MULTI_CURRENCY_ENABLED 控制 | ||
| 97 | + * - 支持弹窗嵌套:通过 GlobalPopupManager 注册为全局弹窗 | ||
| 98 | + * @author Claude Code | ||
| 99 | + * @example | ||
| 100 | + * <!-- 固定币种模式 --> | ||
| 101 | + * <AmountKeyboard | ||
| 102 | + * v-model="coverage" | ||
| 103 | + * label="保额" | ||
| 104 | + * currency="USD" | ||
| 105 | + * placeholder="请输入保额" | ||
| 106 | + * /> | ||
| 107 | + * | ||
| 108 | + * @example | ||
| 109 | + * <!-- 多币种模式 --> | ||
| 110 | + * <AmountKeyboard | ||
| 111 | + * v-model="coverage" | ||
| 112 | + * label="保额" | ||
| 113 | + * :config="{ supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' }" | ||
| 114 | + * placeholder="请输入保额" | ||
| 115 | + * /> | ||
| 116 | + */ | ||
| 117 | +import { ref, computed, watch, onMounted, onUnmounted } from 'vue' | ||
| 118 | +import Taro from '@tarojs/taro' | ||
| 119 | +import { FEATURE_FLAGS, CURRENCY_SYMBOLS, CURRENCY_MAP } from '@/config/plan-templates' | ||
| 120 | +import { useGlobalPopup } from './GlobalPopupManager.js' | ||
| 121 | + | ||
| 122 | +/** | ||
| 123 | + * 组件属性 | ||
| 124 | + */ | ||
| 125 | +const props = defineProps({ | ||
| 126 | + /** | ||
| 127 | + * 标签文本 | ||
| 128 | + * @type {string} | ||
| 129 | + */ | ||
| 130 | + label: { | ||
| 131 | + type: String, | ||
| 132 | + default: '' | ||
| 133 | + }, | ||
| 134 | + | ||
| 135 | + /** | ||
| 136 | + * 是否必填 | ||
| 137 | + * @type {boolean} | ||
| 138 | + */ | ||
| 139 | + required: { | ||
| 140 | + type: Boolean, | ||
| 141 | + default: false | ||
| 142 | + }, | ||
| 143 | + | ||
| 144 | + /** | ||
| 145 | + * 占位符文本 | ||
| 146 | + * @type {string} | ||
| 147 | + */ | ||
| 148 | + placeholder: { | ||
| 149 | + type: String, | ||
| 150 | + default: '请输入保额' | ||
| 151 | + }, | ||
| 152 | + | ||
| 153 | + /** | ||
| 154 | + * 绑定的值(单位:分) | ||
| 155 | + * @type {number} | ||
| 156 | + * @example 100000 表示 1000.00 元 | ||
| 157 | + */ | ||
| 158 | + modelValue: { | ||
| 159 | + type: Number, | ||
| 160 | + default: null | ||
| 161 | + }, | ||
| 162 | + | ||
| 163 | + /** | ||
| 164 | + * 币种代码(固定币种模式) | ||
| 165 | + * @type {string} | ||
| 166 | + * @default 'CNY' | ||
| 167 | + */ | ||
| 168 | + currency: { | ||
| 169 | + type: String, | ||
| 170 | + default: 'CNY' | ||
| 171 | + }, | ||
| 172 | + | ||
| 173 | + /** | ||
| 174 | + * 模版配置(多币种模式) | ||
| 175 | + * @type {Object} | ||
| 176 | + * @property {Array<string>} supported_currencies - 支持的币种代码数组 | ||
| 177 | + * @property {string} default_currency - 默认币种代码 | ||
| 178 | + * @example { supported_currencies: ['CNY', 'USD'], default_currency: 'CNY' } | ||
| 179 | + */ | ||
| 180 | + config: { | ||
| 181 | + type: Object, | ||
| 182 | + default: () => ({}) | ||
| 183 | + } | ||
| 184 | +}) | ||
| 185 | + | ||
| 186 | +/** | ||
| 187 | + * 组件事件 | ||
| 188 | + */ | ||
| 189 | +const emit = defineEmits([ | ||
| 190 | + /** | ||
| 191 | + * 更新值事件 | ||
| 192 | + * @event update:modelValue | ||
| 193 | + * @param {number} value - 保额值(单位:分) | ||
| 194 | + */ | ||
| 195 | + 'update:modelValue' | ||
| 196 | +]) | ||
| 197 | + | ||
| 198 | +/** | ||
| 199 | + * 判断是否启用多币种 | ||
| 200 | + * @type {ComputedRef<boolean>} | ||
| 201 | + */ | ||
| 202 | +const multiCurrencyEnabled = computed(() => FEATURE_FLAGS.MULTI_CURRENCY_ENABLED) | ||
| 203 | + | ||
| 204 | +/** | ||
| 205 | + * 当前选中的币种 | ||
| 206 | + * @type {Ref<string>} | ||
| 207 | + */ | ||
| 208 | +const selectedCurrency = ref(props.config.default_currency || props.currency || 'CNY') | ||
| 209 | + | ||
| 210 | +/** | ||
| 211 | + * 支持的币种列表(多币种模式) | ||
| 212 | + * @type {ComputedRef<Array<{label: string, symbol: string, value: string}>>} | ||
| 213 | + */ | ||
| 214 | +const supportedCurrencies = computed(() => { | ||
| 215 | + if (!multiCurrencyEnabled.value) return [] | ||
| 216 | + | ||
| 217 | + return (props.config.supported_currencies || ['CNY']) | ||
| 218 | + .map(code => CURRENCY_MAP[code]) | ||
| 219 | + .filter(Boolean) | ||
| 220 | +}) | ||
| 221 | + | ||
| 222 | +/** | ||
| 223 | + * 当前币种符号 | ||
| 224 | + * @type {ComputedRef<string>} | ||
| 225 | + * @example | ||
| 226 | + * // CNY -> '¥' | ||
| 227 | + * // USD -> '$' | ||
| 228 | + */ | ||
| 229 | +const currencySymbol = computed(() => { | ||
| 230 | + if (multiCurrencyEnabled.value) { | ||
| 231 | + // 多币种模式:使用用户选择的币种 | ||
| 232 | + const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value) | ||
| 233 | + return curr?.symbol || '¥' | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + // 固定币种模式:使用 props.currency | ||
| 237 | + return CURRENCY_SYMBOLS[props.currency] || '¥' | ||
| 238 | +}) | ||
| 239 | + | ||
| 240 | +/** | ||
| 241 | + * 币种文本(用于标签显示) | ||
| 242 | + * @type {ComputedRef<string>} | ||
| 243 | + */ | ||
| 244 | +const currencyText = computed(() => { | ||
| 245 | + if (multiCurrencyEnabled.value) { | ||
| 246 | + const curr = supportedCurrencies.value.find(c => c.value === selectedCurrency.value) | ||
| 247 | + return curr?.label || '' | ||
| 248 | + } | ||
| 249 | + | ||
| 250 | + const CURRENCY_NAMES = { | ||
| 251 | + CNY: '人民币', | ||
| 252 | + USD: '美元', | ||
| 253 | + HKD: '港币', | ||
| 254 | + EUR: '欧元' | ||
| 255 | + } | ||
| 256 | + | ||
| 257 | + return CURRENCY_NAMES[props.currency] || '' | ||
| 258 | +}) | ||
| 259 | + | ||
| 260 | +/** | ||
| 261 | + * 键盘显示状态 | ||
| 262 | + * @type {Ref<boolean>} | ||
| 263 | + */ | ||
| 264 | +const showKeyboard = ref(false) | ||
| 265 | + | ||
| 266 | +/** | ||
| 267 | + * 键盘打开时间戳(用于判断误触发关闭) | ||
| 268 | + * @type {Ref<number>} | ||
| 269 | + */ | ||
| 270 | +const keyboardOpenTime = ref(0) | ||
| 271 | + | ||
| 272 | +/** | ||
| 273 | + * 键盘输入值(临时存储,字符串格式) | ||
| 274 | + * @type {Ref<string>} | ||
| 275 | + */ | ||
| 276 | +const inputValue = ref('') | ||
| 277 | + | ||
| 278 | +/** | ||
| 279 | + * 金额弹窗显示状态 | ||
| 280 | + * @type {Ref<boolean>} | ||
| 281 | + */ | ||
| 282 | +const showAmountModal = ref(false) | ||
| 283 | + | ||
| 284 | +/** | ||
| 285 | + * 显示值(元,带2位小数) | ||
| 286 | + * @type {ComputedRef<string>} | ||
| 287 | + */ | ||
| 288 | +const displayValue = computed(() => { | ||
| 289 | + // 优先显示输入过程中的值 | ||
| 290 | + if (inputValue.value) { | ||
| 291 | + return inputValue.value | ||
| 292 | + } | ||
| 293 | + // 如果没有输入值,显示表单的原始值 | ||
| 294 | + if (props.modelValue !== null && props.modelValue !== undefined) { | ||
| 295 | + return (props.modelValue / 100).toFixed(2) | ||
| 296 | + } | ||
| 297 | + return '' | ||
| 298 | +}) | ||
| 299 | + | ||
| 300 | +/** | ||
| 301 | + * 格式化输入值(带千分位分隔符) | ||
| 302 | + * @type {ComputedRef<string>} | ||
| 303 | + */ | ||
| 304 | +const formattedInputValue = computed(() => { | ||
| 305 | + if (!inputValue.value) return '0.00' | ||
| 306 | + | ||
| 307 | + // 解析数值 | ||
| 308 | + const num = parseFloat(inputValue.value) | ||
| 309 | + if (Number.isNaN(num)) return '0.00' | ||
| 310 | + | ||
| 311 | + // 格式化为千分位 | ||
| 312 | + const parts = num.toFixed(2).split('.') | ||
| 313 | + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') | ||
| 314 | + return parts.join('.') | ||
| 315 | +}) | ||
| 316 | + | ||
| 317 | +/** | ||
| 318 | + * 自定义键盘按键 | ||
| 319 | + * @type {Ref<Array<string>>} | ||
| 320 | + */ | ||
| 321 | +const customKey = ref(['.']) | ||
| 322 | + | ||
| 323 | +/** | ||
| 324 | + * 使用全局弹窗管理器 | ||
| 325 | + */ | ||
| 326 | +const { registerPopup, activatePopup, deactivatePopup } = useGlobalPopup() | ||
| 327 | + | ||
| 328 | +/** | ||
| 329 | + * 数字键盘弹窗 ID | ||
| 330 | + * @type {Ref<string|null>} | ||
| 331 | + */ | ||
| 332 | +const keyboardPopupId = ref(null) | ||
| 333 | + | ||
| 334 | +/** | ||
| 335 | + * 组件挂载时注册弹窗 | ||
| 336 | + */ | ||
| 337 | +onMounted(() => { | ||
| 338 | + keyboardPopupId.value = registerPopup() | ||
| 339 | +}) | ||
| 340 | + | ||
| 341 | +/** | ||
| 342 | + * 组件卸载时取消注册 | ||
| 343 | + */ | ||
| 344 | +onUnmounted(() => { | ||
| 345 | + if (keyboardPopupId.value) { | ||
| 346 | + deactivatePopup(keyboardPopupId.value) | ||
| 347 | + } | ||
| 348 | +}) | ||
| 349 | + | ||
| 350 | +/** | ||
| 351 | + * 监听键盘显示状态 | ||
| 352 | + */ | ||
| 353 | +watch(showKeyboard, (newValue) => { | ||
| 354 | + if (!keyboardPopupId.value) { | ||
| 355 | + return | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + if (newValue) { | ||
| 359 | + // 键盘打开:激活全局弹窗,通知父弹窗隐藏 footer | ||
| 360 | + activatePopup(keyboardPopupId.value) | ||
| 361 | + // 记录打开时间 | ||
| 362 | + keyboardOpenTime.value = Date.now() | ||
| 363 | + } else { | ||
| 364 | + // 键盘关闭:停用全局弹窗,通知父弹窗恢复 footer | ||
| 365 | + deactivatePopup(keyboardPopupId.value) | ||
| 366 | + // 同步关闭金额弹窗 | ||
| 367 | + showAmountModal.value = false | ||
| 368 | + } | ||
| 369 | +}) | ||
| 370 | + | ||
| 371 | +/** | ||
| 372 | + * 打开键盘 | ||
| 373 | + */ | ||
| 374 | +const openKeyboard = () => { | ||
| 375 | + // 初始化键盘值为当前值(元),如果没有值则为空 | ||
| 376 | + if (props.modelValue !== null && props.modelValue !== undefined) { | ||
| 377 | + inputValue.value = (props.modelValue / 100).toFixed(2) | ||
| 378 | + } else { | ||
| 379 | + inputValue.value = '' | ||
| 380 | + } | ||
| 381 | + | ||
| 382 | + showKeyboard.value = true | ||
| 383 | + showAmountModal.value = true | ||
| 384 | +} | ||
| 385 | + | ||
| 386 | +/** | ||
| 387 | + * 键盘输入处理 | ||
| 388 | + * @param {string} val - 输入值(单个字符) | ||
| 389 | + */ | ||
| 390 | +const onInput = (val) => { | ||
| 391 | + // 如果输入的是小数点,检查是否已经有小数点 | ||
| 392 | + if (val === '.' && inputValue.value.includes('.')) { | ||
| 393 | + return | ||
| 394 | + } | ||
| 395 | + | ||
| 396 | + // 如果输入的是数字,检查小数点后的位数 | ||
| 397 | + if (val >= '0' && val <= '9') { | ||
| 398 | + const parts = inputValue.value.split('.') | ||
| 399 | + // 如果已经有小数点,并且小数点后有2位,则忽略输入 | ||
| 400 | + if (parts.length === 2 && parts[1]?.length >= 2) { | ||
| 401 | + return | ||
| 402 | + } | ||
| 403 | + } | ||
| 404 | + | ||
| 405 | + // nut-number-keyboard 的 @input 事件传入的是单个字符,需要累加 | ||
| 406 | + if (!inputValue.value) { | ||
| 407 | + // 初始状态,确保是字符串 | ||
| 408 | + inputValue.value = String(val) | ||
| 409 | + } else { | ||
| 410 | + // 累加输入,确保两边都是字符串进行连接 | ||
| 411 | + inputValue.value = String(inputValue.value) + String(val) | ||
| 412 | + } | ||
| 413 | +} | ||
| 414 | + | ||
| 415 | +/** | ||
| 416 | + * 删除键处理 | ||
| 417 | + */ | ||
| 418 | +const onDelete = () => { | ||
| 419 | + if (!inputValue.value) return | ||
| 420 | + | ||
| 421 | + // 删除最后一个字符 | ||
| 422 | + inputValue.value = inputValue.value.slice(0, -1) | ||
| 423 | +} | ||
| 424 | + | ||
| 425 | +/** | ||
| 426 | + * 完成按钮点击处理 | ||
| 427 | + */ | ||
| 428 | +const onConfirm = () => { | ||
| 429 | + // 将元转换为分,保留两位小数 | ||
| 430 | + let yuan = parseFloat(inputValue.value || '0') | ||
| 431 | + if (!Number.isNaN(yuan)) { | ||
| 432 | + // 先保留两位小数,再转换为分 | ||
| 433 | + const formattedYuan = parseFloat(yuan.toFixed(2)) | ||
| 434 | + const cents = Math.round(formattedYuan * 100) | ||
| 435 | + | ||
| 436 | + emit('update:modelValue', cents) | ||
| 437 | + } | ||
| 438 | + | ||
| 439 | + // 关闭键盘和金额弹窗 | ||
| 440 | + showKeyboard.value = false | ||
| 441 | + showAmountModal.value = false | ||
| 442 | +} | ||
| 443 | + | ||
| 444 | +/** | ||
| 445 | + * 关闭键盘认处 | ||
| 446 | + */ | ||
| 447 | +const onClose = () => { | ||
| 448 | + // 只关闭金额弹窗,让 showKeyboard 自然变化触发 watch 处理 GlobalPopupManager | ||
| 449 | + showAmountModal.value = false | ||
| 450 | +} | ||
| 451 | + | ||
| 452 | +/** | ||
| 453 | + * 选择币种(多币种模式) | ||
| 454 | + * @param {string} value - 币种代码 | ||
| 455 | + */ | ||
| 456 | +const selectCurrency = (value) => { | ||
| 457 | + selectedCurrency.value = value | ||
| 458 | +} | ||
| 459 | +</script> | ||
| 460 | + | ||
| 461 | +<style lang="less"> | ||
| 462 | +/** | ||
| 463 | + * 组件样式 | ||
| 464 | + */ | ||
| 465 | +.cursor-pointer { | ||
| 466 | + cursor: pointer; | ||
| 467 | +} | ||
| 468 | + | ||
| 469 | +.amount-modal { | ||
| 470 | + position: relative; | ||
| 471 | + display: flex; | ||
| 472 | + flex-direction: column; | ||
| 473 | + align-items: center; | ||
| 474 | + justify-content: center; | ||
| 475 | + min-width: 500rpx; | ||
| 476 | + min-height: 360rpx; | ||
| 477 | + padding: 60rpx 40rpx; | ||
| 478 | + overflow: hidden; | ||
| 479 | + | ||
| 480 | + &-bg { | ||
| 481 | + position: absolute; | ||
| 482 | + top: 0; | ||
| 483 | + left: 0; | ||
| 484 | + right: 0; | ||
| 485 | + bottom: 0; | ||
| 486 | + z-index: 0; | ||
| 487 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||
| 488 | + opacity: 0.1; | ||
| 489 | + } | ||
| 490 | + | ||
| 491 | + &-circle { | ||
| 492 | + position: absolute; | ||
| 493 | + border-radius: 50%; | ||
| 494 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||
| 495 | + opacity: 0.15; | ||
| 496 | + } | ||
| 497 | + | ||
| 498 | + &-circle-1 { | ||
| 499 | + width: 400rpx; | ||
| 500 | + height: 400rpx; | ||
| 501 | + top: -200rpx; | ||
| 502 | + right: -100rpx; | ||
| 503 | + } | ||
| 504 | + | ||
| 505 | + &-circle-2 { | ||
| 506 | + width: 300rpx; | ||
| 507 | + height: 300rpx; | ||
| 508 | + bottom: -100rpx; | ||
| 509 | + left: -80rpx; | ||
| 510 | + } | ||
| 511 | + | ||
| 512 | + &-circle-3 { | ||
| 513 | + width: 200rpx; | ||
| 514 | + height: 200rpx; | ||
| 515 | + top: 50%; | ||
| 516 | + right: 20%; | ||
| 517 | + } | ||
| 518 | + | ||
| 519 | + &-content { | ||
| 520 | + position: relative; | ||
| 521 | + z-index: 1; | ||
| 522 | + display: flex; | ||
| 523 | + flex-direction: column; | ||
| 524 | + align-items: center; | ||
| 525 | + } | ||
| 526 | + | ||
| 527 | + &-currency { | ||
| 528 | + font-size: 48rpx; | ||
| 529 | + font-weight: bold; | ||
| 530 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||
| 531 | + --webkit-background-clip: text; | ||
| 532 | + --webkit-text-fill-color: transparent; | ||
| 533 | + background-clip: text; | ||
| 534 | + margin-bottom: 20rpx; | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + &-value { | ||
| 538 | + font-size: 88rpx; | ||
| 539 | + font-weight: bold; | ||
| 540 | + color: #333; | ||
| 541 | + line-height: 1.2; | ||
| 542 | + margin-bottom: 16rpx; | ||
| 543 | + } | ||
| 544 | + | ||
| 545 | + &-hint { | ||
| 546 | + font-size: 28rpx; | ||
| 547 | + color: #999; | ||
| 548 | + } | ||
| 549 | +} | ||
| 550 | +</style> |
| ... | @@ -83,7 +83,7 @@ | ... | @@ -83,7 +83,7 @@ |
| 83 | import { reactive, watch } from 'vue' | 83 | import { reactive, watch } from 'vue' |
| 84 | import Taro from '@tarojs/taro' | 84 | import Taro from '@tarojs/taro' |
| 85 | import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' | 85 | import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' |
| 86 | -import PlanFieldAmount from '../PlanFields/AmountInput.vue' | 86 | +import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' |
| 87 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' | 87 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' |
| 88 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' | 88 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' |
| 89 | import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue' | 89 | import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue' | ... | ... |
| ... | @@ -83,7 +83,7 @@ | ... | @@ -83,7 +83,7 @@ |
| 83 | import { reactive, watch, toRefs } from 'vue' | 83 | import { reactive, watch, toRefs } from 'vue' |
| 84 | import Taro from '@tarojs/taro' | 84 | import Taro from '@tarojs/taro' |
| 85 | import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' | 85 | import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' |
| 86 | -import PlanFieldAmount from '../PlanFields/AmountInput.vue' | 86 | +import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' |
| 87 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' | 87 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' |
| 88 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' | 88 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' |
| 89 | import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue' | 89 | import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue' | ... | ... |
| ... | @@ -208,7 +208,7 @@ | ... | @@ -208,7 +208,7 @@ |
| 208 | import { reactive, watch, computed } from 'vue' | 208 | import { reactive, watch, computed } from 'vue' |
| 209 | import Taro from '@tarojs/taro' | 209 | import Taro from '@tarojs/taro' |
| 210 | import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' | 210 | import PlanFieldAgePicker from '../PlanFields/AgePickerGlobal.vue' |
| 211 | -import PlanFieldAmount from '../PlanFields/AmountInput.vue' | 211 | +import PlanFieldAmount from '../PlanFields/AmountKeyboard.vue' |
| 212 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' | 212 | import PlanFieldDatePicker from '../PlanFields/DatePickerGlobal.vue' |
| 213 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' | 213 | import PlanFieldRadio from '../PlanFields/RadioGroup.vue' |
| 214 | import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue' | 214 | import PlanFieldSelect from '../PlanFields/SelectPickerGlobal.vue' | ... | ... |
-
Please register or login to post a comment