Skip to content

FinFormulaEditor 公式编辑器 ​

FinFormulaEditor 是以 AST 为唯一事实来源的可视化公式编辑器。组件负责资源选择、结构化编辑、本地校验、服务端校验、草稿导出和正式提交准备,不在浏览器中执行公式。

当前版本不提供源码编辑模式。公式文本只用于调试展示和复制,不能用于持久化或历史回显。

基础用法 ​

loading

演示中的“接口”由本地业务数据经 Promise 返回;接入真实项目时只需把模拟查询函数替换为接口请求,保留资源映射和文档协议。

核心数据流 ​

公式从创建到提交遵循单一流程:

  1. 业务方通过推荐的 sources 声明资源;高级场景也可直接提供左右面板、目录和解析器。
  2. 用户选择资源并通过节点菜单组合 AST。
  3. v-model 输出完整 MathFormulaDocument。
  4. 历史回显时,组件通过 resolver.resolveMany() 按稳定身份批量恢复资源状态。
  5. validateLocal() 执行结构、类型、资源和复杂度校验。
  6. validateServer() 把完整文档、修订号、协议版本和规则版本提交给服务端。
  7. saveDraft() 输出可安全持久化的草稿。
  8. prepareSubmit() 仅在当前修订同时通过本地与服务端校验后输出正式文档。

编辑器交互 ​

  • 从左侧选择字段、参数、常量,或从右侧选择函数和内置运算符;先选择画布中的目标节点,再进行结构化插入。运算符由组件生成语法结构,函数按签名生成参数槽,并校验参数类型及返回类型。
  • 普通选择资源不会静默覆盖已存在内容的节点。将鼠标移入非占位节点,点击节点下方的“替换”入口,再选择新字段、参数或函数;占位节点可直接填充。替换复合表达式时需要确认,替换状态可取消;只读和禁用状态不允许修改公式。
  • 在画布空白或公式节点区域按住鼠标左键拖动可平移;画布获得焦点后也可使用方向键平移,横向滚轮可浏览长公式。普通纵向滚轮仍以指针为中心缩放,范围为 50%~200%。画布右侧提供放大、缩小、比例和复位视图工具;复位只还原视角,不会清空公式。节点单击选择、资源拖拽插入以及只读或禁用状态下的画布浏览保持可用。
  • 下方“有效公式文本”只用于展示和复制;编辑区右上角“查看源码”展示并复制完整的公式文档 JSON。两处均不开放自由源码编辑,保存与回显必须使用完整的 MathFormulaDocument。

公式文档 ​

modelValue 不再是裸 AST,而是版本化公式文档:

ts
import { createMathFormulaDocument } from '@axewo/fin-formula-editor'

const document = createMathFormulaDocument({
  ruleVersion: 'finance-rule-v1',
  expectedType: 'number',
})
字段说明
schemaVersion公式协议版本;未知版本会被拒绝
statusdraft 或 committed
rootAST 根节点;空公式为 null
ruleVersion前后端共同使用的规则版本
expectedType可选的期望返回类型
editRevision语义变化时递增,用于绑定服务端校验结果

外部 JSON 必须先经过 decodeMathFormulaDocument(input)。解码器会检查字段、节点类型、字面量格式、资源身份、函数身份、子节点数量和全树节点标识唯一性。

资源身份 ​

引用节点固定保存稳定身份和显式映射的展示、执行快照:

