hookehuyr

feat(plan): 添加字段条件显示系统扩展功能

- 新增条件规则引擎 (plan-conditions.js)
- 支持 eq/neq/gt/gte/lt/lte/in/not_in 等操作符
- 支持 AND/OR 逻辑组合
- 添加完整的单元测试 (plan-conditions.test.js)
- 添加功能扩展计划文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# 字段条件显示系统扩展计划
## 背景与目标
### 当前问题
- `show_when` 只支持简单的等于比较 (`equals`)
- 不支持 OR 条件、嵌套条件
- 不支持不等于、大于、小于等操作符
- 隐藏字段的清理逻辑分散在 `reset_map`
- 提交时可能包含隐藏字段的脏数据
### 目标
构建一个声明式的条件规则系统,支持复杂逻辑同时保持配置可读性。
## 技术方案:方案 B - 条件规则引擎
### 核心文件变更
| 文件 | 操作 | 说明 |
|------|------|------|
| `src/config/plan-conditions.js` | 新建 | 条件操作符和评估引擎 |
| `src/composables/useFieldDependencies.js` | 修改 | 集成新的条件评估器 |
| `src/composables/usePlanSubmit.js` | 新建 | 提交时字段过滤逻辑 |
| `src/config/plan-templates.js` | 修改 | 迁移现有配置到新格式 |
| `src/composables/__tests__/plan-conditions.test.js` | 新建 | 条件引擎单元测试 |
---
## 阶段 1:核心条件引擎
### 任务 1.1 创建条件操作符定义
- [x] 创建 `src/config/plan-conditions.js`
- [x] 定义比较操作符:`eq`, `ne`, `gt`, `gte`, `lt`, `lte`
- [x] 定义集合操作符:`in`, `nin`
- [x] 定义字符串操作符:`contains`, `startsWith`, `matches`
- [x] 定义布尔操作符:`truthy`, `falsy`, `empty`, `notEmpty`
- [x] 编写单元测试验证操作符正确性
### 任务 1.2 实现条件评估函数
- [x] 实现 `evaluateCondition(condition, formData)` 函数
- [x] 支持简单条件:`{ field, op, value }`
- [x] 支持 AND 逻辑:`{ and: [...] }`
- [x] 支持 OR 逻辑:`{ or: [...] }`
- [x] 支持 NOT 逻辑:`{ not: {...} }`
- [x] 支持嵌套条件组合
- [x] 编写单元测试覆盖各种条件场景
### 任务 1.3 向后兼容处理
- [x] 支持旧格式 `show_when: { field: 'x', equals: 'y' }`
- [x] 支持旧格式数组 `show_when: [{ field: 'x', equals: 'y' }]`
- [x] 自动转换为新格式
---
## 阶段 2:清理机制
### 任务 2.1 字段清理规则
- [x] 在字段定义中添加 `clear_when_hidden` 属性
- [x] 实现 `clear_when_hidden: true` 自动清空
- [x] 实现 `clear_when_hidden: false` 保留值
- [x] 实现级联清理 `clear_when_hidden: { clear_dependents: [...] }`
### 任务 2.2 提交时字段过滤
- [x] 实现 `filterHiddenFields(formData, visibleFields)` 函数
- [x] 只提交当前可见的字段
- [x] 保持 API 兼容性
### 任务 2.3 更新 useFieldDependencies
- [x] 集成新的条件评估引擎
- [x] 实现字段隐藏时的自动清理
- [x] 更新 `isFieldVisible` 使用新引擎
- [x] 保持 API 兼容性
---
## 阶段 3:配置迁移
### 任务 3.1 迁移储蓄类模板
- [x] 迁移 `savingsFormSchema.withdrawal_fields` 到新格式
- [x] 删除 `reset_map`,使用 `clear_when_hidden` 替代
- [x] 验证功能正常(测试通过 160/160)
### 任务 3.2 迁移保障类模板
- [x] 检查 `protectionFormSchema` 是否需要条件
- [x] 按需添加条件规则(保障类暂无条件需求,无需迁移)
### 任务 3.3 更新文档解析工具 ⚠️ 重要
- [x] 更新 `src/utils/parsers/config-generator.js` 生成新格式配置
- [x] 修改 `show_when` 生成逻辑,使用 `{ field, op, value }` 格式
- [x] 停止生成 `reset_map`(已忽略)
- [x] 测试通过(160/160)
**说明**:MCP 文档解析服务保持不变,仅更新配置生成器输出格式。
### 任务 3.4 更新文档
- [x] 更新 `plan-templates.js` 顶部的使用说明
- [x] 添加条件规则配置示例(已在 plan-templates.js 注释中说明)
- [x] 添加常见场景示例(已在 savingsFormSchema 中实现)
---
## 阶段 4:测试与验证
### 任务 4.1 单元测试
- [ ] 条件操作符测试覆盖率 > 90%
- [ ] 条件评估引擎测试覆盖率 > 90%
- [ ] useFieldDependencies 测试更新
### 任务 4.2 集成测试
- [ ] 测试储蓄类产品完整流程
- [ ] 测试字段显示/隐藏切换
- [ ] 测试提交时字段过滤
- [ ] 测试向后兼容性
### 任务 4.3 真机验证
- [ ] 微信开发者工具验证
- [ ] 检查性能影响
- [ ] 检查内存占用
---
## 配置格式示例
### 新格式示例
```javascript
// 简单条件
show_when: { field: 'smoker', op: 'eq', value: '是' }
// 多条件 AND
show_when: {
and: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gte', value: 30 }
]
}
// OR 条件
show_when: {
or: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gt', value: 50 }
]
}
// 嵌套条件
show_when: {
and: [
{ field: 'product_type', op: 'in', value: ['A', 'B'] },
{
or: [
{ field: 'coverage', op: 'gte', value: 100000 },
{ field: 'payment_period', op: 'eq', value: '20年' }
]
}
]
}
// 清理规则
{
id: 'withdrawal_amount',
show_when: { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' },
clear_when_hidden: true // 隐藏时清空
}
```
### 旧格式(保持兼容)
```javascript
// 旧格式仍然支持
show_when: { field: 'withdrawal_mode', equals: '指定提取金额' }
show_when: [{ field: 'withdrawal_mode', equals: '指定提取金额' }]
```
---
## 风险与注意事项
1. **向后兼容**:必须保持现有配置格式可用
2. **性能**:条件评估不能影响表单响应速度
3. **可读性**:配置文件需要保持可理解性
4. **测试覆盖**:每个操作符都需要充分测试
---
## 进度追踪
| 阶段 | 状态 | 完成时间 |
|------|------|---------|
| 阶段 1:核心引擎 | ✅ 完成 | 2026-02-15 |
| 阶段 2:清理机制 | ✅ 完成 | 2026-02-15 |
| 阶段 3:配置迁移 | ✅ 完成 | 2026-02-15 |
| 阶段 4:测试验证 | 🔄 待真机验证 | - |
---
**创建时间**: 2026-02-15
**预计工期**: 3-4 天
**维护者**: Claude Code
/**
* 计划书字段条件引擎测试
*
* @description 测试条件操作符和评估逻辑
* @module config/__tests__/plan-conditions.test
*/
import { describe, it, expect } from 'vitest'
import {
CONDITION_OPERATORS,
normalizeOperator,
evaluateSingleCondition,
evaluateAnd,
evaluateOr,
evaluateNot,
evaluateCondition,
getConditionDependencies,
convertToNewFormat
} from '../plan-conditions'
describe('CONDITION_OPERATORS', () => {
describe('比较操作符', () => {
it('eq - 等于', () => {
expect(CONDITION_OPERATORS.eq('是', '是')).toBe(true)
expect(CONDITION_OPERATORS.eq('是', '否')).toBe(false)
expect(CONDITION_OPERATORS.eq(1, 1)).toBe(true)
expect(CONDITION_OPERATORS.eq(1, '1')).toBe(false) // 严格相等
})
it('ne - 不等于', () => {
expect(CONDITION_OPERATORS.ne('是', '否')).toBe(true)
expect(CONDITION_OPERATORS.ne('是', '是')).toBe(false)
})
it('gt - 大于', () => {
expect(CONDITION_OPERATORS.gt(30, 18)).toBe(true)
expect(CONDITION_OPERATORS.gt(18, 18)).toBe(false)
expect(CONDITION_OPERATORS.gt(10, 18)).toBe(false)
})
it('gte - 大于等于', () => {
expect(CONDITION_OPERATORS.gte(30, 18)).toBe(true)
expect(CONDITION_OPERATORS.gte(18, 18)).toBe(true)
expect(CONDITION_OPERATORS.gte(10, 18)).toBe(false)
})
it('lt - 小于', () => {
expect(CONDITION_OPERATORS.lt(10, 18)).toBe(true)
expect(CONDITION_OPERATORS.lt(18, 18)).toBe(false)
expect(CONDITION_OPERATORS.lt(30, 18)).toBe(false)
})
it('lte - 小于等于', () => {
expect(CONDITION_OPERATORS.lte(10, 18)).toBe(true)
expect(CONDITION_OPERATORS.lte(18, 18)).toBe(true)
expect(CONDITION_OPERATORS.lte(30, 18)).toBe(false)
})
})
describe('集合操作符', () => {
it('in - 包含于', () => {
expect(CONDITION_OPERATORS.in('A', ['A', 'B', 'C'])).toBe(true)
expect(CONDITION_OPERATORS.in('D', ['A', 'B', 'C'])).toBe(false)
expect(CONDITION_OPERATORS.in('A', 'not-array')).toBe(false)
})
it('nin - 不包含于', () => {
expect(CONDITION_OPERATORS.nin('D', ['A', 'B', 'C'])).toBe(true)
expect(CONDITION_OPERATORS.nin('A', ['A', 'B', 'C'])).toBe(false)
})
})
describe('字符串操作符', () => {
it('contains - 包含子串', () => {
expect(CONDITION_OPERATORS.contains('Hello World', 'World')).toBe(true)
expect(CONDITION_OPERATORS.contains('Hello World', 'world')).toBe(false) // 大小写敏感
expect(CONDITION_OPERATORS.contains(null, 'test')).toBe(false)
})
it('startsWith - 前缀匹配', () => {
expect(CONDITION_OPERATORS.startsWith('Hello', 'Hel')).toBe(true)
expect(CONDITION_OPERATORS.startsWith('Hello', 'hel')).toBe(false)
})
it('endsWith - 后缀匹配', () => {
expect(CONDITION_OPERATORS.endsWith('Hello', 'llo')).toBe(true)
expect(CONDITION_OPERATORS.endsWith('Hello', 'LLO')).toBe(false)
})
it('matches - 正则匹配', () => {
expect(CONDITION_OPERATORS.matches('ABC123', '^[A-Z]+\\d+$')).toBe(true)
expect(CONDITION_OPERATORS.matches('abc', '^[A-Z]+$')).toBe(false)
expect(CONDITION_OPERATORS.matches('test', '[invalid')).toBe(false) // 无效正则
})
})
describe('布尔/空值操作符', () => {
it('truthy - 真值', () => {
expect(CONDITION_OPERATORS.truthy(true)).toBe(true)
expect(CONDITION_OPERATORS.truthy(1)).toBe(true)
expect(CONDITION_OPERATORS.truthy('text')).toBe(true)
expect(CONDITION_OPERATORS.truthy(false)).toBe(false)
expect(CONDITION_OPERATORS.truthy(0)).toBe(false)
expect(CONDITION_OPERATORS.truthy('')).toBe(false)
expect(CONDITION_OPERATORS.truthy(null)).toBe(false)
})
it('falsy - 假值', () => {
expect(CONDITION_OPERATORS.falsy(false)).toBe(true)
expect(CONDITION_OPERATORS.falsy(0)).toBe(true)
expect(CONDITION_OPERATORS.falsy('')).toBe(true)
expect(CONDITION_OPERATORS.falsy(null)).toBe(true)
expect(CONDITION_OPERATORS.falsy(true)).toBe(false)
})
it('empty - 空值', () => {
expect(CONDITION_OPERATORS.empty('')).toBe(true)
expect(CONDITION_OPERATORS.empty(null)).toBe(true)
expect(CONDITION_OPERATORS.empty(undefined)).toBe(true)
expect(CONDITION_OPERATORS.empty(0)).toBe(false)
expect(CONDITION_OPERATORS.empty('text')).toBe(false)
})
it('notEmpty - 非空', () => {
expect(CONDITION_OPERATORS.notEmpty('text')).toBe(true)
expect(CONDITION_OPERATORS.notEmpty(0)).toBe(true)
expect(CONDITION_OPERATORS.notEmpty('')).toBe(false)
expect(CONDITION_OPERATORS.notEmpty(null)).toBe(false)
})
it('emptyArray - 空数组', () => {
expect(CONDITION_OPERATORS.emptyArray([])).toBe(true)
expect(CONDITION_OPERATORS.emptyArray(null)).toBe(true)
expect(CONDITION_OPERATORS.emptyArray([1])).toBe(false)
})
it('notEmptyArray - 非空数组', () => {
expect(CONDITION_OPERATORS.notEmptyArray([1])).toBe(true)
expect(CONDITION_OPERATORS.notEmptyArray([])).toBe(false)
})
})
})
describe('normalizeOperator', () => {
it('返回标准操作符', () => {
expect(normalizeOperator('eq')).toBe('eq')
expect(normalizeOperator('gt')).toBe('gt')
})
it('处理别名', () => {
expect(normalizeOperator('equals')).toBe('eq')
expect(normalizeOperator('==')).toBe('eq')
expect(normalizeOperator('===')).toBe('eq')
expect(normalizeOperator('!=')).toBe('ne')
expect(normalizeOperator('>')).toBe('gt')
expect(normalizeOperator('>=')).toBe('gte')
})
it('未知操作符返回 null', () => {
expect(normalizeOperator('unknown')).toBe(null)
})
})
describe('evaluateSingleCondition', () => {
const formData = { smoker: '是', age: 30, name: 'John' }
it('评估等于条件', () => {
expect(evaluateSingleCondition(
{ field: 'smoker', op: 'eq', value: '是' },
formData
)).toBe(true)
expect(evaluateSingleCondition(
{ field: 'smoker', op: 'eq', value: '否' },
formData
)).toBe(false)
})
it('评估大于条件', () => {
expect(evaluateSingleCondition(
{ field: 'age', op: 'gt', value: 18 },
formData
)).toBe(true)
})
it('评估包含条件', () => {
expect(evaluateSingleCondition(
{ field: 'name', op: 'contains', value: 'John' },
formData
)).toBe(true)
})
it('缺少字段或操作符返回 false', () => {
expect(evaluateSingleCondition({ op: 'eq', value: 'x' }, formData)).toBe(false)
expect(evaluateSingleCondition({ field: 'x', value: 'y' }, formData)).toBe(false)
})
it('未知操作符返回 false', () => {
expect(evaluateSingleCondition(
{ field: 'smoker', op: 'unknown', value: '是' },
formData
)).toBe(false)
})
})
describe('evaluateAnd', () => {
const formData = { smoker: '是', age: 30 }
it('所有条件满足返回 true', () => {
expect(evaluateAnd([
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gte', value: 18 }
], formData)).toBe(true)
})
it('任一条件不满足返回 false', () => {
expect(evaluateAnd([
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gte', value: 50 }
], formData)).toBe(false)
})
it('空数组返回 true', () => {
expect(evaluateAnd([], formData)).toBe(true)
})
})
describe('evaluateOr', () => {
const formData = { smoker: '是', age: 30 }
it('任一条件满足返回 true', () => {
expect(evaluateOr([
{ field: 'smoker', op: 'eq', value: '否' },
{ field: 'age', op: 'gte', value: 18 }
], formData)).toBe(true)
})
it('所有条件不满足返回 false', () => {
expect(evaluateOr([
{ field: 'smoker', op: 'eq', value: '否' },
{ field: 'age', op: 'gte', value: 50 }
], formData)).toBe(false)
})
it('空数组返回 false', () => {
expect(evaluateOr([], formData)).toBe(false)
})
})
describe('evaluateNot', () => {
const formData = { smoker: '是' }
it('条件不满足返回 true', () => {
expect(evaluateNot(
{ field: 'smoker', op: 'eq', value: '否' },
formData
)).toBe(true)
})
it('条件满足返回 false', () => {
expect(evaluateNot(
{ field: 'smoker', op: 'eq', value: '是' },
formData
)).toBe(false)
})
})
describe('evaluateCondition', () => {
const formData = {
smoker: '是',
age: 30,
product_type: 'A',
coverage: 100000,
payment_period: '20年'
}
it('空条件返回 true', () => {
expect(evaluateCondition(null, formData)).toBe(true)
expect(evaluateCondition(undefined, formData)).toBe(true)
})
it('简单条件', () => {
expect(evaluateCondition(
{ field: 'smoker', op: 'eq', value: '是' },
formData
)).toBe(true)
})
it('AND 逻辑', () => {
expect(evaluateCondition({
and: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gte', value: 30 }
]
}, formData)).toBe(true)
expect(evaluateCondition({
and: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gt', value: 50 }
]
}, formData)).toBe(false)
})
it('OR 逻辑', () => {
expect(evaluateCondition({
or: [
{ field: 'smoker', op: 'eq', value: '否' },
{ field: 'age', op: 'gte', value: 30 }
]
}, formData)).toBe(true)
expect(evaluateCondition({
or: [
{ field: 'smoker', op: 'eq', value: '否' },
{ field: 'age', op: 'gt', value: 50 }
]
}, formData)).toBe(false)
})
it('NOT 逻辑', () => {
expect(evaluateCondition({
not: { field: 'smoker', op: 'eq', value: '否' }
}, formData)).toBe(true)
})
it('嵌套条件', () => {
// (smoker == '是' OR age > 50) AND product_type in ['A', 'B']
expect(evaluateCondition({
and: [
{
or: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gt', value: 50 }
]
},
{ field: 'product_type', op: 'in', value: ['A', 'B'] }
]
}, formData)).toBe(true)
})
it('数组默认为 AND', () => {
expect(evaluateCondition([
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gte', value: 30 }
], formData)).toBe(true)
})
it('旧格式兼容 - equals', () => {
expect(evaluateCondition(
{ field: 'smoker', equals: '是' },
formData
)).toBe(true)
})
it('旧格式兼容 - 扁平对象', () => {
expect(evaluateCondition(
{ smoker: '是', age: 30 },
formData
)).toBe(true)
})
})
describe('getConditionDependencies', () => {
it('提取简单条件的依赖', () => {
const deps = getConditionDependencies({ field: 'smoker', op: 'eq', value: '是' })
expect(deps.has('smoker')).toBe(true)
expect(deps.size).toBe(1)
})
it('提取 AND 条件的依赖', () => {
const deps = getConditionDependencies({
and: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'gte', value: 30 }
]
})
expect(deps.has('smoker')).toBe(true)
expect(deps.has('age')).toBe(true)
expect(deps.size).toBe(2)
})
it('提取嵌套条件的依赖', () => {
const deps = getConditionDependencies({
and: [
{ field: 'a', op: 'eq', value: 1 },
{
or: [
{ field: 'b', op: 'eq', value: 2 },
{ field: 'c', op: 'eq', value: 3 }
]
}
]
})
expect(deps.has('a')).toBe(true)
expect(deps.has('b')).toBe(true)
expect(deps.has('c')).toBe(true)
expect(deps.size).toBe(3)
})
it('提取扁平对象的依赖', () => {
const deps = getConditionDependencies({ smoker: '是', age: 30 })
expect(deps.has('smoker')).toBe(true)
expect(deps.has('age')).toBe(true)
})
})
describe('convertToNewFormat', () => {
it('已经是新格式则直接返回', () => {
const condition = { field: 'smoker', op: 'eq', value: '是' }
expect(convertToNewFormat(condition)).toEqual(condition)
})
it('转换旧格式 equals', () => {
expect(convertToNewFormat(
{ field: 'smoker', equals: '是' }
)).toEqual({ field: 'smoker', op: 'eq', value: '是' })
})
it('转换旧格式数组为 AND', () => {
expect(convertToNewFormat([
{ field: 'a', equals: 'x' },
{ field: 'b', equals: 'y' }
])).toEqual({
and: [
{ field: 'a', op: 'eq', value: 'x' },
{ field: 'b', op: 'eq', value: 'y' }
]
})
})
it('单个条件的数组不包装 AND', () => {
expect(convertToNewFormat([
{ field: 'a', equals: 'x' }
])).toEqual({ field: 'a', op: 'eq', value: 'x' })
})
it('转换扁平对象', () => {
expect(convertToNewFormat({ smoker: '是' })).toEqual({
field: 'smoker',
op: 'eq',
value: '是'
})
})
it('转换多字段扁平对象为 AND', () => {
expect(convertToNewFormat({ smoker: '是', age: 30 })).toEqual({
and: [
{ field: 'smoker', op: 'eq', value: '是' },
{ field: 'age', op: 'eq', value: 30 }
]
})
})
})
/**
* 计划书字段条件规则引擎
*
* @description 定义条件操作符和评估逻辑,支持复杂的字段显示/隐藏条件
* @module config/plan-conditions
* @author Claude Code
* @created 2026-02-15
* @version 1.0.0
*/
/**
* 条件操作符定义
*
* @description 所有可用的条件比较操作符
* @type {Object<string, Function>}
*/
export const CONDITION_OPERATORS = {
// ========== 比较操作 ==========
/**
* 等于(严格相等)
* @param {*} actual - 实际值
* @param {*} expected - 期望值
* @returns {boolean}
*/
eq: (actual, expected) => actual === expected,
/**
* 不等于
* @param {*} actual - 实际值
* @param {*} expected - 期望值
* @returns {boolean}
*/
ne: (actual, expected) => actual !== expected,
/**
* 大于
* @param {number} actual - 实际值
* @param {number} expected - 期望值
* @returns {boolean}
*/
gt: (actual, expected) => {
const num = Number(actual)
const exp = Number(expected)
return !Number.isNaN(num) && !Number.isNaN(exp) && num > exp
},
/**
* 大于等于
* @param {number} actual - 实际值
* @param {number} expected - 期望值
* @returns {boolean}
*/
gte: (actual, expected) => {
const num = Number(actual)
const exp = Number(expected)
return !Number.isNaN(num) && !Number.isNaN(exp) && num >= exp
},
/**
* 小于
* @param {number} actual - 实际值
* @param {number} expected - 期望值
* @returns {boolean}
*/
lt: (actual, expected) => {
const num = Number(actual)
const exp = Number(expected)
return !Number.isNaN(num) && !Number.isNaN(exp) && num < exp
},
/**
* 小于等于
* @param {number} actual - 实际值
* @param {number} expected - 期望值
* @returns {boolean}
*/
lte: (actual, expected) => {
const num = Number(actual)
const exp = Number(expected)
return !Number.isNaN(num) && !Number.isNaN(exp) && num <= exp
},
// ========== 集合操作 ==========
/**
* 包含于(值在数组中)
* @param {*} actual - 实际值
* @param {Array} expected - 期望数组
* @returns {boolean}
*/
in: (actual, expected) => {
if (!Array.isArray(expected)) return false
return expected.includes(actual)
},
/**
* 不包含于(值不在数组中)
* @param {*} actual - 实际值
* @param {Array} expected - 期望数组
* @returns {boolean}
*/
nin: (actual, expected) => {
if (!Array.isArray(expected)) return true
return !expected.includes(actual)
},
// ========== 字符串操作 ==========
/**
* 字符串包含
* @param {string} actual - 实际值
* @param {string} expected - 期望子串
* @returns {boolean}
*/
contains: (actual, expected) => {
return String(actual ?? '').includes(String(expected ?? ''))
},
/**
* 字符串前缀匹配
* @param {string} actual - 实际值
* @param {string} expected - 期望前缀
* @returns {boolean}
*/
startsWith: (actual, expected) => {
return String(actual ?? '').startsWith(String(expected ?? ''))
},
/**
* 字符串后缀匹配
* @param {string} actual - 实际值
* @param {string} expected - 期望后缀
* @returns {boolean}
*/
endsWith: (actual, expected) => {
return String(actual ?? '').endsWith(String(expected ?? ''))
},
/**
* 正则表达式匹配
* @param {string} actual - 实际值
* @param {string|RegExp} expected - 正则表达式
* @returns {boolean}
*/
matches: (actual, expected) => {
try {
const regex = expected instanceof RegExp ? expected : new RegExp(expected)
return regex.test(String(actual ?? ''))
} catch {
return false
}
},
// ========== 布尔/空值操作 ==========
/**
* 真值检查
* @param {*} actual - 实际值
* @returns {boolean}
*/
truthy: (actual) => !!actual,
/**
* 假值检查
* @param {*} actual - 实际值
* @returns {boolean}
*/
falsy: (actual) => !actual,
/**
* 空值检查(null, undefined, 空字符串)
* @param {*} actual - 实际值
* @returns {boolean}
*/
empty: (actual) => actual === '' || actual === null || actual === undefined,
/**
* 非空检查
* @param {*} actual - 实际值
* @returns {boolean}
*/
notEmpty: (actual) => actual !== '' && actual !== null && actual !== undefined,
/**
* 空数组检查
* @param {*} actual - 实际值
* @returns {boolean}
*/
emptyArray: (actual) => !Array.isArray(actual) || actual.length === 0,
/**
* 非空数组检查
* @param {*} actual - 实际值
* @returns {boolean}
*/
notEmptyArray: (actual) => Array.isArray(actual) && actual.length > 0
}
/**
* 操作符别名映射(向后兼容)
*
* @description 旧格式操作符到新格式的映射
* @type {Object<string, string>}
*/
export const OPERATOR_ALIASES = {
equals: 'eq',
'==': 'eq',
'===': 'eq',
'!=': 'ne',
'!==': 'ne',
'>': 'gt',
'>=': 'gte',
'<': 'lt',
'<=': 'lte'
}
/**
* 解析操作符(支持别名)
*
* @param {string} op - 操作符名称或别名
* @returns {string|null} 标准操作符名称
*/
export function normalizeOperator(op) {
if (CONDITION_OPERATORS[op]) return op
if (OPERATOR_ALIASES[op]) return OPERATOR_ALIASES[op]
return null
}
/**
* 评估单个条件
*
* @description 评估一个简单的条件表达式
* @param {Object} condition - 条件对象
* @param {string} condition.field - 字段名
* @param {string} condition.op - 操作符
* @param {*} condition.value - 期望值
* @param {Object} formData - 表单数据
* @returns {boolean} 条件是否满足
*
* @example
* evaluateSingleCondition(
* { field: 'smoker', op: 'eq', value: '是' },
* { smoker: '是', age: 30 }
* ) // true
*/
export function evaluateSingleCondition(condition, formData) {
const { field, op, value } = condition
if (!field || !op) {
console.warn('[plan-conditions] 条件缺少 field 或 op:', condition)
return false
}
const normalizedOp = normalizeOperator(op)
if (!normalizedOp) {
console.warn(`[plan-conditions] 未知操作符: ${op}`)
return false
}
const operator = CONDITION_OPERATORS[normalizedOp]
const actualValue = formData[field]
try {
return operator(actualValue, value)
} catch (err) {
console.error(`[plan-conditions] 条件评估失败:`, err)
return false
}
}
/**
* 评估 AND 逻辑
*
* @param {Array} conditions - 条件数组
* @param {Object} formData - 表单数据
* @returns {boolean} 所有条件是否都满足
*/
export function evaluateAnd(conditions, formData) {
if (!Array.isArray(conditions) || conditions.length === 0) return true
return conditions.every(c => evaluateCondition(c, formData))
}
/**
* 评估 OR 逻辑
*
* @param {Array} conditions - 条件数组
* @param {Object} formData - 表单数据
* @returns {boolean} 是否有任一条件满足
*/
export function evaluateOr(conditions, formData) {
if (!Array.isArray(conditions) || conditions.length === 0) return false
return conditions.some(c => evaluateCondition(c, formData))
}
/**
* 评估 NOT 逻辑
*
* @param {Object} condition - 条件对象
* @param {Object} formData - 表单数据
* @returns {boolean} 条件是否不满足
*/
export function evaluateNot(condition, formData) {
if (!condition) return false
return !evaluateCondition(condition, formData)
}
/**
* 评估条件(主入口)
*
* @description 评估任意条件表达式,支持简单条件和逻辑组合
* @param {Object|Array} condition - 条件表达式
* @param {Object} formData - 表单数据
* @returns {boolean} 条件是否满足
*
* @example
* // 简单条件
* evaluateCondition({ field: 'smoker', op: 'eq', value: '是' }, formData)
*
* @example
* // AND 条件
* evaluateCondition({
* and: [
* { field: 'smoker', op: 'eq', value: '是' },
* { field: 'age', op: 'gte', value: 30 }
* ]
* }, formData)
*
* @example
* // OR 条件
* evaluateCondition({
* or: [
* { field: 'smoker', op: 'eq', value: '是' },
* { field: 'age', op: 'gt', value: 50 }
* ]
* }, formData)
*
* @example
* // 嵌套条件
* evaluateCondition({
* and: [
* { field: 'type', op: 'in', value: ['A', 'B'] },
* {
* or: [
* { field: 'coverage', op: 'gte', value: 100000 },
* { field: 'period', op: 'eq', value: '20年' }
* ]
* }
* ]
* }, formData)
*/
export function evaluateCondition(condition, formData) {
// 空条件默认为 true
if (!condition) return true
// 数组条件:默认为 AND 逻辑
if (Array.isArray(condition)) {
return evaluateAnd(condition, formData)
}
// 对象条件
if (typeof condition === 'object') {
// AND 逻辑
if (condition.and) {
return evaluateAnd(condition.and, formData)
}
// OR 逻辑
if (condition.or) {
return evaluateOr(condition.or, formData)
}
// NOT 逻辑
if (condition.not) {
return evaluateNot(condition.not, formData)
}
// 简单条件(包含 field 和 op)
if (condition.field && condition.op) {
return evaluateSingleCondition(condition, formData)
}
// 旧格式兼容:{ field: 'xxx', equals: 'yyy' }
if (condition.field && condition.equals !== undefined) {
return evaluateSingleCondition(
{ field: condition.field, op: 'eq', value: condition.equals },
formData
)
}
// 旧格式兼容:{ field: 'xxx', not_equals: 'yyy' }
if (condition.field && condition.not_equals !== undefined) {
return evaluateSingleCondition(
{ field: condition.field, op: 'ne', value: condition.not_equals },
formData
)
}
// 扁平条件对象:{ smoker: '是', age: 30 } → 所有条件 AND
const keys = Object.keys(condition)
if (keys.length > 0 && !keys.some(k => ['and', 'or', 'not', 'field', 'op'].includes(k))) {
return keys.every(field => {
const expectedValue = condition[field]
const actualValue = formData[field]
return actualValue === expectedValue
})
}
}
console.warn('[plan-conditions] 无法识别的条件格式:', condition)
return false
}
/**
* 获取条件依赖的字段列表
*
* @description 从条件表达式中提取所有依赖的字段名
* @param {Object|Array} condition - 条件表达式
* @returns {Set<string>} 依赖的字段名集合
*
* @example
* getConditionDependencies({ and: [{ field: 'a', op: 'eq', value: 1 }, { field: 'b', op: 'eq', value: 2 }] })
* // Set { 'a', 'b' }
*/
export function getConditionDependencies(condition) {
const deps = new Set()
if (!condition) return deps
// 数组条件
if (Array.isArray(condition)) {
condition.forEach(c => {
const subDeps = getConditionDependencies(c)
subDeps.forEach(d => deps.add(d))
})
return deps
}
if (typeof condition === 'object') {
// 简单条件
if (condition.field) {
deps.add(condition.field)
}
// 扁平条件对象
const keys = Object.keys(condition)
if (keys.length > 0 && !keys.some(k => ['and', 'or', 'not', 'field', 'op', 'equals'].includes(k))) {
keys.forEach(k => deps.add(k))
}
// 递归处理逻辑组合
if (condition.and) {
condition.and.forEach(c => {
const subDeps = getConditionDependencies(c)
subDeps.forEach(d => deps.add(d))
})
}
if (condition.or) {
condition.or.forEach(c => {
const subDeps = getConditionDependencies(c)
subDeps.forEach(d => deps.add(d))
})
}
if (condition.not) {
const subDeps = getConditionDependencies(condition.not)
subDeps.forEach(d => deps.add(d))
}
}
return deps
}
/**
* 将旧格式转换为新格式
*
* @description 将旧的 show_when 格式转换为新的条件格式
* @param {Object|Array|string} oldFormat - 旧格式条件
* @returns {Object|null} 新格式条件
*
* @example
* // 旧格式
* { field: 'withdrawal_mode', equals: '指定提取金额' }
* // 转换为
* { field: 'withdrawal_mode', op: 'eq', value: '指定提取金额' }
*
* @example
* // 旧格式数组
* [{ field: 'a', equals: 'x' }, { field: 'b', equals: 'y' }]
* // 转换为
* { and: [{ field: 'a', op: 'eq', value: 'x' }, { field: 'b', op: 'eq', value: 'y' }] }
*/
export function convertToNewFormat(oldFormat) {
if (!oldFormat) return null
// 已经是新格式
if (oldFormat.and || oldFormat.or || oldFormat.not || oldFormat.op) {
return oldFormat
}
// 旧格式单个条件
if (oldFormat.field && oldFormat.equals !== undefined) {
return {
field: oldFormat.field,
op: 'eq',
value: oldFormat.equals
}
}
// 旧格式数组
if (Array.isArray(oldFormat) && oldFormat.length > 0) {
const conditions = oldFormat.map(c => {
if (c.field && c.equals !== undefined) {
return { field: c.field, op: 'eq', value: c.equals }
}
return c
})
// 单个条件不需要包装 and
if (conditions.length === 1) {
return conditions[0]
}
return { and: conditions }
}
// 扁平对象格式 { smoker: '是' }
const keys = Object.keys(oldFormat)
if (keys.length > 0 && !keys.some(k => ['and', 'or', 'not', 'field', 'op', 'equals'].includes(k))) {
if (keys.length === 1) {
return { field: keys[0], op: 'eq', value: oldFormat[keys[0]] }
}
return {
and: keys.map(field => ({ field, op: 'eq', value: oldFormat[field] }))
}
}
return oldFormat
}