Feature: plugin-china-region (#66)
* feat: add core function for china region cascade select * fix: test case * refactor: use belongsToMany for region field and fix component * fix: data import * fix: remove sort field * fix: clear on changeOnSelect is false
This commit is contained in:
parent
ce5690e25c
commit
662619b91c
@ -30,6 +30,7 @@
|
|||||||
"@nocobase/database": "^0.3.0-alpha.0",
|
"@nocobase/database": "^0.3.0-alpha.0",
|
||||||
"@nocobase/father-build": "^0.3.0-alpha.0",
|
"@nocobase/father-build": "^0.3.0-alpha.0",
|
||||||
"@nocobase/plugin-action-logs": "^0.3.0-alpha.0",
|
"@nocobase/plugin-action-logs": "^0.3.0-alpha.0",
|
||||||
|
"@nocobase/plugin-china-region": "^0.3.0-alpha.0",
|
||||||
"@nocobase/plugin-collections": "^0.3.0-alpha.0",
|
"@nocobase/plugin-collections": "^0.3.0-alpha.0",
|
||||||
"@nocobase/plugin-pages": "^0.3.0-alpha.0",
|
"@nocobase/plugin-pages": "^0.3.0-alpha.0",
|
||||||
"@nocobase/server": "^0.3.0-alpha.0",
|
"@nocobase/server": "^0.3.0-alpha.0",
|
||||||
|
@ -68,5 +68,6 @@ api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-use
|
|||||||
api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plugin-file-manager'), {}]);
|
api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plugin-file-manager'), {}]);
|
||||||
api.registerPlugin('plugin-permissions', [path.resolve(__dirname, '../../../plugin-permissions'), {}]);
|
api.registerPlugin('plugin-permissions', [path.resolve(__dirname, '../../../plugin-permissions'), {}]);
|
||||||
api.registerPlugin('plugin-automations', [path.resolve(__dirname, '../../../plugin-automations'), {}]);
|
api.registerPlugin('plugin-automations', [path.resolve(__dirname, '../../../plugin-automations'), {}]);
|
||||||
|
api.registerPlugin('plugin-china-region', [path.resolve(__dirname, '../../../plugin-china-region'), {}]);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
13
packages/app/src/api/migrations/add-china-region-data.ts
Normal file
13
packages/app/src/api/migrations/add-china-region-data.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { init } from '@nocobase/plugin-china-region/src/db/seeders';
|
||||||
|
import api from '../app';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await api.loadPlugins();
|
||||||
|
await api.database.sync();
|
||||||
|
await init(api);
|
||||||
|
})().then(() => {
|
||||||
|
process.exit(0);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
@ -6,8 +6,9 @@ global.sync = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
import api from '../app';
|
|
||||||
import Database from '@nocobase/database';
|
import Database from '@nocobase/database';
|
||||||
|
import { init as chinaRegionSeederInit } from '@nocobase/plugin-china-region/src/db/seeders';
|
||||||
|
import api from '../app';
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
@ -234,6 +235,9 @@ const data = [
|
|||||||
await Action.bulkCreate([
|
await Action.bulkCreate([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 导入地域数据
|
||||||
|
await chinaRegionSeederInit(api);
|
||||||
|
|
||||||
await database.getModel('collections').import(require('./collections/example').default);
|
await database.getModel('collections').import(require('./collections/example').default);
|
||||||
await database.getModel('collections').import(require('./collections/authors').default);
|
await database.getModel('collections').import(require('./collections/authors').default);
|
||||||
await database.getModel('collections').import(require('./collections/books').default);
|
await database.getModel('collections').import(require('./collections/books').default);
|
||||||
|
125
packages/app/src/components/form.fields/cascader/index.tsx
Normal file
125
packages/app/src/components/form.fields/cascader/index.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { connect } from '@formily/react-schema-renderer'
|
||||||
|
import { Cascader as AntdCascader } from 'antd'
|
||||||
|
import { useRequest } from 'umi';
|
||||||
|
import api from '@/api-client';
|
||||||
|
import {
|
||||||
|
transformDataSourceKey,
|
||||||
|
mapStyledProps,
|
||||||
|
mapTextComponent
|
||||||
|
} from '../shared'
|
||||||
|
|
||||||
|
function findTreeNode(tree, values, { value = 'value', children = 'children' }) {
|
||||||
|
let node, i;
|
||||||
|
for (node = tree, i = 0; node && values[i]; i++ ) {
|
||||||
|
node = (node[children] || []).find(item => item[value] === values[i][value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Cascader = connect({
|
||||||
|
getProps: mapStyledProps,
|
||||||
|
getComponent: mapTextComponent,
|
||||||
|
})(function (props) {
|
||||||
|
const {
|
||||||
|
disabled,
|
||||||
|
target,
|
||||||
|
labelField,
|
||||||
|
valueField = 'id',
|
||||||
|
parentField,
|
||||||
|
maxLevel,
|
||||||
|
changeOnSelect,
|
||||||
|
value = [],
|
||||||
|
onChange,
|
||||||
|
// TODO(feature): 增加静态数据支持
|
||||||
|
// dataSource: []
|
||||||
|
} = props;
|
||||||
|
const fieldNames = {
|
||||||
|
label: labelField,
|
||||||
|
value: valueField,
|
||||||
|
children: 'children'
|
||||||
|
};
|
||||||
|
const [options, setOptions] = useState([]);
|
||||||
|
|
||||||
|
const { loading, run } = useRequest(async (selectedOptions = []) => {
|
||||||
|
if (maxLevel != null && selectedOptions.length >= maxLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = selectedOptions[selectedOptions.length - 1] || null;
|
||||||
|
if (last) {
|
||||||
|
if (last.isLeaf) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
last.loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.resource(target).list({
|
||||||
|
filter: {
|
||||||
|
[parentField]: last && last[valueField]
|
||||||
|
},
|
||||||
|
perPage: -1,
|
||||||
|
sort: [valueField]
|
||||||
|
});
|
||||||
|
// TODO(bug): 关联资源加载问题较多,暂时先用 filter 解决
|
||||||
|
// return api.resource(`${target}.${target}`).list({
|
||||||
|
// associatedKey: last,
|
||||||
|
// perPage: -1
|
||||||
|
// });
|
||||||
|
}, {
|
||||||
|
manual: true,
|
||||||
|
onSuccess(result, [selectedOptions = []]) {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = result.map(item => ({
|
||||||
|
...item,
|
||||||
|
isLeaf: maxLevel != null && item.level >= maxLevel
|
||||||
|
}));
|
||||||
|
// 找到已有值指向的 options 节点
|
||||||
|
const root = { [fieldNames.children]: options };
|
||||||
|
const node = findTreeNode(root, selectedOptions, fieldNames);
|
||||||
|
|
||||||
|
if (node && node !== root) {
|
||||||
|
node.children = data;
|
||||||
|
node.loading = false;
|
||||||
|
// use spread array to avoid popup to be collapsed
|
||||||
|
setOptions([...options]);
|
||||||
|
} else {
|
||||||
|
setOptions(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据 value 的值,按需预加载相应的数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (value.length) {
|
||||||
|
value.reduce((promise, option, i) => promise.then(() => run(value.slice(0, i))), Promise.resolve());
|
||||||
|
} else {
|
||||||
|
run([]);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AntdCascader
|
||||||
|
disabled={disabled}
|
||||||
|
options={options}
|
||||||
|
value={value.map(item => item[valueField])}
|
||||||
|
onChange={(v, selected) => {
|
||||||
|
if (maxLevel != null && v.length < maxLevel) {
|
||||||
|
run(selected);
|
||||||
|
}
|
||||||
|
if (changeOnSelect || !v.length || v.length >= maxLevel) {
|
||||||
|
onChange(selected);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
loadData={run}
|
||||||
|
changeOnSelect={changeOnSelect}
|
||||||
|
fieldNames={fieldNames}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Cascader
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/cascader/style/index'
|
@ -17,6 +17,7 @@ import { Filter } from './filter'
|
|||||||
import { RemoteSelect } from './remote-select'
|
import { RemoteSelect } from './remote-select'
|
||||||
import { DrawerSelect } from './drawer-select'
|
import { DrawerSelect } from './drawer-select'
|
||||||
import { SubTable } from './sub-table'
|
import { SubTable } from './sub-table'
|
||||||
|
import { Cascader } from './cascader'
|
||||||
import { Icon } from './icons'
|
import { Icon } from './icons'
|
||||||
import { ColorSelect } from './color-select'
|
import { ColorSelect } from './color-select'
|
||||||
import { Permissions } from './permissions'
|
import { Permissions } from './permissions'
|
||||||
@ -29,6 +30,7 @@ export const setup = () => {
|
|||||||
time: TimePicker,
|
time: TimePicker,
|
||||||
timerange: TimePicker.RangePicker,
|
timerange: TimePicker.RangePicker,
|
||||||
transfer: Transfer,
|
transfer: Transfer,
|
||||||
|
cascader: Cascader,
|
||||||
boolean: Checkbox,
|
boolean: Checkbox,
|
||||||
switch: Switch,
|
switch: Switch,
|
||||||
checkbox: Checkbox,
|
checkbox: Checkbox,
|
||||||
|
@ -44,12 +44,12 @@ const createEnum = (enums: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Select: React.FC<SelectProps> = styled((props: SelectProps) => {
|
export const Select: React.FC<SelectProps> = styled((props: SelectProps) => {
|
||||||
const { dataSource = [], onChange, value, ...others } = props
|
const { dataSource = [], onChange, value, ...others } = props;
|
||||||
const children = createEnum(dataSource).map(item => {
|
const children = createEnum(dataSource).map(item => {
|
||||||
const { label, value, children = [], ...others } = item
|
const { label, value, key, children = [], ...others } = item;
|
||||||
if (children.length) {
|
if (children.length) {
|
||||||
return (
|
return (
|
||||||
<AntSelect.OptGroup label={label}>
|
<AntSelect.OptGroup key={key} label={label}>
|
||||||
{children.map(({value, label, ...others}: any) => (
|
{children.map(({value, label, ...others}: any) => (
|
||||||
<AntSelect.Option
|
<AntSelect.Option
|
||||||
key={value}
|
key={value}
|
||||||
|
@ -256,13 +256,13 @@ export function LinkToFieldLink(props) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}}>{data[labelField]}</a>
|
}}>{data[labelField]}</a>
|
||||||
<Drawer
|
<Drawer
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className={'noco-drawer'}
|
className={'noco-drawer'}
|
||||||
bodyStyle={{padding: 0}}
|
bodyStyle={{padding: 0}}
|
||||||
width={'40%'}
|
width={'40%'}
|
||||||
title={`查看${title}详情`}
|
title={`查看${title}详情`}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@ -361,20 +361,34 @@ export function AttachmentFieldItem(props: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LogField(props) {
|
export function LogField(props) {
|
||||||
const { value = {} } = props;
|
const { value = {} } = props;
|
||||||
return (
|
return (
|
||||||
<div>{value.title||value.name}</div>
|
<div>{value.title||value.name}</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LogFieldValue(props) {
|
export function LogFieldValue(props) {
|
||||||
const { value, schema, data } = props;
|
const { value, schema, data } = props;
|
||||||
return (
|
return (
|
||||||
<div>{JSON.stringify(value)}</div>
|
<div>{JSON.stringify(value)}</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ChinaRegion(props: any) {
|
||||||
|
const { schema, value } = props;
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const values = (Array.isArray(value) ? value : [value])
|
||||||
|
.sort((a, b) => a.level !== b.level ? a.level - b.level : a.sort - b.sort);
|
||||||
|
return (
|
||||||
|
<div className={'china-region-field'}>
|
||||||
|
{values.map(item => item.name).join('/')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
registerFieldComponents({
|
registerFieldComponents({
|
||||||
string: StringField,
|
string: StringField,
|
||||||
textarea: TextareaField,
|
textarea: TextareaField,
|
||||||
@ -396,6 +410,7 @@ registerFieldComponents({
|
|||||||
attachment: AttachmentField,
|
attachment: AttachmentField,
|
||||||
'logs.field': LogField,
|
'logs.field': LogField,
|
||||||
'logs.fieldValue': LogFieldValue,
|
'logs.fieldValue': LogFieldValue,
|
||||||
|
chinaRegion: ChinaRegion,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Field(props: any) {
|
export default function Field(props: any) {
|
||||||
|
10
packages/plugin-china-region/package.json
Normal file
10
packages/plugin-china-region/package.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@nocobase/plugin-china-region",
|
||||||
|
"version": "0.3.0-alpha.0",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nocobase/database": "^0.3.0-alpha.0",
|
||||||
|
"china-division": "2.3.1"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import { TableOptions } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'china_regions',
|
||||||
|
title: '中国行政区划',
|
||||||
|
internal: true,
|
||||||
|
developerMode: true,
|
||||||
|
fields: [
|
||||||
|
// 如使用代码作为 id 可能更节省,但由于代码数字最长为 12 字节,除非使用 bigint(64) 才够放置
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
title: '代码',
|
||||||
|
interface: 'string',
|
||||||
|
type: 'string',
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
title: '名称',
|
||||||
|
interface: 'string',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parent',
|
||||||
|
title: '从属',
|
||||||
|
interface: 'linkTo',
|
||||||
|
type: 'belongsTo',
|
||||||
|
target: 'china_regions',
|
||||||
|
targetKey: 'code',
|
||||||
|
foreignKey: 'parent_code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'children',
|
||||||
|
title: '下辖',
|
||||||
|
interface: 'linkTo',
|
||||||
|
type: 'hasMany',
|
||||||
|
target: 'china_regions',
|
||||||
|
sourceKey: 'code',
|
||||||
|
foreignKey: 'parent_code'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'level',
|
||||||
|
title: '层级',
|
||||||
|
type: 'integer'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as TableOptions;
|
1
packages/plugin-china-region/src/db/seeders/index.ts
Normal file
1
packages/plugin-china-region/src/db/seeders/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as init } from './init';
|
47
packages/plugin-china-region/src/db/seeders/init.ts
Normal file
47
packages/plugin-china-region/src/db/seeders/init.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { provinces, cities, areas, streets, villages } from 'china-division';
|
||||||
|
|
||||||
|
export default async function({ database }) {
|
||||||
|
const Model = database.getModel('china_regions');
|
||||||
|
// TODO(feature): 可以考虑再加一级大区,编码规则应该是支持的
|
||||||
|
// 华北、东北、华东、华南、西南、西北、华中
|
||||||
|
const timer = Date.now();
|
||||||
|
|
||||||
|
await Model.bulkCreate(provinces.map(item => ({
|
||||||
|
code: item.code,
|
||||||
|
name: item.name,
|
||||||
|
level: 1
|
||||||
|
})));
|
||||||
|
|
||||||
|
await Model.bulkCreate(cities.map(item => ({
|
||||||
|
code: item.code,
|
||||||
|
name: item.name,
|
||||||
|
level: 2,
|
||||||
|
parent_code: item.provinceCode
|
||||||
|
})));
|
||||||
|
|
||||||
|
await Model.bulkCreate(areas.map(item => ({
|
||||||
|
code: item.code,
|
||||||
|
name: item.name,
|
||||||
|
level: 3,
|
||||||
|
parent_code: item.cityCode
|
||||||
|
})));
|
||||||
|
|
||||||
|
// 乡级数据 2856 条
|
||||||
|
await Model.bulkCreate(streets.map(item => ({
|
||||||
|
code: item.code,
|
||||||
|
name: item.name,
|
||||||
|
level: 4,
|
||||||
|
parent_code: item.areaCode
|
||||||
|
})));
|
||||||
|
|
||||||
|
// 村级数据 658001 条
|
||||||
|
await Model.bulkCreate(villages.map(item => ({
|
||||||
|
code: item.code,
|
||||||
|
name: item.name,
|
||||||
|
level: 5,
|
||||||
|
parent_code: item.streetCode
|
||||||
|
})));
|
||||||
|
|
||||||
|
const count = await Model.count();
|
||||||
|
console.log(`${count} rows of region data imported in ${(Date.now() - timer) / 1000}s`);
|
||||||
|
};
|
18
packages/plugin-china-region/src/server.ts
Normal file
18
packages/plugin-china-region/src/server.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import path from 'path';
|
||||||
|
// import actions from '@nocobase/actions';
|
||||||
|
|
||||||
|
// 不从主入口导出,考虑到加载的数据量太大比较占内存,只在迁移时处理
|
||||||
|
// export * as seeders from './db/seeders';
|
||||||
|
|
||||||
|
export default async function (options = {}) {
|
||||||
|
const { database, resourcer } = this;
|
||||||
|
|
||||||
|
database.import({
|
||||||
|
directory: path.resolve(__dirname, 'collections'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// resourcer.define({
|
||||||
|
// name: 'china_regions.china_regions',
|
||||||
|
// actions: actions.common
|
||||||
|
// });
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import { Agent, getAgent, getApp } from '../';
|
import { Agent, getAgent, getApp } from '../';
|
||||||
import { Application } from '@nocobase/server';
|
import { Application } from '@nocobase/server';
|
||||||
import { options, types } from '../../interfaces';
|
import { types } from '../../interfaces';
|
||||||
import { FieldModel } from '../../models';
|
|
||||||
|
|
||||||
describe('models.field', () => {
|
describe('models.field', () => {
|
||||||
let app: Application;
|
let app: Application;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { TableOptions } from '@nocobase/database';
|
import { TableOptions } from '@nocobase/database';
|
||||||
import { options } from '../interfaces';
|
import { types, getOptions } from '../interfaces';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'fields',
|
name: 'fields',
|
||||||
@ -57,7 +57,7 @@ export default {
|
|||||||
name: 'interface',
|
name: 'interface',
|
||||||
title: '字段类型',
|
title: '字段类型',
|
||||||
required: true,
|
required: true,
|
||||||
dataSource: options,
|
dataSource: getOptions(),
|
||||||
createOnly: true,
|
createOnly: true,
|
||||||
component: {
|
component: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@ -65,6 +65,17 @@ export default {
|
|||||||
showInDetail: true,
|
showInDetail: true,
|
||||||
showInForm: true,
|
showInForm: true,
|
||||||
"x-linkages": [
|
"x-linkages": [
|
||||||
|
// TODO(draft): 统一解决字段类型和配置参数联动的一种方式
|
||||||
|
// {
|
||||||
|
// type: 'value:schema',
|
||||||
|
// target: 'options',
|
||||||
|
// schema: {
|
||||||
|
// 'x-component-props': {
|
||||||
|
// fields: '{{ $self.values[1].fields || [] }}'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// condition: '{{ !!$self.value }}'
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
"type": "value:visible",
|
"type": "value:visible",
|
||||||
"target": "precision",
|
"target": "precision",
|
||||||
@ -130,9 +141,31 @@ export default {
|
|||||||
"target": "required",
|
"target": "required",
|
||||||
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
|
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "value:visible",
|
||||||
|
"target": "maxLevel",
|
||||||
|
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "value:visible",
|
||||||
|
"target": "incompletely",
|
||||||
|
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO(draft): 将 options 作为集合字段开放出来,可以动态的解决字段参数的配置表单联动问题
|
||||||
|
// {
|
||||||
|
// interface: 'json',
|
||||||
|
// type: 'json',
|
||||||
|
// name: 'options',
|
||||||
|
// title: '配置信息',
|
||||||
|
// defaultValue: {},
|
||||||
|
// component: {
|
||||||
|
// type: 'subFields',
|
||||||
|
// showInForm: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
interface: 'subTable',
|
interface: 'subTable',
|
||||||
type: 'virtual',
|
type: 'virtual',
|
||||||
@ -250,6 +283,33 @@ export default {
|
|||||||
default: 'HH:mm:ss',
|
default: 'HH:mm:ss',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO(refactor): 此部分类型相关的参数,后期应拆分出去
|
||||||
|
{
|
||||||
|
name: 'maxLevel',
|
||||||
|
title: '可选层级',
|
||||||
|
interface: 'radio',
|
||||||
|
type: 'virtual',
|
||||||
|
dataSource: [
|
||||||
|
{ value: 1, label: '省' },
|
||||||
|
{ value: 2, label: '市' },
|
||||||
|
{ value: 3, label: '区/县' },
|
||||||
|
{ value: 4, label: '乡镇/街道' },
|
||||||
|
{ value: 5, label: '村/居委会' },
|
||||||
|
],
|
||||||
|
component: {
|
||||||
|
showInForm: true,
|
||||||
|
default: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'incompletely',
|
||||||
|
title: '可部分选择',
|
||||||
|
interface: 'boolean',
|
||||||
|
type: 'virtual',
|
||||||
|
component: {
|
||||||
|
showInForm: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
interface: 'linkTo',
|
interface: 'linkTo',
|
||||||
multiple: false,
|
multiple: false,
|
||||||
|
@ -4,90 +4,44 @@
|
|||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
export * as types from './types';
|
export * as types from './types';
|
||||||
|
|
||||||
export const options = [
|
export const groupLabelMap = {
|
||||||
{
|
basic: '基本类型',
|
||||||
key: 'basic',
|
media: '多媒体类型',
|
||||||
title: '基本类型',
|
choices: '选择类型',
|
||||||
children: [
|
datetime: '日期和时间',
|
||||||
types.string,
|
relation: '关系类型',
|
||||||
types.textarea,
|
systemInfo: '关系类型',
|
||||||
types.phone,
|
developerMode: '开发者模式',
|
||||||
types.email,
|
others: '其他'
|
||||||
types.number,
|
};
|
||||||
types.percent,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'media',
|
|
||||||
title: '多媒体类型',
|
|
||||||
children: [
|
|
||||||
types.wysiwyg,
|
|
||||||
types.attachment,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'choices',
|
|
||||||
title: '选择类型',
|
|
||||||
children: [
|
|
||||||
types.boolean,
|
|
||||||
types.select,
|
|
||||||
types.multipleSelect,
|
|
||||||
types.radio,
|
|
||||||
types.checkboxes,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'datetime',
|
|
||||||
title: '日期和时间',
|
|
||||||
children: [
|
|
||||||
types.datetime,
|
|
||||||
types.time,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'relation',
|
|
||||||
title: '关系类型',
|
|
||||||
children: [
|
|
||||||
types.subTable,
|
|
||||||
types.linkTo,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'systemInfo',
|
|
||||||
title: '系统信息',
|
|
||||||
children: [
|
|
||||||
types.createdAt,
|
|
||||||
types.updatedAt,
|
|
||||||
types.createdBy,
|
|
||||||
types.updatedBy,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'developerMode',
|
|
||||||
title: '开发者模式',
|
|
||||||
children: [
|
|
||||||
types.primaryKey,
|
|
||||||
types.sort,
|
|
||||||
types.password,
|
|
||||||
types.json,
|
|
||||||
types.icon,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'others',
|
|
||||||
title: '其他',
|
|
||||||
children: [
|
|
||||||
types.description,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
].map(({key, title, children}: any) => ({
|
|
||||||
key,
|
|
||||||
label: title,
|
|
||||||
children: children.map(child => ({
|
|
||||||
label: child.title,
|
|
||||||
value: child.options.interface,
|
|
||||||
disabled: child.disabled,
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default options;
|
export function getOptions() {
|
||||||
|
return Object.keys(groupLabelMap).map(key => ({
|
||||||
|
key,
|
||||||
|
label: groupLabelMap[key],
|
||||||
|
children: Object.values(types)
|
||||||
|
.filter(type => type['group'] === key)
|
||||||
|
.map(type => ({
|
||||||
|
label: type.title,
|
||||||
|
value: type.options.interface,
|
||||||
|
// TODO(draft): 配置信息一并存到数据库方便字段配置时取出参与联动计算
|
||||||
|
// properties: type.properties,
|
||||||
|
disabled: type['disabled'],
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type interfaceType = {
|
||||||
|
title: string,
|
||||||
|
group?: string,
|
||||||
|
options: {
|
||||||
|
[key: string]: any
|
||||||
|
},
|
||||||
|
disabled?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(draft)
|
||||||
|
// 目前仅在内存中注册,应用启动时需要解决扩展字段读取并注册到内存
|
||||||
|
export function register(type: interfaceType) {
|
||||||
|
types[type.options.interface] = type;
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
// TODO: interface 的修改
|
// TODO: interface 的修改
|
||||||
export const string = {
|
export const string = {
|
||||||
title: '单行文本',
|
title: '单行文本',
|
||||||
|
group: 'basic',
|
||||||
options: {
|
options: {
|
||||||
interface: 'string',
|
interface: 'string',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -19,6 +20,7 @@ export const string = {
|
|||||||
|
|
||||||
export const textarea = {
|
export const textarea = {
|
||||||
title: '多行文本',
|
title: '多行文本',
|
||||||
|
group: 'basic',
|
||||||
options: {
|
options: {
|
||||||
interface: 'textarea',
|
interface: 'textarea',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@ -31,6 +33,7 @@ export const textarea = {
|
|||||||
|
|
||||||
export const phone = {
|
export const phone = {
|
||||||
title: '手机号码',
|
title: '手机号码',
|
||||||
|
group: 'basic',
|
||||||
options: {
|
options: {
|
||||||
interface: 'phone',
|
interface: 'phone',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -45,6 +48,7 @@ export const phone = {
|
|||||||
|
|
||||||
export const email = {
|
export const email = {
|
||||||
title: '邮箱',
|
title: '邮箱',
|
||||||
|
group: 'basic',
|
||||||
options: {
|
options: {
|
||||||
interface: 'email',
|
interface: 'email',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -62,6 +66,7 @@ export const email = {
|
|||||||
*/
|
*/
|
||||||
export const number = {
|
export const number = {
|
||||||
title: '数字',
|
title: '数字',
|
||||||
|
group: 'basic',
|
||||||
options: {
|
options: {
|
||||||
interface: 'number',
|
interface: 'number',
|
||||||
type: 'float',
|
type: 'float',
|
||||||
@ -80,6 +85,7 @@ export const number = {
|
|||||||
*/
|
*/
|
||||||
export const percent = {
|
export const percent = {
|
||||||
title: '百分比',
|
title: '百分比',
|
||||||
|
group: 'basic',
|
||||||
options: {
|
options: {
|
||||||
interface: 'percent',
|
interface: 'percent',
|
||||||
type: 'float',
|
type: 'float',
|
||||||
@ -94,6 +100,7 @@ export const percent = {
|
|||||||
|
|
||||||
export const wysiwyg = {
|
export const wysiwyg = {
|
||||||
title: '可视化编辑器',
|
title: '可视化编辑器',
|
||||||
|
group: 'media',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
options: {
|
options: {
|
||||||
interface: 'wysiwyg',
|
interface: 'wysiwyg',
|
||||||
@ -109,6 +116,7 @@ export const wysiwyg = {
|
|||||||
*/
|
*/
|
||||||
export const attachment = {
|
export const attachment = {
|
||||||
title: '附件',
|
title: '附件',
|
||||||
|
group: 'media',
|
||||||
// disabled: true,
|
// disabled: true,
|
||||||
options: {
|
options: {
|
||||||
interface: 'attachment',
|
interface: 'attachment',
|
||||||
@ -129,6 +137,7 @@ export const attachment = {
|
|||||||
*/
|
*/
|
||||||
export const select = {
|
export const select = {
|
||||||
title: '下拉选择(单选)',
|
title: '下拉选择(单选)',
|
||||||
|
group: 'choices',
|
||||||
options: {
|
options: {
|
||||||
interface: 'select',
|
interface: 'select',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -152,6 +161,7 @@ export const select = {
|
|||||||
*/
|
*/
|
||||||
export const multipleSelect = {
|
export const multipleSelect = {
|
||||||
title: '下拉选择(多选)',
|
title: '下拉选择(多选)',
|
||||||
|
group: 'choices',
|
||||||
options: {
|
options: {
|
||||||
interface: 'multipleSelect',
|
interface: 'multipleSelect',
|
||||||
type: 'json', // json 过滤
|
type: 'json', // json 过滤
|
||||||
@ -167,6 +177,7 @@ export const multipleSelect = {
|
|||||||
|
|
||||||
export const radio = {
|
export const radio = {
|
||||||
title: '单选框',
|
title: '单选框',
|
||||||
|
group: 'choices',
|
||||||
options: {
|
options: {
|
||||||
interface: 'radio',
|
interface: 'radio',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -180,6 +191,7 @@ export const radio = {
|
|||||||
|
|
||||||
export const checkboxes = {
|
export const checkboxes = {
|
||||||
title: '多选框',
|
title: '多选框',
|
||||||
|
group: 'choices',
|
||||||
options: {
|
options: {
|
||||||
interface: 'checkboxes',
|
interface: 'checkboxes',
|
||||||
type: 'json',
|
type: 'json',
|
||||||
@ -194,6 +206,7 @@ export const checkboxes = {
|
|||||||
|
|
||||||
export const boolean = {
|
export const boolean = {
|
||||||
title: '是/否',
|
title: '是/否',
|
||||||
|
group: 'choices',
|
||||||
options: {
|
options: {
|
||||||
interface: 'boolean',
|
interface: 'boolean',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -210,6 +223,7 @@ export const boolean = {
|
|||||||
*/
|
*/
|
||||||
export const datetime = {
|
export const datetime = {
|
||||||
title: '日期',
|
title: '日期',
|
||||||
|
group: 'datetime',
|
||||||
options: {
|
options: {
|
||||||
interface: 'datetime',
|
interface: 'datetime',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
@ -222,10 +236,34 @@ export const datetime = {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO(draft): 配置参数的一种描述方式
|
||||||
|
// properties: [
|
||||||
|
// {
|
||||||
|
// name: 'showTime',
|
||||||
|
// title: '显示时间',
|
||||||
|
// interface: 'boolean',
|
||||||
|
// component: {
|
||||||
|
// 'x-linkages': [
|
||||||
|
// {
|
||||||
|
// type: 'value:visible',
|
||||||
|
// target: 'timeFormat',
|
||||||
|
// condition: '{{ $value }}'
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// interface: 'string',
|
||||||
|
// name: 'timeFormat',
|
||||||
|
// title: '时间格式',
|
||||||
|
// filterable: false
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const time = {
|
export const time = {
|
||||||
title: '时间',
|
title: '时间',
|
||||||
|
group: 'datetime',
|
||||||
options: {
|
options: {
|
||||||
interface: 'time',
|
interface: 'time',
|
||||||
type: 'time',
|
type: 'time',
|
||||||
@ -275,6 +313,7 @@ export const time = {
|
|||||||
// });
|
// });
|
||||||
export const subTable = {
|
export const subTable = {
|
||||||
title: '子表格',
|
title: '子表格',
|
||||||
|
group: 'relation',
|
||||||
// disabled: true,
|
// disabled: true,
|
||||||
options: {
|
options: {
|
||||||
interface: 'subTable',
|
interface: 'subTable',
|
||||||
@ -329,6 +368,7 @@ export const subTable = {
|
|||||||
|
|
||||||
export const linkTo = {
|
export const linkTo = {
|
||||||
title: '关联数据',
|
title: '关联数据',
|
||||||
|
group: 'relation',
|
||||||
// disabled: true,
|
// disabled: true,
|
||||||
options: {
|
options: {
|
||||||
interface: 'linkTo',
|
interface: 'linkTo',
|
||||||
@ -346,6 +386,7 @@ export const linkTo = {
|
|||||||
|
|
||||||
export const createdAt = {
|
export const createdAt = {
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
|
group: 'systemInfo',
|
||||||
options: {
|
options: {
|
||||||
interface: 'createdAt',
|
interface: 'createdAt',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
@ -365,6 +406,7 @@ export const createdAt = {
|
|||||||
|
|
||||||
export const updatedAt = {
|
export const updatedAt = {
|
||||||
title: '修改时间',
|
title: '修改时间',
|
||||||
|
group: 'systemInfo',
|
||||||
options: {
|
options: {
|
||||||
interface: 'updatedAt',
|
interface: 'updatedAt',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
@ -384,6 +426,7 @@ export const updatedAt = {
|
|||||||
|
|
||||||
export const createdBy = {
|
export const createdBy = {
|
||||||
title: '创建人',
|
title: '创建人',
|
||||||
|
group: 'systemInfo',
|
||||||
// disabled: true,
|
// disabled: true,
|
||||||
options: {
|
options: {
|
||||||
interface: 'createdBy',
|
interface: 'createdBy',
|
||||||
@ -402,6 +445,7 @@ export const createdBy = {
|
|||||||
|
|
||||||
export const updatedBy = {
|
export const updatedBy = {
|
||||||
title: '修改人',
|
title: '修改人',
|
||||||
|
group: 'systemInfo',
|
||||||
// disabled: true,
|
// disabled: true,
|
||||||
options: {
|
options: {
|
||||||
interface: 'updatedBy',
|
interface: 'updatedBy',
|
||||||
@ -438,6 +482,7 @@ export const group = {
|
|||||||
|
|
||||||
export const description = {
|
export const description = {
|
||||||
title: '说明文字',
|
title: '说明文字',
|
||||||
|
group: 'others',
|
||||||
options: {
|
options: {
|
||||||
interface: 'description',
|
interface: 'description',
|
||||||
// name: 'id',
|
// name: 'id',
|
||||||
@ -453,6 +498,7 @@ export const description = {
|
|||||||
*/
|
*/
|
||||||
export const primaryKey = {
|
export const primaryKey = {
|
||||||
title: '主键',
|
title: '主键',
|
||||||
|
group: 'developerMode',
|
||||||
options: {
|
options: {
|
||||||
interface: 'primaryKey',
|
interface: 'primaryKey',
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@ -474,6 +520,7 @@ export const primaryKey = {
|
|||||||
*/
|
*/
|
||||||
export const sort = {
|
export const sort = {
|
||||||
title: '排序',
|
title: '排序',
|
||||||
|
group: 'developerMode',
|
||||||
options: {
|
options: {
|
||||||
interface: 'sort',
|
interface: 'sort',
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
@ -487,6 +534,7 @@ export const sort = {
|
|||||||
|
|
||||||
export const password = {
|
export const password = {
|
||||||
title: '密码',
|
title: '密码',
|
||||||
|
group: 'developerMode',
|
||||||
options: {
|
options: {
|
||||||
interface: 'password',
|
interface: 'password',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
@ -499,6 +547,7 @@ export const password = {
|
|||||||
|
|
||||||
export const json = {
|
export const json = {
|
||||||
title: 'JSON',
|
title: 'JSON',
|
||||||
|
group: 'developerMode',
|
||||||
options: {
|
options: {
|
||||||
interface: 'json',
|
interface: 'json',
|
||||||
type: 'json',
|
type: 'json',
|
||||||
@ -512,6 +561,7 @@ export const json = {
|
|||||||
|
|
||||||
export const icon = {
|
export const icon = {
|
||||||
title: '图标',
|
title: '图标',
|
||||||
|
group: 'developerMode',
|
||||||
options: {
|
options: {
|
||||||
interface: 'icon',
|
interface: 'icon',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -520,3 +570,32 @@ export const icon = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const chinaRegion = {
|
||||||
|
title: '中国行政区划',
|
||||||
|
group: 'choices',
|
||||||
|
options: {
|
||||||
|
interface: 'chinaRegion',
|
||||||
|
type: 'belongsToMany',
|
||||||
|
// 数据来源的数据表,与 dataSource 不同,需要从表数据加载后转化成 dataSource
|
||||||
|
target: 'china_regions',
|
||||||
|
targetKey: 'code',
|
||||||
|
// 值字段
|
||||||
|
// valueField: 'code',
|
||||||
|
// 名称字段
|
||||||
|
labelField: 'name',
|
||||||
|
// TODO(refactor): 等 toWhere 重构完成后要改成 parent
|
||||||
|
// 上级字段名
|
||||||
|
parentField: 'parent_code',
|
||||||
|
// 深度限制,默认:-1(代表不控制,即如果是数据表,则无限加载)
|
||||||
|
// limit: -1,
|
||||||
|
// 可选层级,默认:-1(代表可选的最深层级)
|
||||||
|
// maxLevel: null,
|
||||||
|
// 是否可以不选择到最深一级
|
||||||
|
// 'x-component-props': { changeOnSelect: true }
|
||||||
|
incompletely: false,
|
||||||
|
component: {
|
||||||
|
type: 'cascader',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -152,6 +152,14 @@ const transforms = {
|
|||||||
if (['radio', 'select', 'multipleSelect', 'checkboxes'].includes(interfaceType)) {
|
if (['radio', 'select', 'multipleSelect', 'checkboxes'].includes(interfaceType)) {
|
||||||
prop.enum = field.get('dataSource');
|
prop.enum = field.get('dataSource');
|
||||||
}
|
}
|
||||||
|
if (interfaceType === 'chinaRegion') {
|
||||||
|
set(prop, 'x-component-props.target', field.get('target'));
|
||||||
|
set(prop, 'x-component-props.labelField', field.get('labelField'));
|
||||||
|
set(prop, 'x-component-props.valueField', field.get('targetKey'));
|
||||||
|
set(prop, 'x-component-props.parentField', field.get('parentField'));
|
||||||
|
set(prop, 'x-component-props.maxLevel', field.get('maxLevel'));
|
||||||
|
set(prop, 'x-component-props.changeOnSelect', field.get('incompletely'));
|
||||||
|
}
|
||||||
schema[field.name] = {
|
schema[field.name] = {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
...prop,
|
...prop,
|
||||||
@ -362,7 +370,7 @@ export default async (ctx, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (!['subTable', 'linkTo', 'attachment', 'createdBy', 'updatedBy'].includes(field.get('interface'))) {
|
if (!['subTable', 'linkTo', 'attachment', 'createdBy', 'updatedBy', 'chinaRegion'].includes(field.get('interface'))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let showInKey;
|
let showInKey;
|
||||||
|
Loading…
Reference in New Issue
Block a user