json
{
  "id": "reference_1",
  "type": "Reference",
  "sourceId": "finance-fields",
  "resourceId": "revenue",
  "resourceKind": "field",
  "path": "income.revenue",
  "version": "2026-01",
  "label": "营业收入",
  "snapshot": {
    "valueType": "number",
    "nullable": false,
    "execution": {
      "tableId": "10",
      "fieldId": "101",
      "fieldCode": "revenue"
    }
  }
}
  • sourceId + resourceKind + resourceId + version 组成资源目录身份。
  • path 可表达资源内部路径,但服务端不能把客户端路径直接拼接成查询语句。
  • label 只用于展示;snapshot 会随 AST 持久化,可保存经过白名单映射的表标识、字段编码和类型等业务解析线索;资源目录的 metadata 只用于展示,不会自动进入 AST。
  • 后端必须按稳定身份和当前认证上下文重新解析资源,不得把客户端 snapshot.execution 中的表名、字段编码或 path 直接拼接到 SQL。标识符应由服务端可信目录校验并映射。
  • snapshot 只允许普通 JSON 数据,禁止复制完整业务 DTO、令牌、请求头或权限对象。

第三方资源映射 ​

使用 createMathResourceAdapter() 在接口边界显式映射白名单字段:

ts
const adaptFields = createMathResourceAdapter<BusinessField>({
  sourceId: 'finance-fields',
  resourceId: 'code',
  resourceKind: 'field',
  version: ({ item }) => item.version,
  label: 'name',
  valueType: ({ item }) => item.dataType === 'decimal' ? 'number' : 'string',
  path: 'path',
  snapshot: ({ item }) => ({
    valueType: item.dataType === 'decimal' ? 'number' : 'string',
    nullable: item.nullable,
  }),
})

适配器不会发送请求,也不会自动复制原始对象。

推荐:统一资源源 ​

常规接入只需声明业务资源来源。组件会按资源类型自动生成左右面板、搜索、分页、动态目录和组合解析器:

ts
import { defineMathResourceSource } from '@axewo/fin-formula-editor'

interface BusinessField {
  id: string
  tableId: string
  code: string
  name: string
  version: string
}

// 文档演示:使用内存数据模拟远程接口,接入业务时替换为真实请求。
const fieldData: BusinessField[] = [
  { id: '101', tableId: '10', code: 'revenue', name: '营业收入', version: '1' },
  { id: '102', tableId: '10', code: 'cost', name: '营业成本', version: '1' },
]

const fieldSource = defineMathResourceSource<BusinessField>({
  sourceId: 'finance-fields',
  resourceKind: 'field',
  label: '字段',
  dataSource: {
    mode: 'remote',
    load: ({ query, pagination, signal }) => Promise.resolve().then(() => {
      signal.throwIfAborted()
      const matches = fieldData.filter(item => item.name.includes(query.trim()))
      if (pagination.mode !== 'page') return { items: matches, hasMore: false }
      const start = (pagination.page - 1) * pagination.pageSize
      return {
        items: matches.slice(start, start + pagination.pageSize),
        total: matches.length,
        hasMore: start + pagination.pageSize < matches.length,
      }
    }),
  },
  map: ({ item }) => ({
    resourceId: item.code,
    version: item.version,
    label: item.name,
    valueType: 'number',
    snapshot: { execution: { tableId: item.tableId, fieldId: item.id, fieldCode: item.code } },
  }),
  loadDependencies: (dependencies, signal) => Promise.resolve().then(() => {
    signal.throwIfAborted()
    return fieldData.filter(item => dependencies.some(dependency => dependency.resourceId === item.code))
  }),
  tab: { searchMode: 'remote', pagination: { mode: 'page', pageSize: 20 } },
})

const sources = [fieldSource]
vue
<FinFormulaEditor v-model="document" :sources="sources" />

sourceId 和 resourceKind 只在资源源声明一次,map 返回值会自动补齐这两个字段;如果映射结果显式返回了不同身份,组件会立即拒绝。列表加载和 loadDependencies 返回的业务 DTO 共用同一个 map,组件自动判断依赖是可用、缺失还是版本不兼容,业务不需要重复组装 MathResolvedResource。字段、参数、常量默认显示在左侧,函数和运算符默认显示在右侧;可通过 position 和 tab 覆盖展示配置。

