mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
refactor: fix bug
This commit is contained in:
parent
8ce3b89193
commit
f52b24eacb
@ -14,7 +14,6 @@ import { FormFieldModel } from '../../../FormFieldModel';
|
|||||||
|
|
||||||
function toValue(record: any | any[], fieldNames, multiple = false) {
|
function toValue(record: any | any[], fieldNames, multiple = false) {
|
||||||
if (!record) return multiple ? [] : undefined;
|
if (!record) return multiple ? [] : undefined;
|
||||||
|
|
||||||
const { label: labelKey, value: valueKey } = fieldNames;
|
const { label: labelKey, value: valueKey } = fieldNames;
|
||||||
|
|
||||||
const convert = (item: any) => {
|
const convert = (item: any) => {
|
||||||
@ -50,29 +49,22 @@ function LazySelect(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AssociationSelect = connect(
|
export const AssociationSelect = connect(
|
||||||
(props: any) => {
|
(props: any) => <LazySelect {...props} />,
|
||||||
return <LazySelect {...props} />;
|
mapProps({ dataSource: 'options' }),
|
||||||
},
|
mapReadPretty((props) => props.value),
|
||||||
mapProps(
|
|
||||||
{
|
|
||||||
dataSource: 'options',
|
|
||||||
},
|
|
||||||
(props, field) => {
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
mapReadPretty((props) => {
|
|
||||||
return props.value;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export class AssociationSelectFieldModel extends FormFieldModel {
|
export class AssociationSelectFieldModel extends FormFieldModel {
|
||||||
static supportedFieldInterfaces = ['m2m', 'm2o', 'o2o', 'o2m', 'oho', 'obo', 'updatedBy', 'createdBy'];
|
static supportedFieldInterfaces = ['m2m', 'm2o', 'o2o', 'o2m', 'oho', 'obo', 'updatedBy', 'createdBy'];
|
||||||
dataSource;
|
dataSource;
|
||||||
fieldNames: { label: string; value: string; color?: string; icon?: any };
|
fieldNames: { label: string; value: string; color?: string; icon?: any };
|
||||||
|
optionsLoadState?: {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
loading: boolean;
|
||||||
|
hasMore: boolean;
|
||||||
|
searchText?: string;
|
||||||
|
};
|
||||||
set onPopupScroll(fn) {
|
set onPopupScroll(fn) {
|
||||||
this.field.setComponentProps({ 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({
|
AssociationSelectFieldModel.registerFlow({
|
||||||
key: 'init',
|
key: 'init',
|
||||||
auto: true,
|
auto: true,
|
||||||
sort: 100,
|
sort: 100,
|
||||||
steps: {
|
steps: {
|
||||||
step1: {
|
step1: {
|
||||||
handler(ctx, params) {
|
handler(ctx) {
|
||||||
let initialized = false;
|
|
||||||
ctx.model.onDropdownVisibleChange = (open) => {
|
ctx.model.onDropdownVisibleChange = (open) => {
|
||||||
if (open && !initialized) {
|
ctx.model.dispatchEvent('dropdownOpen', {
|
||||||
initialized = true;
|
apiClient: ctx.app.apiClient,
|
||||||
|
field: ctx.model.field,
|
||||||
ctx.model.dispatchEvent('dropdownOpen', {
|
form: ctx.model.form,
|
||||||
apiClient: ctx.app.apiClient,
|
});
|
||||||
field: ctx.model.field,
|
|
||||||
form: ctx.model.form,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.model.onPopupScroll = (e) => {
|
ctx.model.onPopupScroll = (e) => {
|
||||||
ctx.model.dispatchEvent('popupScroll', {
|
ctx.model.dispatchEvent('popupScroll', {
|
||||||
event: e,
|
event: e,
|
||||||
@ -119,6 +144,7 @@ AssociationSelectFieldModel.registerFlow({
|
|||||||
field: ctx.model.field,
|
field: ctx.model.field,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.model.onSearch = (searchText) => {
|
ctx.model.onSearch = (searchText) => {
|
||||||
ctx.model.dispatchEvent('search', {
|
ctx.model.dispatchEvent('search', {
|
||||||
searchText,
|
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) => {
|
const loadTitleFieldOptions = (collectionField, dataSourceManager) => {
|
||||||
return async (field) => {
|
return async (field) => {
|
||||||
const form = field.form;
|
const form = field.form;
|
||||||
@ -141,7 +248,9 @@ const loadTitleFieldOptions = (collectionField, dataSourceManager) => {
|
|||||||
|
|
||||||
const targetCollection = collectionManager.getCollection(target);
|
const targetCollection = collectionManager.getCollection(target);
|
||||||
const targetFields = targetCollection?.getFields?.() ?? [];
|
const targetFields = targetCollection?.getFields?.() ?? [];
|
||||||
|
|
||||||
field.loading = true;
|
field.loading = true;
|
||||||
|
|
||||||
const options = targetFields
|
const options = targetFields
|
||||||
.filter((field) => isTitleField(dataSourceManager, field.options))
|
.filter((field) => isTitleField(dataSourceManager, field.options))
|
||||||
.map((field) => ({
|
.map((field) => ({
|
||||||
@ -154,6 +263,7 @@ const loadTitleFieldOptions = (collectionField, dataSourceManager) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 标题字段设置
|
||||||
AssociationSelectFieldModel.registerFlow({
|
AssociationSelectFieldModel.registerFlow({
|
||||||
key: 'fieldNames',
|
key: 'fieldNames',
|
||||||
auto: true,
|
auto: true,
|
||||||
@ -165,152 +275,26 @@ AssociationSelectFieldModel.registerFlow({
|
|||||||
label: {
|
label: {
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-reactions': ['{{loadTitleFieldOptions(collectionField,dataSourceManager)}}'],
|
'x-reactions': ['{{loadTitleFieldOptions(collectionField, dataSourceManager)}}'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultParams: (ctx) => {
|
defaultParams: (ctx) => ({
|
||||||
const fieldNames = {
|
label: ctx.model.field.componentProps.fieldNames?.label,
|
||||||
...ctx.model.collectionField.options?.uiSchema?.['x-component-props']?.['fieldNames'],
|
}),
|
||||||
...ctx.model.field.componentProps.fieldNames,
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
label: fieldNames.label,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
handler(ctx, params) {
|
handler(ctx, params) {
|
||||||
ctx.model.flowEngine.flowSettings.registerScopes({
|
ctx.model.flowEngine.flowSettings.registerScopes({
|
||||||
loadTitleFieldOptions,
|
loadTitleFieldOptions,
|
||||||
collectionField: ctx.model.collectionField,
|
collectionField: ctx.model.collectionField,
|
||||||
dataSourceManager: ctx.app.dataSourceManager,
|
dataSourceManager: ctx.app.dataSourceManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newFieldNames = {
|
const newFieldNames = {
|
||||||
...ctx.model.collectionField.options?.uiSchema?.['x-component-props']?.['fieldNames'],
|
|
||||||
...ctx.model.field.componentProps.fieldNames,
|
...ctx.model.field.componentProps.fieldNames,
|
||||||
label: params.label,
|
label: params.label,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.model.field.setComponentProps({ fieldNames: newFieldNames });
|
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([]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
@ -8,16 +8,27 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { isNum } from '@formily/shared';
|
||||||
|
import * as math from 'mathjs';
|
||||||
import { TableColumnModel } from '../../TableColumnModel';
|
import { TableColumnModel } from '../../TableColumnModel';
|
||||||
import { InputNumberReadPretty } from '../components/InputNumberReadPretty';
|
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 {
|
export class PercentColumnModel extends TableColumnModel {
|
||||||
public static readonly supportedFieldInterfaces = ['percent'];
|
public static readonly supportedFieldInterfaces = ['percent'];
|
||||||
render() {
|
render() {
|
||||||
return (value, record, index) => {
|
return (value, record, index) => {
|
||||||
|
const val = toValue(value, (v) => v * 100);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InputNumberReadPretty value={value} />
|
<InputNumberReadPretty value={val} addonAfter="%" />
|
||||||
{this.renderQuickEditButton(record)}
|
{this.renderQuickEditButton(record)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user