hookehuyr

refactor(plan): 完善计划书模板字段配置

- 更新 README.md 文档说明
- 更新 CHANGELOG.md 记录变更
- 优化 CriticalIllnessTemplate 字段配置
- 优化 LifeInsuranceTemplate 字段配置
- 优化 SavingsTemplate 字段配置

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...@@ -58,6 +58,7 @@ pnpm lint ...@@ -58,6 +58,7 @@ pnpm lint
58 -**Schema 驱动** - 储蓄类模板字段由配置驱动渲染与校验 58 -**Schema 驱动** - 储蓄类模板字段由配置驱动渲染与校验
59 -**提交映射下沉** - 提交字段映射从容器迁移到模板配置 59 -**提交映射下沉** - 提交字段映射从容器迁移到模板配置
60 -**人寿/重疾同步** - 人寿与重疾模板改为 Schema 驱动 60 -**人寿/重疾同步** - 人寿与重疾模板改为 Schema 驱动
61 +-**校验提示优化** - 必填提示与百分比校验统一更准确
61 62
62 ### 字段命名优化 63 ### 字段命名优化
63 -**提取方式字段** - 统一将 specified_amount_type 重命名为 withdrawal_method 64 -**提取方式字段** - 统一将 specified_amount_type 重命名为 withdrawal_method
......
1 +#### [2026-02-14] - 计划书必填校验与提示优化
2 +
3 +### 修复
4 +- 计划书模板必填规则统一判断,避免缺失字段未触发校验
5 +- 必填提示文案按输入/选择类型调整为更准确的提示语
6 +- 百分比字段空值不再误报校验错误
7 +
8 +### 测试
9 +- pnpm test(通过)
10 +- pnpm lint(存在历史警告)
11 +
12 +---
13 +
14 +**详细信息**
15 +- **影响文件**: src/components/plan/PlanTemplates/SavingsTemplate.vue, src/components/plan/PlanTemplates/LifeInsuranceTemplate.vue, src/components/plan/PlanTemplates/CriticalIllnessTemplate.vue, README.md
16 +- **技术栈**: Vue 3, Taro, Vitest
17 +- **测试状态**: 已通过(lint 有警告)
18 +- **备注**: 统一必填与提示逻辑,补齐空值校验边界
19 +
20 +---
21 +
1 #### [2026-02-14] - 提取字段分组修正 22 #### [2026-02-14] - 提取字段分组修正
2 23
3 ### 变更 24 ### 变更
......
...@@ -283,6 +283,27 @@ const onPercentageInput = (value, key) => { ...@@ -283,6 +283,27 @@ const onPercentageInput = (value, key) => {
283 form[key] = cleaned 283 form[key] = cleaned
284 } 284 }
285 285
286 +const isEmptyValue = (value) => {
287 + if (value === null || value === undefined) return true
288 + if (typeof value === 'string' && value.trim() === '') return true
289 + if (Array.isArray(value) && value.length === 0) return true
290 + return false
291 +}
292 +
293 +const getRequiredMessage = (field) => {
294 + if (field?.placeholder) return field.placeholder
295 + const label = field?.label || '必填信息'
296 + const selectTypes = ['radio', 'select', 'date', 'payment_period', 'age']
297 + if (selectTypes.includes(field?.type)) {
298 + return `请选择${label}`
299 + }
300 + return `请输入${label}`
301 +}
302 +
303 +const isFieldRequired = (field) => {
304 + return field?.required === true || field?.required === undefined
305 +}
306 +
286 /** 307 /**
287 * 表单校验(基于 Schema) 308 * 表单校验(基于 Schema)
288 * @returns {boolean} 校验是否通过 309 * @returns {boolean} 校验是否通过
...@@ -295,22 +316,25 @@ const validate = () => { ...@@ -295,22 +316,25 @@ const validate = () => {
295 continue 316 continue
296 } 317 }
297 318
298 - if (field.required) { 319 + if (isFieldRequired(field)) {
299 const value = form[field.key] 320 const value = form[field.key]
300 - if (value === undefined || value === null || value === '') { 321 + if (isEmptyValue(value)) {
301 - Taro.showToast({ title: field.label || '请完善必填信息', icon: 'none' }) 322 + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' })
302 return false 323 return false
303 } 324 }
304 } 325 }
305 326
306 if (field.type === 'percentage' && isFieldVisible(field.key)) { 327 if (field.type === 'percentage' && isFieldVisible(field.key)) {
307 - const percentage = parseFloat(form[field.key]) 328 + const value = form[field.key]
329 + if (!isEmptyValue(value)) {
330 + const percentage = parseFloat(value)
308 if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { 331 if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
309 Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) 332 Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
310 return false 333 return false
311 } 334 }
312 } 335 }
313 } 336 }
337 + }
314 338
315 return true 339 return true
316 } 340 }
......
...@@ -285,6 +285,27 @@ const onPercentageInput = (value, key) => { ...@@ -285,6 +285,27 @@ const onPercentageInput = (value, key) => {
285 form[key] = cleaned 285 form[key] = cleaned
286 } 286 }
287 287
288 +const isEmptyValue = (value) => {
289 + if (value === null || value === undefined) return true
290 + if (typeof value === 'string' && value.trim() === '') return true
291 + if (Array.isArray(value) && value.length === 0) return true
292 + return false
293 +}
294 +
295 +const getRequiredMessage = (field) => {
296 + if (field?.placeholder) return field.placeholder
297 + const label = field?.label || '必填信息'
298 + const selectTypes = ['radio', 'select', 'date', 'payment_period', 'age']
299 + if (selectTypes.includes(field?.type)) {
300 + return `请选择${label}`
301 + }
302 + return `请输入${label}`
303 +}
304 +
305 +const isFieldRequired = (field) => {
306 + return field?.required === true || field?.required === undefined
307 +}
308 +
288 /** 309 /**
289 * 表单校验(基于 Schema) 310 * 表单校验(基于 Schema)
290 * @returns {boolean} 校验是否通过 311 * @returns {boolean} 校验是否通过
...@@ -297,22 +318,25 @@ const validate = () => { ...@@ -297,22 +318,25 @@ const validate = () => {
297 continue 318 continue
298 } 319 }
299 320
300 - if (field.required) { 321 + if (isFieldRequired(field)) {
301 const value = form[field.key] 322 const value = form[field.key]
302 - if (value === undefined || value === null || value === '') { 323 + if (isEmptyValue(value)) {
303 - Taro.showToast({ title: field.label || '请完善必填信息', icon: 'none' }) 324 + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' })
304 return false 325 return false
305 } 326 }
306 } 327 }
307 328
308 if (field.type === 'percentage' && isFieldVisible(field.key)) { 329 if (field.type === 'percentage' && isFieldVisible(field.key)) {
309 - const percentage = parseFloat(form[field.key]) 330 + const value = form[field.key]
331 + if (!isEmptyValue(value)) {
332 + const percentage = parseFloat(value)
310 if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { 333 if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
311 Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) 334 Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
312 return false 335 return false
313 } 336 }
314 } 337 }
315 } 338 }
339 + }
316 340
317 return true 341 return true
318 } 342 }
......
...@@ -377,6 +377,27 @@ const onPercentageInput = (value, key) => { ...@@ -377,6 +377,27 @@ const onPercentageInput = (value, key) => {
377 form[key] = cleaned 377 form[key] = cleaned
378 } 378 }
379 379
380 +const isEmptyValue = (value) => {
381 + if (value === null || value === undefined) return true
382 + if (typeof value === 'string' && value.trim() === '') return true
383 + if (Array.isArray(value) && value.length === 0) return true
384 + return false
385 +}
386 +
387 +const getRequiredMessage = (field) => {
388 + if (field?.placeholder) return field.placeholder
389 + const label = field?.label || '必填信息'
390 + const selectTypes = ['radio', 'select', 'date', 'payment_period', 'age']
391 + if (selectTypes.includes(field?.type)) {
392 + return `请选择${label}`
393 + }
394 + return `请输入${label}`
395 +}
396 +
397 +const isFieldRequired = (field) => {
398 + return field?.required === true || field?.required === undefined
399 +}
400 +
380 /** 401 /**
381 * 表单校验 402 * 表单校验
382 * @returns {boolean} 是否通过校验 403 * @returns {boolean} 是否通过校验
...@@ -393,22 +414,25 @@ const validate = () => { ...@@ -393,22 +414,25 @@ const validate = () => {
393 continue 414 continue
394 } 415 }
395 416
396 - if (field.required) { 417 + if (isFieldRequired(field)) {
397 const value = form[field.key] 418 const value = form[field.key]
398 - if (value === undefined || value === null || value === '') { 419 + if (isEmptyValue(value)) {
399 - Taro.showToast({ title: field.label || '请完善必填信息', icon: 'none' }) 420 + Taro.showToast({ title: getRequiredMessage(field), icon: 'none' })
400 return false 421 return false
401 } 422 }
402 } 423 }
403 424
404 if (field.type === 'percentage' && isFieldVisible(field.key)) { 425 if (field.type === 'percentage' && isFieldVisible(field.key)) {
405 - const percentage = parseFloat(form[field.key]) 426 + const value = form[field.key]
427 + if (!isEmptyValue(value)) {
428 + const percentage = parseFloat(value)
406 if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) { 429 if (Number.isNaN(percentage) || percentage < 0 || percentage > 100) {
407 Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' }) 430 Taro.showToast({ title: '请输入0-100之间的百分比', icon: 'none' })
408 return false 431 return false
409 } 432 }
410 } 433 }
411 } 434 }
435 + }
412 436
413 return true 437 return true
414 } 438 }
......