历史公式回显不能依赖当前页签是否加载到对应条目。远程资源应使用 loadDependencies 按当前公式依赖批量加载原始 DTO,不能通过 pageSize: -1 扫描全部分页数据。列表加载和依赖加载共用 map,组件会判断缺失和版本不兼容;若需要正确区分版本不兼容,依赖加载应按资源标识查询并返回服务端当前版本,而不是先在客户端用历史版本过滤。后端已直接实现标准解析协议时,可改用高级 resolveMany;配置后它优先于 loadDependencies。资源缓存只存在于当前编辑器实例,默认仅缓存短期 active 结果,不跨用户、租户或组件共享。

演示中的 Promise 只模拟请求形态,不会产生真实网络延迟;真实接口需要响应取消信号。组件会取消过期请求并忽略乱序响应,业务加载器仍应处理取消和服务端错误。

高级:直接配置资源面板 ​

需要完全控制面板结构时,可继续直接使用 MathResourcePanelConfig。显式 leftPanel、rightPanel 会分别替换 sources 生成的对应面板;显式 resolver 会整体替换组合解析器;显式 catalog 最后合并并拥有覆盖权。

左右两侧均使用 MathResourcePanelConfig。每个页签独立维护搜索词、分页、加载状态、错误和滚动位置。

ts
const leftPanel: MathResourcePanelConfig = {
  panelId: 'formula-left',
  position: 'left',
  defaultTabId: 'fields',
  tabs: [{
    tabId: 'fields',
    label: '字段',
    resourceKind: 'field',
    sourceId: 'finance-fields',
    dataSource: {
      mode: 'remote',
      load: ({ query, pagination, signal }) =>
        api.queryFields({ query, pagination }, { signal }),
    },
    searchMode: 'remote',
    pagination: { mode: 'page', pageSize: 20 },
  }],
}

搜索模式:

模式行为
local在本地完整数据中筛选
remote搜索词交给远程加载器
loaded只筛选当前已加载内容,不代表全量搜索
none不展示搜索入口

分页支持页码和游标两种模式。加载器统一接收 AbortSignal,组件会取消过期请求并丢弃乱序响应。

历史回显 ​

历史公式回显不能依赖当前资源页签是否加载到对应条目。推荐在资源源中批量加载原始 DTO,并继续复用列表映射:

ts
const source = defineMathResourceSource<BusinessField>({
  sourceId: 'finance-fields',
  resourceKind: 'field',
  dataSource: { mode: 'remote', load: queryFields },
  map: mapField,
  loadDependencies: (dependencies, signal) =>
    api.queryFieldsByIds(dependencies.map(item => item.resourceId), { signal }),
})

只有后端直接返回标准 MathResolvedResource[] 时,才需要高级解析器:

ts
const resolver: MathResourceResolver = {
  async resolveMany(dependencies, signal) {
    return api.resolveFormulaResources({ dependencies }, { signal })
  },
}

解析结果状态包括:

  • active:正常可用;
  • disabled:已停用;
  • missing:资源不存在;
  • forbidden:当前用户无权限;
  • incompatible:版本不兼容。

回显只建立只读状态和诊断,不会静默修改或删除原 AST。

本地与服务端校验 ​

本地校验使用完整文档:

ts
const result = validateMathFormulaDocument(document, {
  catalog,
  complexity: {
    maxNodes: 100,
    maxDepth: 20,
    maxFunctionCalls: 20,
    maxReferences: 50,
    maxScore: 300,
  },
})

服务端校验配置:

ts
const validation = {
  serverValidation: {
    scenario: 'financial-rule',
    businessContext: { reportId: 'report-001' },
    timeoutMs: 10_000,
    async transport(request, signal) {
      return api.validateFormula(request, { signal })
    },
  },
}

请求固定包含:

  • document:完整公式文档;
  • editRevision:当前编辑修订;
  • schemaVersion:公式协议版本;
  • ruleVersion:规则版本;
  • scenario:业务场景;
  • 可选 expectedType 和 businessContext。

服务端响应固定为:

ts
{
  validatedRevision: 3,
  valid: true,
  diagnostics: [],
  traceId: 'trace-001',
}

