)
Frappe UI Filter 控件设计解析基于共享字段组件与条件列表模型的元数据驱动过滤ADR-0003【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe导读本文围绕framework/uiFrappe 的 Vue 3 前端组件库位于仓库 ui 目录中列表视图Filter过滤控件的核心架构决策展开该决策记录在 ADR-0003 中Filter 控件通过移植 CRM 的Filter.vue逻辑与数据模型一条{ field, fieldname, operator, value }条件列表实现而不是包装 frappe-ui 的ListFilter。读完本文你将掌握 Filter 控件的条件数据模型、操作符表设计、值输入控件的复用机制、serializeFilters/parseFilters线格式转换以及它为何能与 Frappe 的get_list直接对接——这些同样适用于任何基于 Frappe 构建的列表页。一、决策背景Filter 控件从何而来ADR-0003 是 Frappe UI 列表视图控件系列 ADR 之一。在此之前ADR-0001 已确立总体原则SortBy / Filter / ColumnSettings / QuickFilter 等列表控件应当是受控controlled、元数据驱动meta-driven的组件——每个控件只通过v-model拥有一份状态切片接收一个doctype客户端从 doctype 的 Meta通过共享的useDoctypeMeta派生 Field Options发出变更后由宿主负责请求与持久化。在此前提下ADR-0003 要回答一个更具体的问题Filter 控件应该以什么方式实现候选方案有三条对应三种截然不同的架构取向方案说明结论包装 frappe-uiListFilter约 295 行直接复用第三方控件否决dict 模型结构性缺陷 功能降级逐字复制 CRM 的Filter.vue717 行连专属控件一起搬进组件库否决重复造轮子共享字段输入无法被复用移植 CRM 的逻辑与数据模型值输入复用共享Fields模块本文所描述的方案采纳被采纳的方案要求 Filter 达到与 CRM 弹出层功能与像素双对齐feature/pixel parity同时通过复用共享值输入组件来“吃自家狗粮”dogfooding。下文各节将逐一拆解该方案的每个组成部分及其在仓库源码中的落点。二、核心数据模型条件列表Filter[]而非 dict2.1 UI 层的条件结构Filter 的v-model是一个条件数组其类型定义位于 types.tsexport interface Filter { fieldname: string; operator: FilterOperator; value: FilterValue; /** 该行条件所依赖的字段 Meta纯线格式辅助函数中可选控件会填充它。 */ field?: FilterField; }其中FilterOperator采用 CRM 的 UI 词汇表注意这是界面层操作符与 Frappe 线格式操作符不同由serializeFilters负责映射export type FilterOperator | equals | not equals | like | not like | in | not in | is | is not | | | | | between | timespan;FilterValue则覆盖三种取值形态标量字符串/布尔、列表in/not in、以及[from, to]二元组between。2.2 为什么必须是列表而不是 dict——同字段重复过滤这是 ADR-0003 最关键的刻意分歧deliberate divergence。frappe-uiListFilter与 CRM 的parseFilters都采用{ fieldname: [operator, value] }的dict 模型而 dict 在结构上无法承载同一字段被过滤两次的情形例如amount 100 AND amount 500dict 的 key 是 fieldnameamount只能出现一次和两条条件无法同时表达。而 Frappe 的列表过滤本质上是条件的集合同字段多条件完全合法。因此 Filter 控件采用列表形态Filter[]并让serializeFilters/parseFilters以三元组列表为线格式——这是唯一与控件Filter[]模型一致的数据形态。该设计在实现中得到验证Filter.vue 的注释明确指出字段选择器故意不排除已被选中的字段因为一个字段可以承载多条条件测试 filters.test.ts 也专门验证了amount的与两条条件序列化为两个独立三元组。三、Field Options从 doctype Meta 派生替代 CRM 端点CRM 中可过滤字段来自crm.api.doc.get_filterable_fields端点ADR-0001 已否决“保留该端点”理由是它会把组件库与 CRM 的数据形态耦合。ADR-0003 延续该决策Field Options 由useDoctypeMeta派生。实现位于 getFilterableFields.ts可过滤字段类型白名单FILTERABLE_FIELDTYPES移植自 CRM 的allowed_fieldtypes涵盖 Autocomplete、Check、Data、Float、Int、Currency、Dynamic Link、Link、Long Text、Select、Small Text、Text Editor、Text、Duration、Rating、Date、Datetime标准字段前置STANDARD_FIELDS每个 doctype 都可过滤的公共字段包括name、owner、modified_by、_user_tags、_liked_by、_comments、_assign、creation、modified按 CRM 的standard_fields顺序排在 Meta 字段之前name字段特例其options被设为当前doctype使 Name 上的equals/in过滤指向该 doctype 自身记录与服务端options: doctype语义一致见 getFilterableFields.ts输出形状label/value/fieldname/fieldtype/options与 CRM 的get_filterable_fields行结构对齐value fieldname保证存量宿主可无改动接入。在 Filter.vue 中allFields即通过getFilterableFields(meta.value?.fields ?? [], props.doctype)计算得出——全程客户端派生无任何 CRM 端点调用。四、操作符表按字段类型分组的纯函数4.1 每组字段类型的操作符集合CRM 的getOperators被提炼为一个无 frappe-ui 依赖的纯.ts模块operators.ts使其可被单元测试直接加载。操作符按字段类型分组字段类型组操作符代码位置字符串Data / Long Text / Small Text / Text Editor / Textequals、not equals、like、not like、in、not in、isoperators.ts数值Float / Int / Currency / Percent字符串组 、、、共 11 个operators.tsSelect / Autocompleteequals、not equals、in、not in、isoperators.tsLink / Dynamic Link同字符串组operators.tsCheck仅 equalsoperators.tsDurationlike、not like、in、not in、isoperators.tsDate / Datetime完整九种equals、not equals、is、、、、、between、timespanoperators.tsRatingequals、not equals、、、、、isoperators.ts这与 ADR 文档的概述完全一致字符串得到 equals / like / in / is…Date 得到含between/timespan的完整九种Check 只有 equals。4.2_assign特例getOperators(fieldtype, fieldname)的第二个参数处理_assign特殊字段无论其字段类型如何一律只提供 like / not like / is 三个操作符见 operators.ts 与 operators.ts。这与 CRM 行为一致用于“指派给某人”这类归属过滤场景。4.3 默认操作符与默认值同一模块还导出了新条件行的默认行为getDefaultOperatorSelect/Check/数值默认equalsDate 默认between其余默认likegetDefaultValueSelect 预选第一个选项Check 默认YesDate 为null其余为空字符串defaultValueFor切换到is/is not时值重置为set切到in/not in时选项字段重置为空数组、自由文本字段重置为空字符串carryOver修改行的字段时若新字段仍支持原操作符则保留操作符与值否则回退到新字段的默认条件Select 值必须存在于新字段选项、Link 值必须属于同一目标 doctype 时才会携带见 operators.ts。五、值输入控件复用共享Fields模块5.1 为什么不复用 CRM 专属控件或 frappe-ui FormControlADR-0004fields-relocated-to-shared-module已将 FormLayout 中的字段类型值输入组件提升为独立共享模块src/components/Fields/使 FormLayout 与列表视图的 Filter / QuickFilter 共同消费它们。ADR-0003 在此基础上进一步明确Filter 的值输入必须是共享Fields模块的组件——不采用 CRM 的专属Link/DurationInput/RatingInput三件套那会把 CRM 的专属实现作为Fields值输入组件的重复副本拖入组件库导致“同一值输入有两个家”、二者逐步分叉不采用 frappe-ui 的FormControl它是通用表单控件不具备字段类型感知能力。Filter 只挂载零耦合子集Check、Select、Link、Date、Datetime、Number、Duration、Rating、text从不提供表单上下文注入因此需要表单上下文深度注入的组件如 TableField 等在 Filter 上下文中自然休眠无需特殊处理。5.2 操作符驱动的输入切换valueControl.tsADR 文档指出共享输入尚未覆盖的几个缺口——Datebetween→ 区间选择、is/is not→ Set/Not-Set 下拉、timespan预设——是由Filter 的.vue内部按操作符驱动的输入切换处理的而非新增字段组件。这一逻辑被独立到 valueControl.ts而非塞进Filter.vue本身其优点是嵌套的 ConditionBuilder 可以基于同一套规则渲染相同的值输入无需复制见 Filter.vue 的注释。valueControl(f)的判定顺序是“操作符优先其次字段类型”is/is not→setSet / Not Set 下拉选项见 valueControl.tstimespan→timespan17 个预设区间last week、last month、this quarter、next 6 months、yesterday 等见 valueControl.tsin/not in且字段为选项字段 →multiSelectLink 为multiLinkDynamic Link 与自由文本则回落到文本框like / not like / in / not in文本系→text文本框Select / Check →selectCheck 的选项固定为Yes\nNoLink →linkDynamic Link 无固定目标 doctype回落为纯文本数值 →numberDate / Datetime between→dateRangeDateRangePickerDuration →duration、Rating →ratingDate →date、Datetime →datetime兜底 →text。valueControl.ts只返回控件 id 与 propsValueControlSpec组件映射表放在 valueControlComponents.ts 中使调度规则模块保持零.vue导入、可被测试运行器加载。5.3 共享输入在模板中的渲染Filter.vue 中每行条件通过动态组件渲染值输入component :isVALUE_CONTROLS[valueControl(f).control] v-bindvalueControl(f).props classw-full :modelValuef.value update:modelValue(v) updateValue(v, i) /每行条件由“Where / And”标签、字段 Combobox、操作符 Select、值输入、删除按钮构成所有行共享同一 CSS Gridgrid-cols-[auto_auto_auto_auto_auto]保证各行的字段/操作符/值列在垂直方向对齐见 Filter.vue。操作符切换时会用defaultValueFor重置值为适配新操作符的默认值如 Set/Not-Set 的set、in的空列表。六、线格式serializeFilters/parseFilters6.1 三元组列表与 Frappeget_list直连线格式wire form是 Frappe 的[fieldname, operator, value]三元组列表WireFilters定义在 filters.ts。ADR 文档明确强调这是有意的分歧——不是CRM 用 fieldname 作 key 的 dict。原因与拒绝ListFilter的理由完全相同dict 无法承载同一字段被过滤两次。宿主拿到这个列表后可以直接作为 Frappeget_list的filters参数传递。6.2 序列化规则serializeFilters 将条件数组映射为三元组列表其中UI 操作符 → wire 操作符WIRE_OPERATOR表完成映射equals → 、not equals → !、like → LIKE、not like → NOT LIKE、is / is not / in / not in / between / timespan原样透传见 filters.tsCheck 布尔化Yes → true、No → falsefilters.tslike通配符包裹字符串值若不含%则自动包成%value%filters.ts这也是valueControl.ts中like的 placeholder 只提示裸词如John而非%John%的原因in值归一选项字段传入数组时直接透传自由文本传入逗号字符串时按逗号拆分、去空格、过滤空项filters.ts。6.3 反序列化规则parseFilters 是序列化的逆操作UI_OPERATOR表把 wire 操作符映射回 UI 词汇含/!/LIKE/NOT LIKE等Check 字段的-布尔值恢复为Yes/No的 equals 条件字段不在fields列表Meta中的条件被丢弃——控件无法渲染没有 Meta 的行顺序保留因此同一字段的两条条件往返后仍是两行。6.4 测试验证filters.test.ts 以 Vitest 对这对纯函数做了完整验证equals序列化为三元组、like值包裹%、逗号字符串拆分为数组、MultiSelect 数组透传、Check 布尔往返、多条件合并、同字段双条件保持为两个三元组以及parseFilters反向解析、Check 布尔还原、字段缺失丢弃、双条件往返一致。七、受控组件Filter 只读状态、只发变更Filter 被设计为受控组件Filter.vueconst model defineModelFilter[]({ default: () [] });v-model是条件数组组件不持有数据资源、不调用任何 CRM 端点、不负责持久化。交互逻辑全部围绕数组的不可变更新addFilter(option)从字段选择器选中字段用conditionFor(field)生成新条件行并在弹层挂载后自动打开它updateField(option, i)用carryOver智能保留可延续的操作符与值updateOperator(op, i)用defaultValueFor重置值为适配新操作符的默认值updateValue(v, i)、removeFilter(i)、clearAll(close)分别更新、删除、清空条件。空状态时显示一个普通“Filter”按钮Combobox 的#trigger渲染真实 Button点击打开字段选择器非空时显示带条件计数徽标的按钮与弹层见 Filter.vue。弹层的定位复用 frappe-ui 原语Popover自包含的 Popper 封装避免自行推导定位逻辑。宿主侧的状态封装见 useFilters.tsconditions是 Filter 与 QuickFilter 共同绑定的同一数组跨控件同步零事件接线对应 ADR-0005wire是serializeFilters的计算投影宿主直接拿wire去取数。八、被否决的方案为什么三条路都走不通ADR-0003 的 “Considered Options” 部分完整记录了三个被否决选项及其理由这正是理解本决策价值的关键包装 frappe-uiListFilter约 295 行。否决理由有二其一其 dict 模型{ fieldname: [operator, value] }在结构上无法容纳同一字段被过滤两次如amount 10 AND amount 100其二它每种类型只提供 2–4 个操作符且输入只有 select / Link / text 三类——功能降级无法满足像素/功能对齐的验收标准。逐字复制 CRM 的Filter.vue717 行连同自定义控件一起。否决理由会把 CRM 专属的Link/DurationInput/RatingInput作为Fields值输入组件的重复品拖进组件库——什么都未被复用dogfooding 落空值输入出现两个家并逐步分叉。保留 CRM 的get_filterable_fields端点。ADR-0001 已否决组件库会与 CRM 的数据形态耦合Field Options 应从 Meta 派生。这三个否决理由共同指向一个结论共享组件的价值来自单一数据模型条件列表与单一组件来源Fields 模块任何“就近复制”都会让两者分叉。九、导出面与宿主接入Filter 模块的公共导出位于 index.ts组件FilterFilter.vue线格式助手parseFilters/serializeFilters及WireFilter/WireFilters类型纯函数getFilterableFields、getOperators、getDefaultOperator、getDefaultValue及OperatorOption类型类型FilterCondition即Filter、FilterField、FilterOperator、FilterProps、FilterValue。在列表页中的典型用法完整集成示例见 ListView/USAGE.mdFilter v-modelview.filters.conditions.value :doctypedoctype /其中view来自useListView(doctype)取数时使用 wire 投影import { serializeFilters } from framework/ui/Filter; // view.filters.wire.value → Frappe filter list // 例如 [[status, , Open], [amount, , 100]]由于 Filter 是受控组件宿主还可以直接赋值条件数组如从 localStorage 或数据库恢复并通过parseFilters将存储的 wire 三元组还原为带 Meta 的条件列表。十、小结ADR-0003 把 Filter 控件的设计收敛为一条清晰的主线以条件列表为唯一数据模型、以 doctype Meta 为 Field Options 来源、以共享 Fields 组件为值输入实现、以三元组列表为 Frappe 线格式。从仓库源码看这条主线在每个层面都有对应落点Filter[]类型定义在 types.ts操作符表在 operators.ts值输入调度在 valueControl.ts 与 valueControlComponents.ts线格式转换在 filters.ts并有 filters.test.ts 对最易出错的部分操作符映射、通配符、布尔化、同字段多条件做了单元级验证。这套设计不仅让 Filter 与 QuickFilter 共享同一状态切片、与 ConditionBuilder 复用同一套输入规则也为其他基于 Frappe 的列表页提供了一份可直接借鉴的“受控 元数据驱动 共享字段输入”参考实现。【免费下载链接】frappeLow code web framework for real world applications, in Python and Javascript项目地址: https://gitcode.com/GitHub_Trending/fr/frappe创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考