feat: region-v2 (#8831)

* feat: region-v2

* fix: add doc
This commit is contained in:
Jiann
2026-03-23 16:25:49 +08:00
committed by GitHub
parent 1be40eb45c
commit 0fcb39cd32
9 changed files with 289 additions and 1 deletions
+5
View File
@@ -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 @@
# 中国行政区
## 介绍
## 字段配置
![20240512180305](https://static-docs.nocobase.com/20240512180305.png)
## 示例
待补充
+5
View File
@@ -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
![20240512180305](https://static-docs.nocobase.com/20240512180305.png)
## 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,
});
}
}
@@ -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 });
@@ -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';