mirror of
https://github.com/nocobase/nocobase.git
synced 2026-09-19 02:23:00 +08:00
@@ -374,6 +374,11 @@
|
||||
"type": "custom-link",
|
||||
"label": "加密",
|
||||
"link": "/data-sources/field-encryption/"
|
||||
},
|
||||
{
|
||||
"type": "custom-link",
|
||||
"label": "中国行政区",
|
||||
"link": "/data-sources/data-modeling/collection-fields/advanced/china-region"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 中国行政区
|
||||
|
||||
## 介绍
|
||||
|
||||
## 字段配置
|
||||
|
||||

|
||||
|
||||
## 示例
|
||||
|
||||
待补充
|
||||
@@ -374,6 +374,11 @@
|
||||
"type": "custom-link",
|
||||
"label": "Encryption",
|
||||
"link": "/data-sources/field-encryption/"
|
||||
},
|
||||
{
|
||||
"type": "custom-link",
|
||||
"label": "China Region",
|
||||
"link": "/data-sources/data-modeling/collection-fields/advanced/china-region"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# China Region
|
||||
|
||||
## Introduction
|
||||
|
||||
## Field configuration
|
||||
|
||||

|
||||
|
||||
## Instructions
|
||||
|
||||
to be added.
|
||||
@@ -635,9 +635,12 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
||||
| undefined;
|
||||
if (Array.isArray(childrenDefs) && childrenDefs.length) {
|
||||
for (const c of childrenDefs) {
|
||||
// 翻译 filterable.children 的 title(支持 {{t("...")}} 模板)
|
||||
const rawTitle = c.title || c.name;
|
||||
const translatedTitle = model.context?.t ? model.context.t(rawTitle) : rawTitle;
|
||||
extraChildren.push({
|
||||
name: c.name,
|
||||
title: c.title || c.name,
|
||||
title: translatedTitle,
|
||||
type: (c.schema?.type as string) || 'string',
|
||||
// 为子项赋予一个可用的接口,以便拿到操作符(使用 input => string operators)
|
||||
interface: c.schema?.['x-component'] === 'Select' ? 'select' : 'input',
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { useChinaRegionDataSource, useChinaRegionLoadData } from './ChinaRegionProvider';
|
||||
import { ChinaRegionFieldInterface } from './chinaRegion';
|
||||
import { ChinaRegionFieldModel, DisplayChinaRegionFieldModel } from './models';
|
||||
|
||||
export class PluginFieldChinaRegionClient extends Plugin {
|
||||
async load() {
|
||||
@@ -18,6 +19,10 @@ export class PluginFieldChinaRegionClient extends Plugin {
|
||||
useChinaRegionLoadData,
|
||||
});
|
||||
this.app.dataSourceManager.addFieldInterfaces([ChinaRegionFieldInterface]);
|
||||
this.flowEngine.registerModels({
|
||||
ChinaRegionFieldModel,
|
||||
DisplayChinaRegionFieldModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Cascader as AntdCascader } from 'antd';
|
||||
import { isBoolean, omit } from 'lodash';
|
||||
import { FieldModel, useAPIClient, useRequest } from '@nocobase/client';
|
||||
import { EditableItemModel } from '@nocobase/flow-engine';
|
||||
|
||||
const ChinaRegionCascader: React.FC<any> = (props) => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
maxLevel = 3,
|
||||
changeOnSelectLast,
|
||||
labelInValue,
|
||||
fieldNames = {
|
||||
label: 'name',
|
||||
value: 'code',
|
||||
children: 'children',
|
||||
},
|
||||
multiple, // 过滤掉,不支持多选
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const api = useAPIClient();
|
||||
// Load initial provinces data
|
||||
const {
|
||||
data: initialData,
|
||||
loading,
|
||||
run,
|
||||
} = useRequest(
|
||||
{
|
||||
resource: 'chinaRegions',
|
||||
action: 'list',
|
||||
params: {
|
||||
sort: 'code',
|
||||
paginate: false,
|
||||
filter: {
|
||||
level: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Manage cascader options state
|
||||
const [options, setOptions] = React.useState<any[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if ((initialData as any)?.data) {
|
||||
const processed = (initialData as any).data.map((item) => ({
|
||||
...item,
|
||||
isLeaf: maxLevel === 1,
|
||||
}));
|
||||
setOptions(processed);
|
||||
}
|
||||
}, [initialData, maxLevel]);
|
||||
|
||||
// Load data on dropdown open
|
||||
const handleDropdownVisibleChange = React.useCallback(
|
||||
(visible: boolean) => {
|
||||
if (visible && options.length === 0) {
|
||||
run();
|
||||
}
|
||||
},
|
||||
[options.length, run],
|
||||
);
|
||||
|
||||
// Load children on expand
|
||||
const loadData = React.useCallback(
|
||||
(selectedOptions: any[]) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
if (targetOption?.children?.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetOption.loading = true;
|
||||
|
||||
api
|
||||
.resource('chinaRegions')
|
||||
.list({
|
||||
sort: 'code',
|
||||
paginate: false,
|
||||
filter: {
|
||||
parentCode: targetOption.code,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
targetOption.loading = false;
|
||||
targetOption.children =
|
||||
data?.data?.map((item) => ({
|
||||
...item,
|
||||
isLeaf: maxLevel <= item.level,
|
||||
})) || [];
|
||||
// Use functional update to avoid dependency on options
|
||||
setOptions((prevOptions) => [...prevOptions]);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
targetOption.loading = false;
|
||||
});
|
||||
},
|
||||
[api, maxLevel], // Removed options dependency
|
||||
);
|
||||
|
||||
// Convert value to array format for Cascader
|
||||
const toValue = React.useCallback(() => {
|
||||
if (!value) return undefined;
|
||||
const arr = Array.isArray(value) ? value : [value];
|
||||
return arr.map((item) => {
|
||||
if (typeof item === 'object') {
|
||||
return item[fieldNames.value];
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}, [value, fieldNames.value]);
|
||||
|
||||
// Custom display render to show labels from value when options not loaded
|
||||
const displayRender = React.useCallback(
|
||||
(labels: string[], selectedOptions: any[]) => {
|
||||
if (!value) return labels.join(' / ');
|
||||
|
||||
const valueArr = Array.isArray(value) ? value : [value];
|
||||
|
||||
// Sort value objects by level to match cascader hierarchy
|
||||
const sortedValues = valueArr
|
||||
.filter((v) => typeof v === 'object')
|
||||
.sort((a, b) => (a.level || 0) - (b.level || 0));
|
||||
|
||||
return labels
|
||||
.map((label, index) => {
|
||||
// If selectedOptions has the item, use it
|
||||
if (selectedOptions[index]) {
|
||||
return selectedOptions[index][fieldNames.label];
|
||||
}
|
||||
|
||||
// Otherwise, get from sorted value objects by index
|
||||
if (sortedValues[index] && sortedValues[index][fieldNames.label]) {
|
||||
return sortedValues[index][fieldNames.label];
|
||||
}
|
||||
|
||||
return label;
|
||||
})
|
||||
.join(' / ');
|
||||
},
|
||||
[value, fieldNames.label],
|
||||
);
|
||||
|
||||
// Handle onChange
|
||||
const handleChange = React.useCallback(
|
||||
(newValue: any, selectedOptions: any[]) => {
|
||||
if (newValue && labelInValue) {
|
||||
onChange?.(selectedOptions.map((option) => omit(option, [fieldNames.children])) || null);
|
||||
} else {
|
||||
onChange?.(newValue || null);
|
||||
}
|
||||
},
|
||||
[onChange, labelInValue, fieldNames.children],
|
||||
);
|
||||
|
||||
return (
|
||||
<AntdCascader
|
||||
{...restProps}
|
||||
loading={loading}
|
||||
options={options}
|
||||
value={toValue()}
|
||||
loadData={loadData}
|
||||
fieldNames={fieldNames}
|
||||
displayRender={displayRender}
|
||||
multiple={false}
|
||||
changeOnSelect={isBoolean(changeOnSelectLast) ? !changeOnSelectLast : restProps.changeOnSelect}
|
||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export class ChinaRegionFieldModel extends FieldModel {
|
||||
render() {
|
||||
return <ChinaRegionCascader {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
EditableItemModel.bindModelToInterface('ChinaRegionFieldModel', ['chinaRegion'], { isDefault: true });
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FieldModel } from '@nocobase/client';
|
||||
import { DisplayItemModel } from '@nocobase/flow-engine';
|
||||
|
||||
export class DisplayChinaRegionFieldModel extends FieldModel {
|
||||
render() {
|
||||
const { value } = this.props;
|
||||
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle array of region objects
|
||||
if (Array.isArray(value)) {
|
||||
const sorted = [...value].sort((a, b) => {
|
||||
if (a.level !== b.level) {
|
||||
return a.level - b.level;
|
||||
}
|
||||
return (a.sort || 0) - (b.sort || 0);
|
||||
});
|
||||
const names = sorted.map((item) => item.name || item.label || item).filter(Boolean);
|
||||
return <span>{names.join('/')}</span>;
|
||||
}
|
||||
|
||||
// Handle single value
|
||||
if (typeof value === 'object' && value.name) {
|
||||
return <span>{value.name}</span>;
|
||||
}
|
||||
|
||||
// Handle string value
|
||||
return <span>{String(value)}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
DisplayItemModel.bindModelToInterface('DisplayChinaRegionFieldModel', ['chinaRegion'], { isDefault: true });
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export * from './ChinaRegionFieldModel';
|
||||
export * from './DisplayChinaRegionFieldModel';
|
||||
Reference in New Issue
Block a user