From f52b24eacb0b7b9f9a9b07cbceefb70791cf60cd Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Thu, 19 Jun 2025 15:06:44 +0800 Subject: [PATCH] refactor: fix bug --- .../AssociationSelectFieldModel/index.tsx | 306 +++++++++--------- .../table/fields/PercentColumnModel.tsx | 13 +- 2 files changed, 157 insertions(+), 162 deletions(-) diff --git a/packages/core/client/src/flow/models/form/fields/AssociationSelectFieldModel/index.tsx b/packages/core/client/src/flow/models/form/fields/AssociationSelectFieldModel/index.tsx index 3281e736db..9b11be9e37 100644 --- a/packages/core/client/src/flow/models/form/fields/AssociationSelectFieldModel/index.tsx +++ b/packages/core/client/src/flow/models/form/fields/AssociationSelectFieldModel/index.tsx @@ -14,7 +14,6 @@ import { FormFieldModel } from '../../../FormFieldModel'; function toValue(record: any | any[], fieldNames, multiple = false) { if (!record) return multiple ? [] : undefined; - const { label: labelKey, value: valueKey } = fieldNames; const convert = (item: any) => { @@ -50,29 +49,22 @@ function LazySelect(props) { } export const AssociationSelect = connect( - (props: any) => { - return ; - }, - mapProps( - { - dataSource: 'options', - }, - (props, field) => { - return { - ...props, - }; - }, - ), - mapReadPretty((props) => { - return props.value; - }), + (props: any) => , + mapProps({ dataSource: 'options' }), + mapReadPretty((props) => props.value), ); export class AssociationSelectFieldModel extends FormFieldModel { static supportedFieldInterfaces = ['m2m', 'm2o', 'o2o', 'o2m', 'oho', 'obo', 'updatedBy', 'createdBy']; dataSource; fieldNames: { label: string; value: string; color?: string; icon?: any }; - + optionsLoadState?: { + page: number; + pageSize: number; + loading: boolean; + hasMore: boolean; + searchText?: string; + }; set onPopupScroll(fn) { this.field.setComponentProps({ onPopupScroll: fn }); } @@ -93,25 +85,58 @@ export class AssociationSelectFieldModel extends FormFieldModel { } } +export const DEFAULT_ASSOCIATION_PAGE_SIZE = 40; + +async function fetchAssociationItems({ ctx, page = 1, searchText = '' }) { + const { target } = ctx.model.collectionField.options; + const fieldNames = ctx.model.field.componentProps.fieldNames; + const labelField = fieldNames?.label; + + const apiClient = ctx.app.apiClient; + const collectionManager = ctx.model.collectionField.collection.collectionManager; + const targetCollection = collectionManager.getCollection(target); + const targetLabelField = targetCollection.getField(labelField); + const targetInterface = ctx.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface( + targetLabelField.options.interface, + ); + const operator = targetInterface?.filterable?.operators?.[0]?.value || '$includes'; + + const filter = searchText + ? { + [labelField]: { + [operator]: searchText, + }, + } + : undefined; + + const response = await apiClient.request({ + url: `/${target}:list`, + params: { + page, + pageSize: DEFAULT_ASSOCIATION_PAGE_SIZE, + filter, + }, + }); + + return response.data?.data || []; +} + +//初始化 AssociationSelectFieldModel.registerFlow({ key: 'init', auto: true, sort: 100, steps: { step1: { - handler(ctx, params) { - let initialized = false; + handler(ctx) { ctx.model.onDropdownVisibleChange = (open) => { - if (open && !initialized) { - initialized = true; - - ctx.model.dispatchEvent('dropdownOpen', { - apiClient: ctx.app.apiClient, - field: ctx.model.field, - form: ctx.model.form, - }); - } + ctx.model.dispatchEvent('dropdownOpen', { + apiClient: ctx.app.apiClient, + field: ctx.model.field, + form: ctx.model.form, + }); }; + ctx.model.onPopupScroll = (e) => { ctx.model.dispatchEvent('popupScroll', { event: e, @@ -119,6 +144,7 @@ AssociationSelectFieldModel.registerFlow({ field: ctx.model.field, }); }; + ctx.model.onSearch = (searchText) => { ctx.model.dispatchEvent('search', { searchText, @@ -130,6 +156,87 @@ AssociationSelectFieldModel.registerFlow({ }, }, }); +// 下拉打开加载第一页数据 +AssociationSelectFieldModel.registerFlow({ + key: 'dropdownOpen', + on: { eventName: 'dropdownOpen' }, + steps: { + step1: { + async handler(ctx) { + ctx.model.optionsLoadState = { + page: 1, + pageSize: DEFAULT_ASSOCIATION_PAGE_SIZE, + hasMore: true, + loading: false, + }; + + const data = await fetchAssociationItems({ ctx, page: 1 }); + ctx.model.setDataSource(data); + }, + }, + }, +}); + +// 滚动分页追加 +AssociationSelectFieldModel.registerFlow({ + key: 'popupScroll', + on: { eventName: 'popupScroll' }, + steps: { + step1: { + async handler(ctx) { + const event = ctx.extra?.event; + if (!event?.target) return; + + const { scrollTop, scrollHeight, clientHeight } = event.target; + // 只在接近底部时才触发加载 + if (scrollTop + clientHeight < scrollHeight - 20) { + return; + } + const state = (ctx.model.optionsLoadState ||= { + page: 1, + pageSize: DEFAULT_ASSOCIATION_PAGE_SIZE, + hasMore: true, + loading: false, + }); + if (state.loading || !state.hasMore) return; + state.loading = true; + try { + const nextPage = state.page + 1; + const data = await fetchAssociationItems({ ctx, page: nextPage, searchText: state.searchText || '' }); + ctx.model.setDataSource([...ctx.model.getDataSource(), ...data]); + state.hasMore = data.length === state.pageSize; + if (state.hasMore) state.page = nextPage; + } finally { + state.loading = false; + } + }, + }, + }, +}); + +//搜索数据 +AssociationSelectFieldModel.registerFlow({ + key: 'search', + on: { eventName: 'search' }, + steps: { + step1: { + async handler(ctx) { + const searchText = ctx.extra.searchText?.trim(); + ctx.model.optionsLoadState = { + page: 1, + pageSize: DEFAULT_ASSOCIATION_PAGE_SIZE, + hasMore: true, + loading: false, + searchText, + }; + + const data = await fetchAssociationItems({ ctx, page: 1, searchText }); + ctx.model.setDataSource(data); + }, + }, + }, +}); + const loadTitleFieldOptions = (collectionField, dataSourceManager) => { return async (field) => { const form = field.form; @@ -141,7 +248,9 @@ const loadTitleFieldOptions = (collectionField, dataSourceManager) => { const targetCollection = collectionManager.getCollection(target); const targetFields = targetCollection?.getFields?.() ?? []; + field.loading = true; + const options = targetFields .filter((field) => isTitleField(dataSourceManager, field.options)) .map((field) => ({ @@ -154,6 +263,7 @@ const loadTitleFieldOptions = (collectionField, dataSourceManager) => { }; }; +// 标题字段设置 AssociationSelectFieldModel.registerFlow({ key: 'fieldNames', auto: true, @@ -165,152 +275,26 @@ AssociationSelectFieldModel.registerFlow({ label: { 'x-component': 'Select', 'x-decorator': 'FormItem', - 'x-reactions': ['{{loadTitleFieldOptions(collectionField,dataSourceManager)}}'], + 'x-reactions': ['{{loadTitleFieldOptions(collectionField, dataSourceManager)}}'], }, }, - defaultParams: (ctx) => { - const fieldNames = { - ...ctx.model.collectionField.options?.uiSchema?.['x-component-props']?.['fieldNames'], - ...ctx.model.field.componentProps.fieldNames, - }; - return { - label: fieldNames.label, - }; - }, + defaultParams: (ctx) => ({ + label: ctx.model.field.componentProps.fieldNames?.label, + }), handler(ctx, params) { ctx.model.flowEngine.flowSettings.registerScopes({ loadTitleFieldOptions, collectionField: ctx.model.collectionField, dataSourceManager: ctx.app.dataSourceManager, }); + const newFieldNames = { - ...ctx.model.collectionField.options?.uiSchema?.['x-component-props']?.['fieldNames'], ...ctx.model.field.componentProps.fieldNames, label: params.label, }; + ctx.model.field.setComponentProps({ fieldNames: newFieldNames }); }, }, }, }); -AssociationSelectFieldModel.registerFlow({ - key: 'event1', - on: { - eventName: 'dropdownOpen', - }, - steps: { - step1: { - async handler(ctx, params) { - const { target } = ctx.model.collectionField.options; - const apiClient = ctx.app.apiClient; - const response = await apiClient.request({ - url: `/${target}:list`, - params: { - pageSize: 20, - }, - }); - const { data } = response.data; - ctx.model.setDataSource(data); - }, - }, - }, -}); - -const paginationState = { - page: 1, - pageSize: 20, - loading: false, - hasMore: true, -}; - -AssociationSelectFieldModel.registerFlow({ - key: 'event2', - on: { - eventName: 'popupScroll', - }, - steps: { - step1: { - async handler(ctx, params) { - if (paginationState.loading || !paginationState.hasMore) { - return; - } - paginationState.loading = true; - - try { - const { target } = ctx.model.collectionField.options; - const apiClient = ctx.app.apiClient; - - const response = await apiClient.request({ - url: `/${target}:list`, - params: { - page: paginationState.page, - pageSize: paginationState.pageSize, - }, - }); - - const { data } = response.data; - - const currentDataSource = ctx.model.getDataSource() || []; - ctx.model.setDataSource([...currentDataSource, ...data]); - - if (data.length < paginationState.pageSize) { - paginationState.hasMore = false; - } else { - paginationState.page++; - } - } catch (error) { - console.error('滚动分页请求失败:', error); - } finally { - paginationState.loading = false; - } - }, - }, - }, -}); - -AssociationSelectFieldModel.registerFlow({ - key: 'event3', - on: { - eventName: 'search', - }, - steps: { - step1: { - async handler(ctx, params) { - try { - const collectionField = ctx.model.collectionField; - const collectionManager = collectionField.collection.collectionManager; - const targetCollection = collectionManager.getCollection(collectionField.options.target); - - const labelFieldName = ctx.model.field.componentProps.fieldNames.label; - const targetLabelField = targetCollection.getField(labelFieldName); - - const targetInterface = ctx.app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface( - targetLabelField.options.interface, - ); - const operator = targetInterface?.filterable?.operators?.[0]?.value || '$includes'; - - const searchText = ctx.extra.searchText?.trim(); - - const apiClient = ctx.app.apiClient; - const response = await apiClient.request({ - url: `/${collectionField.options.target}:list`, - params: { - filter: { - [labelFieldName]: { - [operator]: searchText, - }, - }, - }, - }); - - const { data } = response.data; - ctx.model.setDataSource(data); - } catch (error) { - console.error('AssociationSelectField search flow error:', error); - // 出错时也可以选择清空数据源或者显示错误提示 - ctx.model.setDataSource([]); - } - }, - }, - }, -}); diff --git a/packages/core/client/src/flow/models/table/fields/PercentColumnModel.tsx b/packages/core/client/src/flow/models/table/fields/PercentColumnModel.tsx index 80613effdf..cb7e832585 100644 --- a/packages/core/client/src/flow/models/table/fields/PercentColumnModel.tsx +++ b/packages/core/client/src/flow/models/table/fields/PercentColumnModel.tsx @@ -8,16 +8,27 @@ */ import React from 'react'; +import { isNum } from '@formily/shared'; +import * as math from 'mathjs'; import { TableColumnModel } from '../../TableColumnModel'; import { InputNumberReadPretty } from '../components/InputNumberReadPretty'; +const isNumberLike = (index: any): index is number => isNum(index) || /^-?\d+(\.\d+)?$/.test(index); + +const toValue = (value: any, callback: (v: number) => number) => { + if (isNumberLike(value)) { + return math.round(callback(value), 9); + } + return null; +}; export class PercentColumnModel extends TableColumnModel { public static readonly supportedFieldInterfaces = ['percent']; render() { return (value, record, index) => { + const val = toValue(value, (v) => v * 100); return ( <> - + {this.renderQuickEditButton(record)} );