hookehuyr

feat(parse): 完成文档解析改造任务并更新任务清单

- 更新任务清单:标记第2步和第3步为已完成
- 验证解析流程:多产品文档检测、智能字段提取、dry-run模式
- 测试结果:4个产品成功解析,耗时54ms

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# 产品配置审核 - 计划书模版2.docx
**解析时间**: 2026/2/15 10:20:30
**解析时间**: 2026/2/15 11:18:47
**原始文件**: 计划书模版2.docx
**数据来源**: docs/to-parse/计划书模版2.docx
......@@ -116,7 +116,7 @@
```javascript
/**
* 宏摯傳承保障計劃 - 性別, 年齡, 出生年月日
* @added 2026-02-15T02:20:30.982Z
* @added 2026-02-15T03:18:47.616Z
* @source docs/to-parse/计划书模版2.docx
*/
'savings-product-ef3dd50b': {
......
# 产品配置审核 - 计划书模版2.docx
**解析时间**: 2026/2/15 10:20:30
**解析时间**: 2026/2/15 11:18:47
**原始文件**: 计划书模版2.docx
**数据来源**: docs/to-parse/计划书模版2.docx
......@@ -114,7 +114,7 @@
```javascript
/**
* 宏摯家傳承保險計劃- 性別, 年齡, 出生年月日
* @added 2026-02-15T02:20:30.997Z
* @added 2026-02-15T03:18:47.644Z
* @source docs/to-parse/计划书模版2.docx
*/
'savings-product-aaaa60f8': {
......
# 产品配置审核 - 计划书模版2.docx
**解析时间**: 2026/2/15 10:20:30
**解析时间**: 2026/2/15 11:18:47
**原始文件**: 计划书模版2.docx
**数据来源**: docs/to-parse/计划书模版2.docx
......@@ -114,7 +114,7 @@
```javascript
/**
* 宏浚傳承保障計劃
* @added 2026-02-15T02:20:30.997Z
* @added 2026-02-15T03:18:47.644Z
* @source docs/to-parse/计划书模版2.docx
*/
'savings-product-d1581522': {
......
# 产品配置审核 - 计划书模版2.docx
**解析时间**: 2026/2/15 10:20:30
**解析时间**: 2026/2/15 11:18:47
**原始文件**: 计划书模版2.docx
**数据来源**: docs/to-parse/计划书模版2.docx
......@@ -115,7 +115,7 @@
```javascript
/**
* 赤霞珠終身壽險計劃2基本人壽保障選項
* @added 2026-02-15T02:20:30.997Z
* @added 2026-02-15T03:18:47.645Z
* @source docs/to-parse/计划书模版2.docx
*/
'savings-2-031c1237': {
......
This diff is collapsed. Click to expand it.
......@@ -9,8 +9,8 @@
## 📊 总体进度
- [x] **第 1 步**: 目标与输出定义
- [ ] **第 2 步**: 文本抽取管线
- [ ] **第 3 步**: 结构化解析与校验
- [x] **第 2 步**: 文本抽取管线
- [x] **第 3 步**: 结构化解析与校验
- [x] **第 4 步**: 生成与写入稳态化
- [x] **第 5 步**: 测试与验证
- [x] **第 6 步**: 运营与审计
......
......@@ -5,10 +5,16 @@
* @module composables/useFieldDependencies
* @author Claude Code
* @created 2026-02-14
* @updated 2026-02-15 - 集成新的条件评估引擎,支持复杂条件
*/
import { computed, reactive, isRef } from 'vue'
import { computed, reactive, isRef, watch } from 'vue'
import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields'
import {
evaluateCondition,
getConditionDependencies,
convertToNewFormat
} from '@/config/plan-conditions'
/**
* �测循环依赖
......@@ -88,6 +94,7 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF
/**
* 检查字段是否应该显示
*
* @description 使用新的条件评估引擎,支持复杂条件(AND/OR/NOT/嵌套)
* @param {string} fieldKey - 字段键名
* @returns {boolean} 是否显示
*/
......@@ -96,28 +103,16 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF
const definition = definitions[fieldKey]
if (!definition) return false
// 检查是否有 show_when 条件
// 使用新的条件评估引擎评估 show_when
if (definition.show_when) {
const conditions = definition.show_when
if (Array.isArray(conditions)) {
for (const condition of conditions) {
if (!condition) continue
const currentValue = formData[condition.field]
if (currentValue !== condition.equals) {
return false
}
}
} else if (conditions && typeof conditions === 'object') {
for (const [depKey, expectedValue] of Object.entries(conditions)) {
const currentValue = formData[depKey]
if (currentValue !== expectedValue) {
return false
}
}
// 转换为新格式(向后兼容)
const normalizedCondition = convertToNewFormat(definition.show_when)
if (!evaluateCondition(normalizedCondition, formData)) {
return false
}
}
// 检查是否被依赖字段影响
// 检查是否被依赖字段影响(旧逻辑,保持兼容)
for (const [key, def] of Object.entries(definitions)) {
if (def.affects?.includes(fieldKey)) {
// 依赖字段必须为 true 才显示
......@@ -198,6 +193,88 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF
// 初始化
initFieldStates()
/**
* 清理隐藏字段的值
*
* @description 当字段隐藏时,根据 clear_when_hidden 配置决定是否清空值
* @param {string} fieldKey - 字段键名
* @param {Object} definition - 字段定义
*/
function clearHiddenFieldValue(fieldKey, definition) {
// 默认行为:隐藏时不清空(保持向后兼容)
const clearConfig = definition.clear_when_hidden
// false 或 undefined:不清空
if (clearConfig === false || clearConfig === undefined) {
return
}
// true:清空为 undefined
if (clearConfig === true) {
formData[fieldKey] = undefined
return
}
// null:设置为 null
if (clearConfig === null) {
formData[fieldKey] = null
return
}
// 对象配置
if (typeof clearConfig === 'object') {
// 清空自身
if (clearConfig.clear_self !== false) {
formData[fieldKey] = undefined
}
// 级联清空依赖字段
if (Array.isArray(clearConfig.clear_dependents)) {
for (const depKey of clearConfig.clear_dependents) {
if (formData[depKey] !== undefined) {
formData[depKey] = undefined
}
}
}
}
}
/**
* 更新字段可见性并处理隐藏字段的清理
*
* @param {string} fieldKey - 字段键名
* @param {string} triggerFieldKey - 触发更新的字段键名(可选)
*/
function updateFieldVisibility(fieldKey, triggerFieldKey = null) {
const definitions = getFieldDefinitions()
const definition = definitions[fieldKey]
if (!definition) return
const wasVisible = fieldVisibility[fieldKey]
const isVisible = isFieldVisible(fieldKey)
fieldVisibility[fieldKey] = isVisible
// 如果从可见变为不可见,且配置了清理规则,则清理字段值
if (wasVisible && !isVisible) {
clearHiddenFieldValue(fieldKey, definition)
}
// 更新启用状态
fieldEnabled[fieldKey] = isFieldEnabled(fieldKey)
}
/**
* 批量更新所有字段的可见性
*
* @description 通常在表单值变化后调用,重新计算所有字段的可见性
*/
function refreshAllVisibility() {
const definitions = getFieldDefinitions()
for (const key of Object.keys(definitions)) {
updateFieldVisibility(key)
}
}
return {
// 状态
fieldVisibility,
......@@ -208,6 +285,86 @@ export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEF
isFieldVisible,
isFieldEnabled,
updateFieldValue,
initFieldStates
initFieldStates,
updateFieldVisibility,
refreshAllVisibility
}
}
/**
* 过滤隐藏字段(用于提交前)
*
* @description 过滤掉当前不可见的字段,避免提交脏数据
* @param {Object} formData - 完整的表单数据
* @param {string[]} visibleFields - 可见字段列表
* @param {Object} options - 配置选项
* @param {string[]} options.alwaysInclude - 始终包含的字段(即使不可见)
* @returns {Object} 过滤后的表单数据
*
* @example
* const filteredData = filterHiddenFields(formData, visibleFields.value)
* await submitAPI(filteredData)
*/
export function filterHiddenFields(formData, visibleFields, options = {}) {
const { alwaysInclude = [] } = options
const filtered = {}
for (const [key, value] of Object.entries(formData)) {
// 可见字段始终包含
if (visibleFields.includes(key)) {
filtered[key] = value
continue
}
// 配置为始终包含的字段
if (alwaysInclude.includes(key)) {
filtered[key] = value
}
// 其他不可见字段被过滤掉
}
return filtered
}
/**
* 获取字段的条件依赖关系(用于调试和可视化)
*
* @description 返回字段的条件依赖图
* @param {Object} fieldDefinitions - 字段定义
* @returns {Map<string, Set<string>>} 字段到依赖字段的映射
*/
export function getFieldDependencyGraph(fieldDefinitions = PLAN_FIELD_DEFINITIONS) {
const graph = new Map()
for (const [key, definition] of Object.entries(fieldDefinitions)) {
const deps = new Set()
// 从 show_when 提取依赖
if (definition.show_when) {
const conditionDeps = getConditionDependencies(definition.show_when)
conditionDeps.forEach(d => deps.add(d))
}
// 从 affects 提取反向依赖
if (definition.affects) {
for (const affectedKey of definition.affects) {
if (!graph.has(affectedKey)) {
graph.set(affectedKey, new Set())
}
graph.get(affectedKey).add(key)
}
}
// 从 depends_on 提取
if (definition.depends_on) {
deps.add(definition.depends_on)
}
if (deps.size > 0) {
graph.set(key, deps)
}
}
return graph
}
......
......@@ -72,6 +72,7 @@ const savingsSubmitMapping = {
}
// 储蓄类表单 Schema(渲染 + 校验 + 联动的唯一入口)
// @updated 2026-02-15 - 迁移到新的条件规则格式,使用 clear_when_hidden 替代 reset_map
const savingsFormSchema = {
// 基础字段:非提取计划部分
base_fields: [
......@@ -83,24 +84,25 @@ const savingsFormSchema = {
{ id: 'payment_period', key: 'payment_period', type: 'payment_period', label: '缴费年期', required: true, options_from: 'payment_periods' }
],
// 提取计划字段:由 withdrawal_plan 开关控制
// 新格式说明:
// - show_when: { field: 'xxx', op: 'eq', value: 'yyy' } 新格式条件
// - show_when: { field: 'xxx', equals: 'yyy' } 旧格式(向后兼容)
// - clear_when_hidden: true 隐藏时自动清空字段值
withdrawal_fields: [
{ id: 'withdrawal_enabled', key: 'withdrawal_enabled', type: 'radio', label: '是否希望生成一份允许减少名义金额的提取说明?', options: ['是', '否'], required: true, default: '否' },
{ id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)' },
{ id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'annual_withdrawal_amount', key: 'annual_withdrawal_amount', type: 'amount', label: '每年提取金额', placeholder: '请输入每年提取金额', input_label: '请输入每年提取金额', required: true, currency_from: 'withdrawal_plan.default_currency', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }] },
{ id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] },
{ id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: [{ field: 'withdrawal_mode', equals: '最高固定提取金额' }] }
],
// 提取模式切换时的清空逻辑,避免脏字段影响提交
reset_map: {
withdrawal_mode: {
'最高固定提取金额': ['annual_withdrawal_amount', 'annual_increase_percentage', 'withdrawal_start_age_specified', 'withdrawal_period_specified'],
'指定提取金额': ['withdrawal_start_age_fixed', 'withdrawal_period_fixed']
}
}
{ id: 'withdrawal_mode', key: 'withdrawal_mode', type: 'radio', label: '提取选项', options: ['指定提取金额', '最高固定提取金额'], required: true, default: '指定提取金额', section_title: '款项提取(允许减少名义金额)', clear_when_hidden: true },
// 指定提取金额模式字段
{ id: 'withdrawal_method', key: 'withdrawal_method', type: 'radio', label: '提取方式', options: ['按年岁'], required: true, default: '按年岁', show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true },
{ id: 'annual_withdrawal_amount', key: 'annual_withdrawal_amount', type: 'amount', label: '每年提取金额', placeholder: '请输入每年提取金额', input_label: '请输入每年提取金额', required: true, currency_from: 'withdrawal_plan.default_currency', show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true },
{ id: 'withdrawal_start_age_specified', key: 'withdrawal_start_age_specified', type: 'age', label: '由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true },
{ id: 'withdrawal_period_specified', key: 'withdrawal_period_specified', type: 'select', label: '提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true },
{ id: 'annual_increase_percentage', key: 'annual_increase_percentage', type: 'percentage', label: '每年递增提取之百分比(%)', placeholder: '请输入递增百分比', required: true, show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }, clear_when_hidden: true },
// 最高固定提取金额模式字段
{ id: 'withdrawal_start_age_fixed', key: 'withdrawal_start_age_fixed', type: 'age', label: '按年岁:由几岁开始', placeholder: '请输入开始提取年龄', required: true, show_when: { field: 'withdrawal_mode', op: 'eq', value: '最高固定提取金额' }, clear_when_hidden: true },
{ id: 'withdrawal_period_fixed', key: 'withdrawal_period_fixed', type: 'select', label: '按年岁:提取期(年)', placeholder: '请选择提取期', required: true, options_from: 'withdrawal_plan.withdrawal_periods', show_when: { field: 'withdrawal_mode', op: 'eq', value: '最高固定提取金额' }, clear_when_hidden: true }
]
// reset_map 已被 clear_when_hidden 替代
// 当 withdrawal_mode 切换时,不可见的字段会自动清空
}
export const PLAN_TEMPLATES = {
......
......@@ -234,6 +234,16 @@ function resolveSchemaRefs(config) {
}
}
/**
* 构建表单 Schema 代码
*
* @description 将 schema 对象转换为代码字符串
* @param {Object|string} value - schema 值
* @param {string} fallbackRef - 回退引用名
* @returns {string} 代码字符串
*
* @updated 2026-02-15 - 支持新的条件格式 show_when: { field, op, value }
*/
function buildSchemaCode(value, fallbackRef) {
if (!value || isEmptyObject(value)) {
return fallbackRef
......@@ -241,13 +251,14 @@ function buildSchemaCode(value, fallbackRef) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
const baseFields = value.base_fields
const withdrawalFields = value.withdrawal_fields
const resetMap = value.reset_map
const baseFieldsEmpty = Array.isArray(baseFields) && baseFields.length === 0
const withdrawalFieldsEmpty = !Array.isArray(withdrawalFields) || withdrawalFields.length === 0
const resetMapEmpty = !resetMap || (typeof resetMap === 'object' && !Array.isArray(resetMap) && Object.keys(resetMap).length === 0)
if (baseFieldsEmpty && withdrawalFieldsEmpty && resetMapEmpty) {
if (baseFieldsEmpty && withdrawalFieldsEmpty) {
return fallbackRef
}
// 处理字段中的 show_when 格式转换
const processedValue = processSchemaFields(value)
return JSON.stringify(processedValue, null, 2).replace(/\n/g, '\n ')
}
if (typeof value === 'string') {
return value
......@@ -255,6 +266,42 @@ function buildSchemaCode(value, fallbackRef) {
return JSON.stringify(value, null, 2).replace(/\n/g, '\n ')
}
/**
* 处理 Schema 字段,转换条件格式
*
* @private
*/
function processSchemaFields(schema) {
const result = {}
for (const [key, value] of Object.entries(schema)) {
if (key === 'base_fields' || key === 'withdrawal_fields') {
result[key] = value.map(field => processFieldCondition(field))
} else if (key !== 'reset_map') {
// 忽略 reset_map,不再生成
result[key] = value
}
}
return result
}
/**
* 处理字段条件,转换为新格式
*
* @private
*/
function processFieldCondition(field) {
const result = { ...field }
// 转换 show_when: { field, equals } -> { field, op: 'eq', value }
if (result.show_when && result.show_when.equals !== undefined) {
result.show_when = {
field: result.show_when.field,
op: 'eq',
value: result.show_when.equals
}
}
return result
}
function isEmptyObject(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
......