useFieldDependencies.js 9.98 KB
/**
 * 字段关联系统 Composable
 *
 * @description 管理计划书字段之间的关联关系(显示/隐藏、启用/禁用)
 * @module composables/useFieldDependencies
 * @author Claude Code
 * @created 2026-02-14
 * @updated 2026-02-15 - 集成新的条件评估引擎,支持复杂条件
 */

import { computed, reactive, isRef, watch } from 'vue'
import { PLAN_FIELD_DEFINITIONS } from '@/config/plan-fields'
import {
  evaluateCondition,
  getConditionDependencies,
  convertToNewFormat
} from '@/config/plan-conditions'

/**
 * �测循环依赖
 *
 * @private
 * @param {string} fieldKey - 字段键名
 * @param {Set<string>} visited - 已访问的字段集合(用于递归)
 * @returns {boolean} 是否存在循环依赖
 *
 * @example
 * // 场景:A 依赖 B,B 依赖 C,C 依赖 A(循环)
 * detectCircularDeps('A') // false
 * detectCircularDeps('B') // true
 * detectCircularDeps('C') // true
 */
function detectCircularDeps(fieldKey, fieldDefinitions, visited = new Set()) {
  // 防止无限递归
  if (visited.size > 50) {
    console.error('[useFieldDependencies] 依赖层级过深,可能存在循环依赖')
    return true
  }

  // 检查是否已访问
  if (visited.has(fieldKey)) {
    console.error(`[useFieldDependencies] �测到循环依赖: ${[...visited, fieldKey].join(' -> ')}`)
    return true
  }
  visited.add(fieldKey)

  const definition = fieldDefinitions[fieldKey]
  if (!definition?.affects) return false

  // 递归检查依赖字段
  for (const depKey of definition.affects) {
    if (detectCircularDeps(depKey, fieldDefinitions, visited)) {
      return true
    }
  }

  visited.delete(fieldKey)
  return false
}

/**
 * 字段关联系统
 *
 * @description 管理字段的显示/隐藏状态,根据字段关联关系自动更新
 * @param {Object} formData - 表单数据
 * @returns {Object} 字段关联管理方法和状态
 *
 * @example
 * const { visibleFields, updateFieldValue, isFieldVisible, isFieldEnabled } = useFieldDependencies(formData)
 *
 * // 检查字段是否可见
 * if (isFieldVisible('withdrawal_mode')) {
 *   // 处理逻辑
 * }
 *
 * // 更新字段值
 * updateFieldValue('withdrawal_enabled', true)
 *
 * // 获取所有可见字段
 * const visible = visibleFields.value
 */
export function useFieldDependencies(formData, fieldDefinitions = PLAN_FIELD_DEFINITIONS) {
  // 字段显示状态映射
  const fieldVisibility = reactive({})

  // 字段启用状态映射
  const fieldEnabled = reactive({})

  const getFieldDefinitions = () => {
    const definitions = isRef(fieldDefinitions) ? fieldDefinitions.value : fieldDefinitions
    return definitions || {}
  }

  /**
   * 检查字段是否应该显示
   *
   * @description 使用新的条件评估引擎,支持复杂条件(AND/OR/NOT/嵌套)
   * @param {string} fieldKey - 字段键名
   * @returns {boolean} 是否显示
   */
  function isFieldVisible(fieldKey) {
    const definitions = getFieldDefinitions()
    const definition = definitions[fieldKey]
    if (!definition) return false

    // 使用新的条件评估引擎评估 show_when
    if (definition.show_when) {
      // 转换为新格式(向后兼容)
      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 才显示
        if (formData[key] !== true) {
          return false
        }
      }
    }

    return true
  }

  /**
   * 检查字段是否启用
   *
   * @param {string} fieldKey - 字段键名
   * @returns {boolean} 是否启用
   */
  function isFieldEnabled(fieldKey) {
    const definition = getFieldDefinitions()[fieldKey]
    if (!definition) return false

    // 如果有依赖字段,检查依赖字段是否满足
    if (definition.depends_on) {
      const depValue = formData[definition.depends_on]
      return depValue === true
    }

    return true
  }

  /**
   * 更新字段值并更新关联状态
   *
   * @param {string} fieldKey - 字段键名
   * @param {*} value - 新值
   */
  function updateFieldValue(fieldKey, value) {
    formData[fieldKey] = value

    // 更新受影响字段的显示状态
    const definition = getFieldDefinitions()[fieldKey]
    if (definition?.affects) {
      for (const affectedKey of definition.affects) {
        fieldVisibility[affectedKey] = isFieldVisible(affectedKey)
        fieldEnabled[affectedKey] = isFieldEnabled(affectedKey)
      }
    }
  }

  /**
   * 获取所有可见字段列表
   *
   * @returns {string[]} 可见字段键名数组
   */
  const visibleFields = computed(() => {
    return Object.keys(getFieldDefinitions()).filter(key => isFieldVisible(key))
  })

  /**
   * 初始化所有字段的显示状态(包含循环依赖检测)
   */
  function initFieldStates() {
    const definitions = getFieldDefinitions()
    // 开发环境检测循环依赖
    if (process.env.NODE_ENV === 'development') {
      for (const key of Object.keys(definitions)) {
        detectCircularDeps(key, definitions)
      }
    }

    for (const key of Object.keys(definitions)) {
      fieldVisibility[key] = isFieldVisible(key)
      fieldEnabled[key] = isFieldEnabled(key)
    }
  }

  // 初始化
  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,
    fieldEnabled,
    visibleFields,

    // 方法
    isFieldVisible,
    isFieldEnabled,
    updateFieldValue,
    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
}