响应修订号必须等于请求修订号。公式变化后,旧结果立即失效;网络失败、超时、取消、未授权和协议错误属于请求状态,不能伪装成公式诊断,也不能视为验证通过。

草稿与正式提交 ​

ts
const draft = editorRef.value?.saveDraft()
const committed = await editorRef.value?.prepareSubmit()
  • saveDraft():允许空公式、占位节点、类型错误和失效资源,但文档必须符合协议且可安全序列化。
  • prepareSubmit():要求当前修订通过本地校验,并具有同一修订的服务端通过结果;必要时自动发起服务端校验。
  • 组件只产生可提交文档,实际持久化由业务方负责。

Props ​

参数类型默认值说明
modelValueMathFormulaDocument空草稿版本化公式文档
ruleVersionstring'1'空文档初始化和规则切换版本
catalogMathCatalogInput内置目录高级目录配置;在 sources 目录后合并并覆盖
sourcesreadonly MathResourceSourceConfig[]—推荐入口;统一声明资源列表、DTO 映射和依赖解析
leftPanelMathResourcePanelConfig—高级左侧面板;覆盖 sources 生成的左面板
rightPanelMathResourcePanelConfig—高级右侧面板;覆盖 sources 生成的右面板
resolverMathResourceResolver—高级历史解析器;整体覆盖 sources 组合解析器
validationOmit<MathValidationOptions, 'catalog'>—本地与服务端校验配置
validateOnChangebooleantrue文档变化后是否执行本地校验
disabled / readonlybooleanfalse禁用或只读
clearablebooleantrue是否显示清空按钮
maxHistorynumber100撤销历史上限
height / minHeightnumber | string620 / 480编辑器尺寸
placeholderstring默认引导文案空画布引导提示
localestring全局语言目录展示语言

Events ​

事件载荷说明
update:modelValueMathFormulaDocument文档语义变化
changeMathEditorChangePayload文档、原因和当前本地校验结果
validation-changeMathValidationResult本地校验结果变化
resource-loadMathResourceLoadEvent资源请求开始
resource-loadedMathResourceLoadSuccessEvent资源请求成功
resource-load-errorMathResourceLoadErrorEvent资源请求失败
command-blockedMathCommandBlockedEvent编辑命令或资源插入被阻止,包含 reason、可选目标和资源
draftMathDraftResult草稿导出成功
submit-readyMathSubmitResult当前修订可正式提交
focus / blurFocusEvent编辑器焦点事件

Exposed API ​

方法说明
focus() / blur()聚焦或移出画布
clear()清空根公式
undo() / redo()撤销或重做,返回是否发生变化
canUndo() / canRedo()查询历史状态
getDocument()获取文档深拷贝
setDocument(input)严格解码并设置外部文档
executeCommand(command)执行文档级不可变命令
selectNode(nodeId?)选择或取消选择节点
validateLocal()执行本地校验
validateServer()执行结构化服务端校验
saveDraft()导出草稿文档
prepareSubmit()准备正式提交文档

插槽 ​

  • header:编辑器头部;
  • left-resource-item / right-resource-item:资源项内容;
  • left-resource-group-label / right-resource-group-label:资源分组标题;
  • left-resource-detail / right-resource-detail:资源详情;
  • diagnostic-item:可信诊断展示;
  • footer:接收当前 validation 和 document。

文本解析与序列化边界 ​

parseMathExpression() 和 serializeMathExpression() 仍作为调试、测试与展示工具保留,但文本不携带完整资源身份。文本解析得到的引用只具有 sourceId: 'text-parser' 的调试身份,不能直接替代业务公式文档。

正式导入使用:

ts
const decoded = decodeMathFormulaDocument(input)
if (!decoded.ok) {
  console.table(decoded.issues)
  return
}

const stableJson = encodeMathFormulaDocument(decoded.document)

持久化时始终保存完整公式文档,不要只保存公式文本。