hookehuyr

fix(plan): 修复保额输入组件问题并统一计划书模版样式

- 修复 AmountInput 组件键盘输入报错问题
- 修复快速输入时显示 [object Object] 的异常
- 优化输入体验,引入内部状态分离显示值与模型值
- 修复标签和币种提示分两行显示的样式问题
- 统一计划书模版组件间距,增强视觉一致性
- 为重疾保险模版添加配置缺失提示

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -63,7 +63,8 @@ ...@@ -63,7 +63,8 @@
63 "Bash(~/.bun/bin/bun --version)", 63 "Bash(~/.bun/bin/bun --version)",
64 "Bash(npm run dev:weapp:*)", 64 "Bash(npm run dev:weapp:*)",
65 "Bash(__NEW_LINE_19c6a134b9496225__ echo \"✅ 已删除不再使用的 Apifox 相关脚本\")", 65 "Bash(__NEW_LINE_19c6a134b9496225__ echo \"✅ 已删除不再使用的 Apifox 相关脚本\")",
66 - "Bash(cat:*)" 66 + "Bash(cat:*)",
67 + "Bash(pkill:*)"
67 ] 68 ]
68 }, 69 },
69 "enableAllProjectMcpServers": true, 70 "enableAllProjectMcpServers": true,
......
...@@ -5,6 +5,41 @@ ...@@ -5,6 +5,41 @@
5 5
6 --- 6 ---
7 7
8 +## [2026-02-06] - 修复保额输入组件输入报错及体验问题
9 +
10 +### 修复
11 +- 修复 `AmountInput` 组件在键盘输入时报错 `value.replace is not a function` 的问题
12 +- 修复快速输入时出现 `[object Object]` 的异常显示问题
13 +- 修复输入过程中格式化逻辑导致的光标跳动和无法输入小数点的问题
14 +
15 +### 技术实现
16 +- 增强 `onInput` 事件处理,防御性提取小程序/Web事件对象中的 `value` (`e.detail.value``e.target.value`)
17 +- 将输入值显式转换为字符串 `String(value)` 再进行处理
18 +- 引入内部状态 `inputValue` 分离显示值与模型值
19 +- 优化同步逻辑:仅当外部 `modelValue` 发生实质性变更时才更新显示值,保留用户输入过程中的中间状态(如 "1.")
20 +- 添加 `@blur` 事件,仅在失焦时进行严格的两位小数格式化
21 +
22 +## [2026-02-06] - 修复保额输入组件样式
23 +
24 +### 修复
25 +- 修复 `AmountInput` 组件标签和币种提示分两行显示的问题
26 +
27 +### 技术实现
28 +- 使用 Flexbox (`flex items-center`) 强制标签和币种提示在同一行显示
29 +- 移除冗余的空 `<span>` 标签
30 +
31 +## [2026-02-06] - 美化和统一计划书模版样式
32 +
33 +### 优化
34 +- 统一“人寿保险”和“重疾保险”模版的组件间距(`mb-5`),解决字段过于紧凑的问题
35 +- 统一错误处理逻辑,为重疾保险模版添加“模版配置未找到”的提示状态
36 +- 确保两个模版在视觉和交互上的一致性
37 +
38 +### 技术实现
39 +- 更新 `LifeInsuranceTemplate.vue``CriticalIllnessTemplate.vue`
40 +- 为所有表单组件添加 `class="mb-5"`
41 +-`CriticalIllnessTemplate.vue` 中添加 `v-if="config"` 判断及 `v-else` 错误提示
42 +
8 ## [2026-02-06] - 优化计划书表单字段顺序及联动逻辑 43 ## [2026-02-06] - 优化计划书表单字段顺序及联动逻辑
9 44
10 ### 优化 45 ### 优化
...@@ -800,7 +835,7 @@ ...@@ -800,7 +835,7 @@
800 --- 835 ---
801 836
802 **详细信息** 837 **详细信息**
803 -- **影响文件**: 838 +- **影响文件**:
804 - `src/pages/search/index.vue` 839 - `src/pages/search/index.vue`
805 - `src/pages/knowledge-base/index.vue` 840 - `src/pages/knowledge-base/index.vue`
806 - `src/pages/material-list/index.vue` 841 - `src/pages/material-list/index.vue`
......
1 <template> 1 <template>
2 <div> 2 <div>
3 <!-- 标签 --> 3 <!-- 标签 -->
4 - <div v-if="label" class="text-sm text-gray-600 mb-2"> 4 + <div v-if="label" class="text-sm text-gray-600 mb-2 flex items-center">
5 - {{ label }} 5 + <span>{{ label }}</span>
6 <span v-if="currencyText" class="text-gray-500">{{ currencyText }}</span> 6 <span v-if="currencyText" class="text-gray-500">{{ currencyText }}</span>
7 </div> 7 </div>
8 8
...@@ -29,8 +29,9 @@ ...@@ -29,8 +29,9 @@
29 <!-- 保额输入 --> 29 <!-- 保额输入 -->
30 <div class="border border-gray-200 rounded-lg flex items-center overflow-hidden"> 30 <div class="border border-gray-200 rounded-lg flex items-center overflow-hidden">
31 <nut-input 31 <nut-input
32 - :model-value="formattedValue" 32 + :model-value="inputValue"
33 @input="onInput" 33 @input="onInput"
34 + @blur="onBlur"
34 type="digit" 35 type="digit"
35 :placeholder="placeholder" 36 :placeholder="placeholder"
36 class="!p-0 !bg-transparent flex-1 !text-sm !text-gray-900" 37 class="!p-0 !bg-transparent flex-1 !text-sm !text-gray-900"
...@@ -68,7 +69,7 @@ ...@@ -68,7 +69,7 @@
68 * placeholder="请输入保额" 69 * placeholder="请输入保额"
69 * /> 70 * />
70 */ 71 */
71 -import { ref, computed } from 'vue' 72 +import { ref, computed, watch } from 'vue'
72 import { FEATURE_FLAGS, CURRENCY_SYMBOLS, CURRENCY_MAP } from '@/config/plan-templates' 73 import { FEATURE_FLAGS, CURRENCY_SYMBOLS, CURRENCY_MAP } from '@/config/plan-templates'
73 74
74 /** 75 /**
...@@ -201,34 +202,64 @@ const currencyText = computed(() => { ...@@ -201,34 +202,64 @@ const currencyText = computed(() => {
201 }) 202 })
202 203
203 /** 204 /**
204 - * 格式化显示值(元,带2位小数) 205 + * 内部显示值(元)
205 - * @description 将分转换为元进行显示 206 + * @type {Ref<string>}
206 - * @type {ComputedRef<string>}
207 - * @example
208 - * // modelValue = 100000 (分)
209 - * // formattedValue() // 返回: '1000.00'
210 */ 207 */
211 -const formattedValue = computed(() => { 208 +const inputValue = ref('')
212 - if (props.modelValue === null || props.modelValue === undefined) { 209 +
213 - return '' 210 +/**
214 - } 211 + * 监听 modelValue 变化,同步到内部 inputValue
215 - // 分 -> 元,保留2位小数 212 + * 仅当外部值与当前输入值解析结果不一致时才同步,避免输入过程中被格式化打断
216 - return (props.modelValue / 100).toFixed(2) 213 + */
217 -}) 214 +watch(
215 + () => props.modelValue,
216 + (newVal) => {
217 + // 解析当前 inputValue 为分
218 + const currentCents = Math.round(parseFloat(inputValue.value || '0') * 100)
219 +
220 + // 只有当值真正改变时才更新输入框(允许用户输入过程中保留 "1." 等中间状态)
221 + if (newVal !== currentCents) {
222 + if (newVal === null || newVal === undefined) {
223 + inputValue.value = ''
224 + } else {
225 + // 分 -> 元,保留2位小数
226 + inputValue.value = (newVal / 100).toFixed(2)
227 + }
228 + }
229 + },
230 + { immediate: true }
231 +)
218 232
219 /** 233 /**
220 * 用户输入处理 234 * 用户输入处理
221 * @description 将用户输入的元转换为分存储 235 * @description 将用户输入的元转换为分存储
222 - * @param {string} value - 输入值 236 + * @param {string|number|Object} val - 输入值
223 - *
224 - * @example
225 - * // 用户输入: '1000.50'
226 - * // onInput('1000.50')
227 - * // -> emit('update:modelValue', 100050) // 分
228 */ 237 */
229 -const onInput = (value) => { 238 +const onInput = (val) => {
230 - // 移除非数字和小数点 239 + let value = val
231 - const cleanValue = value.replace(/[^\d.]/g, '') 240 +
241 + // 防御性处理:如果接收到的是事件对象
242 + if (typeof val === 'object' && val !== null) {
243 + if (val.detail && typeof val.detail.value !== 'undefined') {
244 + // 小程序原生事件
245 + value = val.detail.value
246 + } else if (val.target && typeof val.target.value !== 'undefined') {
247 + // Web 原生事件
248 + value = val.target.value
249 + } else {
250 + // 无法提取值,直接返回,避免 [object Object]
251 + return
252 + }
253 + }
254 +
255 + // 确保 value 为字符串
256 + const valStr = String(value)
257 +
258 + // 更新内部显示值(允许用户输入任意合法字符,如小数点)
259 + inputValue.value = valStr
260 +
261 + // 移除非数字和小数点(安全处理)
262 + const cleanValue = valStr.replace(/[^\d.]/g, '')
232 263
233 // 转换为分(整数) 264 // 转换为分(整数)
234 const yuan = parseFloat(cleanValue) 265 const yuan = parseFloat(cleanValue)
...@@ -240,6 +271,16 @@ const onInput = (value) => { ...@@ -240,6 +271,16 @@ const onInput = (value) => {
240 } 271 }
241 272
242 /** 273 /**
274 + * 失去焦点时格式化
275 + */
276 +const onBlur = () => {
277 + if (props.modelValue !== null && props.modelValue !== undefined) {
278 + inputValue.value = (props.modelValue / 100).toFixed(2)
279 + }
280 +}
281 +
282 +
283 +/**
243 * 选择币种(多币种模式) 284 * 选择币种(多币种模式)
244 * @param {string} value - 币种代码 285 * @param {string} value - 币种代码
245 */ 286 */
......
1 <template> 1 <template>
2 - <div> 2 + <div v-if="config">
3 <!-- 性别 --> 3 <!-- 性别 -->
4 <PlanFieldRadio 4 <PlanFieldRadio
5 v-model="form.gender" 5 v-model="form.gender"
6 label="性别" 6 label="性别"
7 :options="['男', '女']" 7 :options="['男', '女']"
8 + class="mb-5"
8 /> 9 />
9 10
10 <!-- 出生年月日 --> 11 <!-- 出生年月日 -->
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
13 label="出生年月日" 14 label="出生年月日"
14 placeholder="请选择日期" 15 placeholder="请选择日期"
15 @change="onBirthdayChange" 16 @change="onBirthdayChange"
17 + class="mb-5"
16 /> 18 />
17 19
18 <!-- 年龄(根据出生日期自动计算,可编辑) --> 20 <!-- 年龄(根据出生日期自动计算,可编辑) -->
...@@ -20,6 +22,7 @@ ...@@ -20,6 +22,7 @@
20 v-model="form.age" 22 v-model="form.age"
21 label="年龄" 23 label="年龄"
22 placeholder="请选择出生日期自动计算" 24 placeholder="请选择出生日期自动计算"
25 + class="mb-5"
23 /> 26 />
24 27
25 <!-- 是否吸烟 --> 28 <!-- 是否吸烟 -->
...@@ -27,6 +30,7 @@ ...@@ -27,6 +30,7 @@
27 v-model="form.smoker" 30 v-model="form.smoker"
28 label="是否吸烟" 31 label="是否吸烟"
29 :options="['是', '否']" 32 :options="['是', '否']"
33 + class="mb-5"
30 /> 34 />
31 35
32 <!-- 保额 --> 36 <!-- 保额 -->
...@@ -35,6 +39,7 @@ ...@@ -35,6 +39,7 @@
35 label="保额" 39 label="保额"
36 placeholder="请输入保额" 40 placeholder="请输入保额"
37 :currency="config.currency" 41 :currency="config.currency"
42 + class="mb-5"
38 /> 43 />
39 44
40 <!-- 缴费年期 --> 45 <!-- 缴费年期 -->
...@@ -43,6 +48,7 @@ ...@@ -43,6 +48,7 @@
43 label="缴费年期" 48 label="缴费年期"
44 placeholder="请选择缴费年期" 49 placeholder="请选择缴费年期"
45 :options="config.payment_periods" 50 :options="config.payment_periods"
51 + class="mb-5"
46 /> 52 />
47 53
48 <!-- 保险期间 --> 54 <!-- 保险期间 -->
...@@ -53,6 +59,12 @@ ...@@ -53,6 +59,12 @@
53 </div> 59 </div>
54 </div> 60 </div>
55 </div> 61 </div>
62 +
63 + <!-- 配置缺失提示 -->
64 + <div v-else class="text-center text-gray-500 py-10">
65 + <p>⚠️ 模版配置未找到</p>
66 + <p class="text-sm mt-2">请检查产品配置或联系开发人员</p>
67 + </div>
56 </template> 68 </template>
57 69
58 <script setup> 70 <script setup>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
5 v-model="form.gender" 5 v-model="form.gender"
6 label="性别" 6 label="性别"
7 :options="['男', '女']" 7 :options="['男', '女']"
8 + class="mb-5"
8 /> 9 />
9 10
10 <!-- 出生年月日 --> 11 <!-- 出生年月日 -->
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
13 label="出生年月日" 14 label="出生年月日"
14 placeholder="请选择日期" 15 placeholder="请选择日期"
15 @change="onBirthdayChange" 16 @change="onBirthdayChange"
17 + class="mb-5"
16 /> 18 />
17 19
18 <!-- 年龄(根据出生日期自动计算,可编辑) --> 20 <!-- 年龄(根据出生日期自动计算,可编辑) -->
...@@ -20,6 +22,7 @@ ...@@ -20,6 +22,7 @@
20 v-model="form.age" 22 v-model="form.age"
21 label="年龄" 23 label="年龄"
22 placeholder="请选择出生日期自动计算" 24 placeholder="请选择出生日期自动计算"
25 + class="mb-5"
23 /> 26 />
24 27
25 <!-- 是否吸烟 --> 28 <!-- 是否吸烟 -->
...@@ -27,6 +30,7 @@ ...@@ -27,6 +30,7 @@
27 v-model="form.smoker" 30 v-model="form.smoker"
28 label="是否吸烟" 31 label="是否吸烟"
29 :options="['是', '否']" 32 :options="['是', '否']"
33 + class="mb-5"
30 /> 34 />
31 35
32 <!-- 保额 --> 36 <!-- 保额 -->
...@@ -35,6 +39,7 @@ ...@@ -35,6 +39,7 @@
35 label="保额" 39 label="保额"
36 placeholder="请输入保额" 40 placeholder="请输入保额"
37 :currency="config.currency" 41 :currency="config.currency"
42 + class="mb-5"
38 /> 43 />
39 44
40 <!-- 缴费年期 --> 45 <!-- 缴费年期 -->
...@@ -43,6 +48,7 @@ ...@@ -43,6 +48,7 @@
43 label="缴费年期" 48 label="缴费年期"
44 placeholder="请选择缴费年期" 49 placeholder="请选择缴费年期"
45 :options="config.payment_periods" 50 :options="config.payment_periods"
51 + class="mb-5"
46 /> 52 />
47 53
48 <!-- 保险期间 --> 54 <!-- 保险期间 -->
......