feat: add map plugin (#1229)

* feat: add map plugin

* feat: update

* feat: add Map.Designer

* feat: support polygon and clear canvas

* feat: improve and support linestring

* feat: map type default

* feat: support group order

* feat: support register group

* feat: improve named and logic

* fix: rename

* feat: better

* refactor: move to use postgresSQL supported type

* feat: support circle

* feat: support mysql

* chore: @nocobase/plugin-map

* fix: some error in postgres

* fix: line lose

* fix: accessKey or securityCode is incorrect

* fix: improve

* fix: shake screen in modal

* feat: support serviceHOST

* feat: improve

* feat: support view map in detail

* feat: support patten in details

* fix: something went wrong in edit mode

* fix: field name incorrectly

* feat: support sqlite

* feat: support circle in mysql

* feat: support map configuration

* feat: support map configuration

* fix: remove unused div

* feat: support show map in details

* fix: disabled in details

* fix: unused

* feat: improve readpretty

* fix: schemaInitialize

* feat: improve alert and search

* fix: mysql polygon not work

* test: add fields test

* test: improve

* test: update

* fix: test error

* feat: improve search and support zoom

* fix: if success should reset err message

* feat: add isOverride to confirm

* feat: improve
This commit is contained in:
Dunqing 2022-12-14 21:45:43 +08:00 committed by GitHub
parent 63581688e9
commit a593720c81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1885 additions and 49 deletions

View File

@ -0,0 +1 @@
export { default } from '@nocobase/plugin-map/client';

View File

@ -13,7 +13,6 @@ const InternalField: React.FC = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { name, interface: interfaceType, uiSchema, defaultValue } = useCollectionField(); const { name, interface: interfaceType, uiSchema, defaultValue } = useCollectionField();
const collectionField = useCollectionField(); const collectionField = useCollectionField();
const component = useComponent(uiSchema?.['x-component']); const component = useComponent(uiSchema?.['x-component']);
const compile = useCompile(); const compile = useCompile();
const setFieldProps = (key, value) => { const setFieldProps = (key, value) => {

View File

@ -189,6 +189,7 @@ export const OverridingFieldAction = (props) => {
useCancelAction, useCancelAction,
showReverseFieldConfig: !data?.reverseField, showReverseFieldConfig: !data?.reverseField,
createOnly: true, createOnly: true,
isOverride: true,
targetScope: { name: childCollections }, targetScope: { name: childCollections },
...scope, ...scope,
}} }}

View File

@ -5,7 +5,13 @@ import * as types from '../interfaces';
export const interfaces = new Map<string, ISchema>(); export const interfaces = new Map<string, ISchema>();
const fields = {}; const fields = {};
const groupLabels = {}; const groups: Record<
string,
{
label: string;
order: number;
}
> = {};
export function registerField(group: string, type: string, schema) { export function registerField(group: string, type: string, schema) {
fields[group] = fields[group] || {}; fields[group] = fields[group] || {};
@ -13,42 +19,54 @@ export function registerField(group: string, type: string, schema) {
interfaces.set(type, schema); interfaces.set(type, schema);
} }
export function registerGroupLabel(key: string, label: string) { export function registerGroup(key: string, label: string | { label: string; order?: number }) {
groupLabels[key] = label; const group = typeof label === 'string' ? { label } : label;
if (!group.order) {
group.order = (Object.keys(groups).length + 1) * 10;
}
groups[key] = group as Required<typeof group>;
} }
/**
* @deprecated
*/
export const registerGroupLabel = registerGroup;
Object.keys(types).forEach((type) => { Object.keys(types).forEach((type) => {
const schema = types[type]; const schema = types[type];
registerField(schema.group || 'others', type, { order: 0, ...schema }); registerField(schema.group || 'others', type, { order: 0, ...schema });
}); });
registerGroupLabel('basic', '{{t("Basic")}}'); registerGroup('basic', '{{t("Basic")}}');
registerGroupLabel('choices', '{{t("Choices")}}'); registerGroup('choices', '{{t("Choices")}}');
registerGroupLabel('media', '{{t("Media")}}'); registerGroup('media', '{{t("Media")}}');
registerGroupLabel('datetime', '{{t("Date & Time")}}'); registerGroup('datetime', '{{t("Date & Time")}}');
registerGroupLabel('relation', '{{t("Relation")}}'); registerGroup('relation', '{{t("Relation")}}');
registerGroupLabel('advanced', '{{t("Advanced type")}}'); registerGroup('advanced', '{{t("Advanced type")}}');
registerGroupLabel('systemInfo', '{{t("System info")}}'); registerGroup('systemInfo', '{{t("System info")}}');
registerGroupLabel('others', '{{t("Others")}}'); registerGroup('others', '{{t("Others")}}');
export const getOptions = () => { export const getOptions = () => {
return Object.keys(groupLabels).map((groupName) => { return Object.keys(groups)
return { .map((groupName) => {
label: groupLabels[groupName], const group = groups[groupName];
key: groupName, return {
children: Object.keys(fields[groupName] || {}) ...group,
.map((type) => { key: groupName,
const field = fields[groupName][type]; children: Object.keys(fields[groupName] || {})
return { .map((type) => {
value: type, const field = fields[groupName][type];
label: field.title, return {
name: type, value: type,
...fields[groupName][type], label: field.title,
}; name: type,
}) ...fields[groupName][type],
.sort((a, b) => a.order - b.order), };
}; })
}); .sort((a, b) => a.order - b.order),
};
})
.sort((a, b) => a.order - b.order);
}; };
export const options = getOptions(); export const options = getOptions();

View File

@ -6,7 +6,7 @@ export * from './CollectionManagerSchemaComponentProvider';
export * from './CollectionManagerShortcut'; export * from './CollectionManagerShortcut';
export * from './CollectionProvider'; export * from './CollectionProvider';
export * from './Configuration'; export * from './Configuration';
export { registerField, registerGroupLabel } from './Configuration/interfaces'; export { registerField, registerGroup, registerGroupLabel } from './Configuration/interfaces';
export * from './context'; export * from './context';
export * from './hooks'; export * from './hooks';
export * as interfacesProperties from './interfaces/properties'; export * as interfacesProperties from './interfaces/properties';
@ -15,4 +15,3 @@ export * from './ResourceActionProvider';
export { getConfigurableProperties } from './templates/properties'; export { getConfigurableProperties } from './templates/properties';
export * from './templates/types'; export * from './templates/types';
export * from './types'; export * from './types';

4
packages/plugins/map/client.d.ts vendored Executable file
View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/client';
export { default } from './lib/client';

30
packages/plugins/map/client.js Executable file
View File

@ -0,0 +1,30 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/client"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -0,0 +1,18 @@
{
"name": "@nocobase/plugin-map",
"version": "0.8.0-alpha.13",
"main": "lib/server/index.js",
"devDependencies": {
"@nocobase/server": "0.8.0-alpha.13",
"@nocobase/test": "0.8.0-alpha.13"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@amap/amap-jsapi-types": "^0.0.10",
"@formily/react": "2.0.20",
"@emotion/css": "^11.7.1",
"antd": "~4.19.5",
"sequelize": "^6.9.0",
"ahooks": "^3.0.5"
}
}

4
packages/plugins/map/server.d.ts vendored Executable file
View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/server';
export { default } from './lib/server';

30
packages/plugins/map/server.js Executable file
View File

@ -0,0 +1,30 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/server"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -0,0 +1,343 @@
import React from 'react';
import { useEffect, useRef, useState } from 'react';
import AMapLoader from '@amap/amap-jsapi-loader';
import '@amap/amap-jsapi-types';
import { useFieldSchema } from '@formily/react';
import { useCollection } from '@nocobase/client';
import { css } from '@emotion/css';
import { Alert, Button, Modal } from 'antd';
import { useMapTranslation } from '../locales';
import Search from './Search';
import { useMemoizedFn } from 'ahooks';
import { useMapConfiguration } from '../hooks';
import { useHistory } from 'react-router';
interface AMapComponentProps {
accessKey: string;
securityJsCode: string;
value: any;
onChange: (value: number[]) => void;
disabled?: boolean;
mapType: string;
zoom: number;
}
const methodMapping = {
point: {
mouseTool: 'marker',
propertyKey: 'position',
overlay: 'Marker',
},
polygon: {
mouseTool: 'polygon',
editor: 'PolygonEditor',
propertyKey: 'path',
overlay: 'Polygon',
},
lineString: {
mouseTool: 'polyline',
editor: 'PolylineEditor',
propertyKey: 'path',
overlay: 'Polyline',
},
circle: {
mouseTool: 'circle',
editor: 'CircleEditor',
transformOptions(value) {
return {
center: value.slice(0, 2),
radius: value[2],
};
},
overlay: 'Circle',
},
};
const AMapComponent: React.FC<AMapComponentProps> = (props) => {
const { accessKey, securityJsCode } = useMapConfiguration(props.mapType) || {};
const { value, onChange, disabled, zoom = 13 } = props;
const { t } = useMapTranslation();
const fieldSchema = useFieldSchema();
const aMap = useRef<any>();
const map = useRef<AMap.Map>();
const mouseTool = useRef<any>();
const [needUpdateFlag, forceUpdate] = useState([]);
const [errMessage, setErrMessage] = useState('');
const { getField } = useCollection();
const collectionField = getField(fieldSchema.name);
const type = collectionField?.interface;
const overlay = useRef<any>();
const editor = useRef(null);
const history = useHistory();
const id = useRef(`nocobase-map-${type}-${Date.now().toString(32)}`);
const [commonOptions] = useState<AMap.PolylineOptions & AMap.PolygonOptions>({
strokeWeight: 5,
strokeColor: '#4e9bff',
fillColor: '#4e9bff',
strokeOpacity: 1,
});
const toRemoveOverlay = useMemoizedFn(() => {
if (overlay.current) {
map.current?.remove(overlay.current);
}
});
const setTarget = useMemoizedFn(() => {
if (!disabled && type !== 'point' && editor.current) {
editor.current.setTarget(overlay.current);
editor.current.open();
}
});
const onMapChange = useMemoizedFn((target, onlyChange = false) => {
let nextValue = null;
if (type === 'point') {
const { lat, lng } = (target as AMap.Marker).getPosition();
nextValue = [lng, lat];
} else if (type === 'polygon' || type === 'lineString') {
nextValue = (target as AMap.Polygon).getPath().map((item) => [item.lng, item.lat]);
if (nextValue.length < 2) {
return;
}
} else if (type === 'circle') {
const center = target.getCenter();
const radius = target.getRadius();
nextValue = [center.lng, center.lat, radius];
}
if (!onlyChange) {
toRemoveOverlay();
overlay.current = target;
setTarget();
}
onChange(nextValue);
});
const createEditor = useMemoizedFn(() => {
const mapping = methodMapping[type as keyof typeof methodMapping];
if (mapping && 'editor' in mapping && !editor.current) {
editor.current = new aMap.current[mapping.editor](map.current);
editor.current.on('adjust', function ({ target }) {
onMapChange(target, true);
});
editor.current.on('move', function ({ target }) {
onMapChange(target, true);
});
}
});
const executeMouseTool = useMemoizedFn(() => {
if (!mouseTool.current || editor.current?.getTarget()) return;
const mapping = methodMapping[type as keyof typeof methodMapping];
if (!mapping) {
return;
}
mouseTool.current[mapping.mouseTool]({
...commonOptions,
} as AMap.PolylineOptions);
});
const createMouseTool = useMemoizedFn(() => {
if (mouseTool.current) return;
mouseTool.current = new aMap.current.MouseTool(map.current);
mouseTool.current.on('draw', function ({ obj }) {
onMapChange(obj);
});
executeMouseTool();
});
const toCenter = (position, imm?: boolean) => {
if (map.current) {
map.current.setZoomAndCenter(18, position, imm);
}
};
const onReset = () => {
const ok = () => {
toRemoveOverlay();
if (editor.current) {
editor.current.setTarget();
editor.current.close();
}
onChange(null);
};
Modal.confirm({
title: t('Clear the canvas'),
content: t('Are you sure to clear the canvas?'),
okText: t('Confirm'),
cancelText: t('Cancel'),
onOk() {
ok();
},
});
};
// 编辑时
useEffect(() => {
if (!aMap.current) return;
if (!value || overlay.current) {
return;
}
const mapping = methodMapping[type as keyof typeof methodMapping];
if (!mapping) {
return;
}
const options = { ...commonOptions };
if ('transformOptions' in mapping) {
Object.assign(options, mapping.transformOptions(value));
} else if ('propertyKey' in mapping) {
options[mapping.propertyKey] = value;
}
const nextOverlay = new aMap.current[mapping.overlay](options);
// 聚焦在编辑的位置
map.current.setFitView([nextOverlay]);
nextOverlay.setMap(map.current);
overlay.current = nextOverlay;
createEditor();
setTarget();
}, [value, needUpdateFlag, type, commonOptions]);
// 当在编辑时,关闭 mouseTool
useEffect(() => {
if (!mouseTool.current) return;
if (disabled) {
mouseTool.current?.close();
editor.current?.close();
} else {
executeMouseTool();
editor.current?.open();
}
}, [disabled]);
// AMap.MouseTool & AMap.XXXEditor
useEffect(() => {
if (!aMap.current || !type || disabled) return;
createMouseTool();
createEditor();
}, [disabled, needUpdateFlag, type]);
useEffect(() => {
if (!mouseTool.current || !editor.current) return;
const target = editor.current.getTarget();
if (target) {
mouseTool.current.close?.();
} else {
executeMouseTool();
}
}, [type, value]);
useEffect(() => {
if (!accessKey || map.current) return;
if (securityJsCode) {
(window as any)._AMapSecurityConfig = {
[securityJsCode.endsWith('_AMapService') ? 'serviceHOST' : 'securityJsCode']: securityJsCode,
};
}
AMapLoader.load({
key: accessKey,
version: '2.0',
plugins: ['AMap.MouseTool', 'AMap.PolygonEditor', 'AMap.PolylineEditor', 'AMap.CircleEditor'],
})
.then((amap) => {
setTimeout(() => {
map.current = new amap.Map(id.current, {
resizeEnable: true,
zoom,
} as AMap.MapOptions);
aMap.current = amap;
setErrMessage('');
forceUpdate([]);
}, Math.random() * 300);
})
.catch((err) => {
if (err.includes('多个不一致的 key')) {
setErrMessage(t('The AccessKey is incorrect, please check it'));
} else {
setErrMessage(err);
}
});
return () => {
map.current?.destroy();
aMap.current = null;
map.current = null;
mouseTool.current = null;
editor.current = null;
};
}, [accessKey, type, securityJsCode]);
if (!accessKey || errMessage) {
return (
<Alert
action={
<Button type="primary" onClick={() => history.push('/admin/settings/map-configuration/configuration')}>
{t('Go to the configuration page')}
</Button>
}
message={errMessage || t('Please configure the AccessKey and SecurityJsCode first')}
type="error"
/>
);
}
return (
<div
className={css`
position: relative;
`}
id={id.current}
style={{
height: '500px',
}}
>
{!disabled ? (
<>
<Search toCenter={toCenter} aMap={aMap.current} />
<div
className={css`
position: absolute;
bottom: 20px;
left: 10px;
z-index: 2;
pointer-events: none;
`}
>
<Alert message={t('Click to select the starting point and double-click to end the drawing')} type="info" />
</div>
<div
className={css`
position: absolute;
bottom: 20px;
right: 20px;
z-index: 2;
`}
>
<Button
disabled={!value}
style={{
height: '40px',
}}
onClick={onReset}
type="primary"
>
{t('Clear')}
</Button>
</div>
</>
) : null}
</div>
);
};
export default AMapComponent;

View File

@ -0,0 +1,91 @@
import { useAPIClient, useCompile, useRequest } from '@nocobase/client';
import { useBoolean } from 'ahooks';
import { Form, Input, Tabs, Button, Card, message } from 'antd';
import React, { useMemo, useEffect } from 'react';
import { MapTypes } from '../constants';
import { MapConfigurationResourceKey, useMapConfiguration } from '../hooks';
import { useMapTranslation } from '../locales';
const AMapConfiguration = ({ type }) => {
const { t } = useMapTranslation();
const [isDisabled, disableAction] = useBoolean(false);
const apiClient = useAPIClient();
const [form] = Form.useForm();
const data = useMapConfiguration(type);
useEffect(() => {
if (data) {
form.setFieldsValue(data);
disableAction.toggle();
}
}, [data]);
const resource = useMemo(() => {
return apiClient.resource(MapConfigurationResourceKey);
}, [apiClient]);
const onSubmit = (values) => {
resource
.set({
...values,
type,
})
.then((res) => {
message.success(t('Saved successfully'));
})
.catch((err) => {
message.success(t('Saved failed'));
});
};
return (
<Form form={form} layout="vertical" onFinish={onSubmit}>
<Form.Item required name="accessKey" label={t('Access key')}>
<Input disabled={isDisabled} />
</Form.Item>
<Form.Item required name="securityJsCode" label={t('securityJsCode or serviceHost')}>
<Input disabled={isDisabled} />
</Form.Item>
{isDisabled ? (
<Button onClick={disableAction.toggle} type="ghost">
{t('Edit')}
</Button>
) : (
<Form.Item>
<Button type="primary" htmlType="submit">
{t('Save')}
</Button>
</Form.Item>
)}
</Form>
);
};
const components = {
amap: AMapConfiguration,
google: () => <div>Coming soon</div>,
};
const tabList = MapTypes.map((item) => {
return {
...item,
component: components[item.value],
};
});
const Configuration = () => {
const compile = useCompile();
return (
<Card bordered>
<Tabs type="card">
{tabList.map((tab) => {
return (
<Tabs.TabPane key={tab.value} tab={compile(tab.label)}>
<tab.component type={tab.value} />
</Tabs.TabPane>
);
})}
</Tabs>
</Card>
);
};
export default Configuration;

View File

@ -0,0 +1,260 @@
import { Field } from '@formily/core';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import {
GeneralSchemaDesigner,
SchemaSettings,
useCollection,
useCollectionManager,
useDesignable,
useFormBlockContext,
} from '@nocobase/client';
import _ from 'lodash';
import React from 'react';
import { useMapTranslation } from '../locales';
const Designer = () => {
const { getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const { form } = useFormBlockContext();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useMapTranslation();
const { dn, refresh } = useDesignable();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const originalTitle = collectionField?.uiSchema?.title;
const initialValue = {
title: field.title === originalTitle ? undefined : field.title,
};
if (!field.readPretty) {
initialValue['required'] = field.required;
}
let readOnlyMode = 'editable';
if (fieldSchema['x-disabled'] === true) {
readOnlyMode = 'readonly';
}
if (fieldSchema['x-read-pretty'] === true) {
readOnlyMode = 'read-pretty';
}
return (
<GeneralSchemaDesigner>
<SchemaSettings.ModalItem
key="edit-field-title"
title={t('Edit field title')}
schema={
{
type: 'object',
title: t('Edit field title'),
properties: {
title: {
title: t('Field title'),
default: field?.title,
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ title }) => {
if (title) {
field.title = title;
fieldSchema.title = title;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
title: fieldSchema.title,
},
});
}
dn.refresh();
}}
/>
{!field.readPretty && (
<SchemaSettings.ModalItem
key="edit-description"
title={t('Edit description')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
description: {
// title: t('Description'),
default: field?.description,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ description }) => {
field.description = description;
fieldSchema.description = description;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
description: fieldSchema.description,
},
});
dn.refresh();
}}
/>
)}
{field.readPretty && (
<SchemaSettings.ModalItem
key="edit-tooltip"
title={t('Edit tooltip')}
schema={
{
type: 'object',
title: t('Edit description'),
properties: {
tooltip: {
default: fieldSchema?.['x-decorator-props']?.tooltip,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ tooltip }) => {
field.decoratorProps.tooltip = tooltip;
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
dn.refresh();
}}
/>
)}
{!field.readPretty && (
<SchemaSettings.SwitchItem
key="required"
title={t('Required')}
checked={fieldSchema.required as boolean}
onChange={(required) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
field.required = required;
fieldSchema['required'] = required;
schema['required'] = required;
dn.emit('patch', {
schema,
});
refresh();
}}
/>
)}
{form && !form?.readPretty && fieldSchema?.['x-component-props']?.['pattern-disable'] != true && (
<SchemaSettings.SelectItem
key="pattern"
title={t('Pattern')}
options={[
{ label: t('Editable'), value: 'editable' },
{ label: t('Readonly'), value: 'readonly' },
{ label: t('Easy-reading'), value: 'read-pretty' },
]}
value={readOnlyMode}
onChange={(v) => {
const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'],
};
switch (v) {
case 'readonly': {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = true;
schema['x-read-pretty'] = false;
schema['x-disabled'] = true;
field.readPretty = false;
field.disabled = true;
break;
}
case 'read-pretty': {
fieldSchema['x-read-pretty'] = true;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = true;
schema['x-disabled'] = false;
field.readPretty = true;
break;
}
default: {
fieldSchema['x-read-pretty'] = false;
fieldSchema['x-disabled'] = false;
schema['x-read-pretty'] = false;
schema['x-disabled'] = false;
field.readPretty = false;
field.disabled = false;
break;
}
}
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
)}
<SchemaSettings.ModalItem
key="map-zoom"
title={t('Set default zoom level')}
schema={
{
type: 'object',
title: t('Set default zoom level'),
properties: {
zoom: {
title: t('Zoom'),
default: field.componentProps.zoom || 13,
description: t('The default zoom level of the map'),
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
precision: 0,
},
},
},
} as ISchema
}
onSubmit={({ zoom }) => {
if (zoom) {
_.set(fieldSchema, 'x-component-props.zoom', zoom);
Object.assign(field.componentProps, fieldSchema['x-component-props']);
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-component-props': field.componentProps,
},
});
}
dn.refresh();
}}
/>
<SchemaSettings.Remove
key="remove"
removeParentsIfNoChildren
confirm={{
title: t('Delete field'),
}}
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};
export default Designer;

View File

@ -0,0 +1,29 @@
import { connect, mapReadPretty } from '@formily/react';
import React from 'react';
import AMapComponent from './AMap';
import ReadPretty from './ReadPretty';
import { css } from '@emotion/css';
import Designer from './Designer';
const InternalMap = connect((props) => {
return (
<div
className={css`
border: 1px solid transparent;
.ant-formily-item-error & {
border: 1px solid #ff4d4f;
}
`}
>
{props.mapType ? <AMapComponent {...props} /> : null}
</div>
);
}, mapReadPretty(ReadPretty));
const Map = InternalMap as typeof InternalMap & {
Designer: typeof Designer;
};
Map.Designer = Designer;
export default Map;

View File

@ -0,0 +1,34 @@
import { useField, useFieldSchema } from '@formily/react';
import { useCollection } from '@nocobase/client';
import React, { useEffect } from 'react';
import AMapComponent from './AMap';
const ReadPretty = (props) => {
const { value, readOnly } = props;
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const collectionField = getField(fieldSchema.name);
const mapType = props.mapType || collectionField?.uiSchema['x-component-props']?.mapType;
const field = useField();
useEffect(() => {
if (!field.title) {
field.title = collectionField.uiSchema.title;
}
}, collectionField.title);
if (!readOnly)
return (
<div
style={{
whiteSpace: 'pre-wrap',
}}
>
{value?.map((item) => (Array.isArray(item) ? `(${item.join(',')})` : item)).join(',')}
</div>
);
return mapType === 'amap' ? <AMapComponent mapType={mapType} {...props}></AMapComponent> : null;
};
export default ReadPretty;

View File

@ -0,0 +1,93 @@
import { message, Select } from 'antd';
import { css } from '@emotion/css';
import React, { useEffect, useRef, useState } from 'react';
import { useDebounceFn } from 'ahooks';
import { useMapTranslation } from '../locales';
interface SearchProps {
aMap: any;
toCenter: (p: any) => void;
}
const Search = (props: SearchProps) => {
const { aMap, toCenter } = props;
const { t } = useMapTranslation();
const placeSearch = useRef<any>();
const [options, setOptions] = useState([]);
useEffect(() => {
aMap?.plugin('AMap.PlaceSearch', () => {
placeSearch.current = new aMap.PlaceSearch({
city: '全国',
pageSize: 30,
});
});
}, [aMap]);
const { run: onSearch } = useDebounceFn(
(keyword) => {
if (!placeSearch.current) {
return;
}
placeSearch.current.search(keyword || ' ', (status, result) => {
if (status === 'complete') {
setOptions(
result.poiList.pois.map((item) => {
return {
...item,
label: `${item.name}-${item.address}`,
value: item.id,
};
}),
);
} else {
if (status === 'no_data') {
setOptions([]);
return;
}
message.error(t('Please configure the AMap securityCode or securityHost correctly'));
}
});
},
{
wait: 300,
},
);
const onSelect = (value) => {
const place = options.find((o) => {
return o.value === value;
});
if (place?.location) {
toCenter(place.location);
}
};
return (
<div
className={css`
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
width: calc(100% - 20px);
`}
>
<Select
showSearch
allowClear
style={{
background: 'rgba(255, 255, 255, 0.8)',
}}
placeholder={t('Enter keywords to search')}
filterOption={false}
onSearch={onSearch}
onSelect={onSelect}
options={options}
></Select>
</div>
);
};
export default Search;

View File

@ -0,0 +1,6 @@
import { generateNTemplate } from "./locales";
export const MapTypes = [
{ label: generateNTemplate('AMap'), value: 'amap' },
{ label: generateNTemplate('Google Maps'), value: 'google' },
]

View File

@ -0,0 +1,23 @@
import { IField } from '@nocobase/client';
import { generateNTemplate } from '../locales';
import { commonSchema } from './schema';
export const circle: IField = {
name: 'circle',
type: 'object',
group: 'map',
order: 3,
title: generateNTemplate('Circle'),
description: generateNTemplate('Circle'),
sortable: true,
default: {
type: 'circle',
uiSchema: {
type: 'void',
'x-component': 'Map',
'x-component-designer': 'Map.Designer',
'x-component-props': {},
},
},
...commonSchema,
};

View File

@ -0,0 +1,16 @@
import { circle } from './circle'
import { lineString } from './lineString'
import { point } from './point'
import { polygon } from './polygon'
export const fields = [
point,
polygon,
lineString,
circle
]
export const interfaces = fields.reduce((ins, field) => {
ins[field.name] = field
return ins
}, {})

View File

@ -0,0 +1,23 @@
import { IField } from '@nocobase/client';
import { generateNTemplate } from '../locales';
import { commonSchema } from './schema';
export const lineString: IField = {
name: 'lineString',
type: 'object',
group: 'map',
order: 2,
title: generateNTemplate('Line'),
description: generateNTemplate('Line'),
sortable: true,
default: {
type: 'lineString',
uiSchema: {
type: 'void',
'x-component': 'Map',
'x-component-designer': 'Map.Designer',
'x-component-props': {},
},
},
...commonSchema,
};

View File

@ -0,0 +1,24 @@
import { ISchema } from '@formily/react';
import { IField } from '@nocobase/client';
import { generateNTemplate } from '../locales';
import { commonSchema } from './schema';
export const point: IField = {
name: 'point',
type: 'object',
group: 'map',
order: 1,
title: generateNTemplate('Point'),
description: generateNTemplate('Point'),
sortable: true,
default: {
type: 'point',
uiSchema: {
type: 'void',
'x-component': 'Map',
'x-component-designer': 'Map.Designer',
'x-component-props': {},
},
},
...commonSchema,
};

View File

@ -0,0 +1,23 @@
import { IField } from '@nocobase/client';
import { generateNTemplate } from '../locales';
import { commonSchema } from './schema';
export const polygon: IField = {
name: 'polygon',
type: 'object',
group: 'map',
order: 4,
title: generateNTemplate('Polygon'),
description: generateNTemplate('Polygon'),
sortable: true,
default: {
type: 'polygon',
uiSchema: {
type: 'void',
'x-component': 'Map',
'x-component-designer': 'Map.Designer',
'x-component-props': {},
},
},
...commonSchema,
};

View File

@ -0,0 +1,57 @@
import { ISchema } from '@formily/react';
import { interfacesProperties } from '@nocobase/client';
import { MapTypes } from '../constants';
import { generateNTemplate } from '../locales';
const { defaultProps } = interfacesProperties;
if (Array.isArray(defaultProps.type.enum)) {
defaultProps.type.enum.push(
{
label: 'Point',
value: 'point',
},
{
label: 'LineString',
value: 'lineString',
},
{
label: 'Polygon',
value: 'polygon',
},
{
label: 'Circle',
value: 'circle',
},
);
}
export const commonSchema = {
properties: {
...defaultProps,
'uiSchema.x-component-props.mapType': {
title: generateNTemplate('Map type'),
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
showSearch: false,
allowClear: false,
},
'x-disabled': '{{ isOverride || !createOnly }}',
default: 'amap',
enum: MapTypes
}
},
schemaInitialize(schema: ISchema, { readPretty, block }) {
if (block === 'Form') {
Object.assign(schema, {
'x-component-props': {
readOnly: readPretty ? true : false
},
'x-designer': 'Map.Designer',
});
}
},
}

View File

@ -0,0 +1 @@
export * from './useMapConfiguration'

View File

@ -0,0 +1,15 @@
import { useRequest } from "@nocobase/client";
export const MapConfigurationResourceKey = 'map-configuration';
export const useMapConfiguration = (type: string) => {
return useRequest({
resource: MapConfigurationResourceKey,
action: 'get',
params: {
type,
},
}).data?.data;
}

View File

@ -0,0 +1,43 @@
import {
CollectionManagerContext,
CurrentAppInfoProvider,
SchemaComponentOptions,
SettingsCenterProvider,
} from '@nocobase/client';
import React, { useContext } from 'react';
import Configuration from './components/Configuration';
import Map from './components/Map';
import { interfaces } from './fields';
import { Initialize } from './initialize';
import { useMapTranslation } from './locales';
export default React.memo((props) => {
const ctx = useContext(CollectionManagerContext);
const { t } = useMapTranslation();
return (
<CurrentAppInfoProvider>
<Initialize>
<SettingsCenterProvider
settings={{
'map-configuration': {
title: t('Map Manager'),
icon: 'EnvironmentOutlined',
tabs: {
configuration: {
title: t('Configuration'),
component: Configuration,
},
},
},
}}
>
<SchemaComponentOptions components={{ Map }}>
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, ...interfaces } }}>
{props.children}
</CollectionManagerContext.Provider>
</SchemaComponentOptions>
</SettingsCenterProvider>
</Initialize>
</CurrentAppInfoProvider>
);
});

View File

@ -0,0 +1,33 @@
import React from 'react';
import { registerField, registerGroup, useCurrentAppInfo } from '@nocobase/client';
import { generateNTemplate } from './locales';
import './locales';
import { fields } from './fields';
import { useEffect } from 'react';
export const useRegisterInterface = () => {
const { data } = useCurrentAppInfo() || {};
useEffect(() => {
const dialect = data?.database.dialect;
if (!dialect) return;
registerGroup(fields[0].group, {
label: generateNTemplate('Map-based geometry'),
order: 51,
});
fields.forEach((field) => {
if (Array.isArray(field.dialects)) {
if (!field.dialects.includes(dialect)) {
return;
}
}
registerField(field.group, field.title, field);
});
}, [data]);
};
export const Initialize: React.FC = (props) => {
useRegisterInterface();
return <React.Fragment>{props.children}</React.Fragment>;
};

View File

@ -0,0 +1,5 @@
const locale = {
}
export default locale;

View File

@ -0,0 +1,22 @@
import { i18n } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
import enUS from './en-US';
import zhCN from './zh-CN';
export const NAMESPACE = 'map';
i18n.addResources('zh-CN', NAMESPACE, zhCN);
i18n.addResources('en-US', NAMESPACE, enUS);
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
}
export function generateNTemplate(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}' })}}`;
}
export function useMapTranslation() {
return useTranslation(NAMESPACE);
}

View File

@ -0,0 +1,45 @@
const locale = {
'Map-based geometry': '基于地图的几何图形',
'Map type': '地图类型',
'Point': '点',
'Line': '线',
'Circle': '圆',
'Polygon': '多边形',
'Access key': '访问密钥',
'securityJsCode or serviceHost': 'securityJsCode 或 serviceHost',
'AMap': '高德地图',
'Google Maps': '谷歌地图',
'Clear': '清空',
'Click to select the starting point and double-click to end the drawing': '点击选择起点,双击结束绘制',
'Clear the canvas': '清空画布',
'Are you sure to clear the canvas?': '您确定要清空画布吗?',
'Confirm': '确定',
'Cancel': '取消',
'Enter keywords to search': '输入地方名关键字搜索(必须包含省/市)',
'The AccessKey is incorrect, please check it': '访问密钥不正确,请检查',
'Please configure the AMap securityCode or serviceHost correctly': '请正确配置高德地图 securityCode 或 serviceHost',
'Map Manager': '地图管理',
'Configuration': '配置',
'Saved successfully': '保存成功',
'Saved failed': '保存失败',
'Edit': '编辑',
'Save': '保存',
'Please configure the AccessKey and SecurityJsCode first': '请先配置 AccessKey 和 SecurityJsCode',
'Go to the configuration page': '前往配置页面',
'Zoom': '缩放',
'Set default zoom level': '设置默认缩放级别',
'The default zoom level of the map': '地图默认缩放级别',
// Designer
'Edit field title': '编辑字段标题',
'Field title': '字段标题',
'Edit tooltip': '编辑提示信息',
'Delete field': '删除字段',
"Required": "必填",
'Pattern': '模式',
"Editable": "可编辑",
"Readonly": "只读(禁止编辑)",
"Easy-reading": "只读(阅读模式)",
"Edit description": "编辑描述",
}
export default locale;

View File

@ -0,0 +1 @@
export { default } from './server';

View File

@ -0,0 +1,163 @@
import Database from '@nocobase/database';
import { mockDatabase } from '@nocobase/test';
import { PointField, CircleField, PolygonField, LineStringField } from '../fields';
const data = {
polygon: [
[114.081074, 22.563646],
[114.147335, 22.559207],
[114.134975, 22.531621],
[114.09103, 22.520045],
[114.033695, 22.575376],
[114.025284, 22.55461],
[114.033523, 22.533048],
],
point: [114.048868, 22.554927],
circle: [114.058996, 22.549695, 4171],
lineString: [
[114.047323, 22.534158],
[114.120966, 22.544146],
],
};
describe('fields', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase();
db.registerFieldTypes({
point: PointField,
circle: CircleField,
polygon: PolygonField,
lineString: LineStringField,
});
});
afterEach(async () => {
await db.close();
});
const createCollection = async () => {
const fields = [
{
type: 'point',
name: 'point',
},
{
type: 'polygon',
name: 'polygon',
},
{
type: 'circle',
name: 'circle',
},
{
type: 'lineString',
name: 'lineString',
},
];
const Test = db.collection({
name: 'tests',
fields,
});
await db.sync();
return Test;
};
it('define', async () => {
const Test = await createCollection();
await Test.model.create();
});
it('create', async () => {
const Test = await createCollection();
const model = await Test.model.create(data);
expect(model.get()).toMatchObject(data);
});
it('find', async () => {
const Test = await createCollection();
await Test.model.create(data);
expect(await Test.model.findOne()).toMatchObject(data);
});
it('set and get', async () => {
const Test = await createCollection();
const model = await Test.model.create();
model.set('point', [1, 2]);
expect(model.get('point')).toMatchObject([1, 2]);
model.set('polygon', [
[3, 4],
[5, 6],
]);
expect(model.get('polygon')).toMatchObject([
[3, 4],
[5, 6],
]);
model.set('lineString', [[5, 6], [7, 8]]);
expect(model.get('lineString')).toMatchObject([[5, 6], [7, 8]]);
model.set('circle', [1, 2, 0.5]);
expect(model.get('circle')).toMatchObject([1, 2, 0.5]);
});
it('create and update', async () => {
const Test = await createCollection();
const model = await Test.model.create(data);
await model.save();
const findOne = () =>
db.getRepository('tests').findOne({
except: ['createdAt', 'updatedAt', 'id'],
});
expect(await findOne()).toMatchObject(data);
await model.update({
point: [1, 2],
polygon: null,
});
expect(await findOne()).toMatchInlineSnapshot(`
Object {
"circle": Array [
114.058996,
22.549695,
4171,
],
"lineString": Array [
Array [
114.047323,
22.534158,
],
Array [
114.120966,
22.544146,
],
],
"point": Array [
1,
2,
],
"polygon": null,
}
`);
});
it('empty', async () => {
const Test = await createCollection();
const model = await Test.model.create();
await model.save();
const findOne = () =>
db.getRepository('tests').findOne({
except: ['createdAt', 'updatedAt', 'id'],
});
expect(await findOne()).toMatchInlineSnapshot(`
Object {
"circle": null,
"lineString": null,
"point": null,
"polygon": null,
}
`);
});
});

View File

@ -0,0 +1,46 @@
import { Context } from '@nocobase/actions';
import { MapConfigurationCollectionName } from '../constants';
export const getConfiguration = async (ctx: Context, next) => {
const {
params: { type },
} = ctx.action;
const repo = ctx.db.getRepository(MapConfigurationCollectionName);
const record = await repo.findOne({
filter: {
type,
},
});
ctx.body = record
return next();
};
export const setConfiguration = async (ctx: Context, next) => {
const {
params: values,
} = ctx.action;
const repo = ctx.db.getRepository(MapConfigurationCollectionName);
const record = await repo.findOne({
filter: {
type: values.type,
},
});
if (record) {
await repo.update({
values,
filter: {
type: values.type,
}
});
} else {
await repo.create({
values,
})
}
ctx.body = 'ok'
return next();
};

View File

@ -0,0 +1,27 @@
import { CollectionOptions } from "@nocobase/client";
import { MapConfigurationCollectionName } from "../constants";
export default {
name: MapConfigurationCollectionName,
title: '{{t("Map Manager")}}',
fields: [
{
title: 'Access key',
comment: '访问密钥',
name: 'accessKey',
type: 'string'
},
{
title: 'securityJsCode',
comment: 'securityJsCode or serviceHOST',
name: 'securityJsCode',
type: 'string'
},
{
title: 'Map type',
comment: '地图类型',
name: 'type',
type: 'string',
}
]
} as CollectionOptions

View File

@ -0,0 +1 @@
export const MapConfigurationCollectionName = 'mapConfiguration';

View File

@ -0,0 +1,50 @@
import { BaseColumnFieldOptions, Field, FieldContext } from '@nocobase/database';
import { DataTypes } from 'sequelize';
import { isPg, toValue } from '../helpers';
// @ts-ignore
class Circle extends DataTypes.ABSTRACT {
key = 'Circle';
}
export class CircleField extends Field {
constructor(options?: any, context?: FieldContext) {
const { name } = options
super(
{
get() {
const value = this.getDataValue(name);
if (isPg(context)) {
if (typeof value === 'string') {
return toValue(`(${value})`)
}
return value ? [value.x, value.y, value.radius] : null
} else {
return value
}
},
set(value) {
if (isPg(context)) {
value = value.join(',')
}
this.setDataValue(name, value)
},
...options,
},
context,
);
}
get dataType() {
if (isPg(this.context)) {
return Circle;
} else {
return DataTypes.JSON
}
}
}
export interface CircleFieldOptions extends BaseColumnFieldOptions {
type: 'circle';
}

View File

@ -0,0 +1,4 @@
export * from './point'
export * from './lineString'
export * from './polygon'
export * from './circle'

View File

@ -0,0 +1,56 @@
import { BaseColumnFieldOptions, Field, FieldContext } from '@nocobase/database';
import { DataTypes } from 'sequelize';
import { isMysql, isPg, isSqlite, joinComma, toValue } from '../helpers';
// @ts-ignore
class LineString extends DataTypes.ABSTRACT {
key = 'Path';
}
export class LineStringField extends Field {
constructor(options?: any, context?: FieldContext) {
const { name } = options
super(
{
get() {
const value = this.getDataValue(name);
if (isPg(context)) {
return toValue(value)
} else if (isMysql(context)) {
return value?.coordinates || null
} else {
return value
}
},
set(value) {
if (isPg(context)) {
value = joinComma(value.map(joinComma))
} else if (isMysql(context)) {
value = value?.length ? {
type: 'LineString',
coordinates: value
} : null
}
this.setDataValue(name, value)
},
...options,
},
context,
);
}
get dataType() {
if (isPg(this.context)) {
return LineString
} if (isMysql(this.context)) {
return DataTypes.GEOMETRY('LINESTRING');
} else {
return DataTypes.JSON;
}
}
}
export interface LineStringOptions extends BaseColumnFieldOptions {
type: 'lineString';
}

View File

@ -0,0 +1,59 @@
import { BaseColumnFieldOptions, Field, FieldContext } from '@nocobase/database';
import { DataTypes } from 'sequelize';
import { isMysql, isPg, joinComma, toValue } from '../helpers';
// @ts-ignore
class Point extends DataTypes.ABSTRACT {
key = 'Point';
}
export class PointField extends Field {
constructor(options?: any, context?: FieldContext) {
const { name } = options
super(
{
get() {
const value = this.getDataValue(name);
if (isPg(context)) {
if (typeof value === 'string') {
return toValue(value)
}
return value ? [value.x, value.y] : null
} else if (isMysql(context)) {
return value?.coordinates || null
} else {
return value
}
},
set(value) {
if (isPg(context)) {
value = joinComma(value)
} else if (isMysql(context)) {
value = value?.length ? {
type: 'Point',
coordinates: value
} : null
}
this.setDataValue(name, value)
},
...options,
},
context,
);
}
get dataType() {
if (isPg(this.context)) {
return Point;
} if (isMysql(this.context)) {
return DataTypes.GEOMETRY('POINT');
} else {
return DataTypes.JSON;
}
}
}
export interface PointFieldOptions extends BaseColumnFieldOptions {
type: 'point';
}

View File

@ -0,0 +1,56 @@
import { BaseColumnFieldOptions, Field, FieldContext } from '@nocobase/database';
import { DataTypes } from 'sequelize';
import { isMysql, isPg, joinComma, toValue } from '../helpers';
// @ts-ignore
class Polygon extends DataTypes.ABSTRACT {
key = 'Polygon'
}
export class PolygonField extends Field {
constructor(options?: any, context?: FieldContext) {
const { name } = options
super(
{
get() {
const value = this.getDataValue(name)
if (isPg(context)) {
return toValue(value)
} else if (isMysql(context)) {
return value?.coordinates[0].slice(0, -1) || null
} else {
return value
}
},
set(value) {
if (isPg(context)) {
value = value ? joinComma(value.map((item: any) => joinComma(item))) : null
} else if (isMysql(context)) {
value = value?.length ? {
type: 'Polygon',
coordinates: [value.concat([value[0]])]
} : null
}
this.setDataValue(name, value)
},
...options,
},
context,
);
}
get dataType() {
if (isPg(this.context)) {
return Polygon;
} else if (isMysql(this.context)) {
return DataTypes.GEOMETRY('POLYGON');
} else {
return DataTypes.JSON;
}
}
}
export interface PolygonFieldOptions extends BaseColumnFieldOptions {
type: 'polygon';
}

View File

@ -0,0 +1,25 @@
export const joinComma = (value: any[]) => {
if (!value) return null
return `(${value.join(',')})`
}
export const toValue = (value?: string) => {
if (!value) return null
return JSON.parse(value.replace(/\(/g, '[').replace(/\)/g, ']'))
}
export const getDialect = (ctx) => {
return (ctx.db || ctx.database).sequelize.getDialect();
};
export const isPg = (ctx) => {
return getDialect(ctx) === 'postgres';
};
export const isSqlite = (ctx) => {
return getDialect(ctx) === 'sqlite';
};
export const isMysql = (ctx) => {
return getDialect(ctx) === 'mysql';
};

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,46 @@
import { InstallOptions, Plugin } from '@nocobase/server';
import { CircleField, LineStringField, PointField, PolygonField } from './fields';
import { resolve } from 'path';
import { getConfiguration, setConfiguration } from './actions';
export class MapPlugin extends Plugin {
afterAdd() { }
beforeLoad() {
const fields = {
point: PointField,
polygon: PolygonField,
lineString: LineStringField,
circle: CircleField
};
this.db.registerFieldTypes(fields);
}
async load() {
await this.db.import({
directory: resolve(__dirname, 'collections'),
});
this.app.resource(({
name: 'map-configuration',
actions: {
get: getConfiguration,
set: setConfiguration
},
only: ['get', 'set']
}))
}
async install(options?: InstallOptions) { }
async afterEnable() { }
async afterDisable() { }
async remove() { }
}
export default MapPlugin;

View File

@ -29,6 +29,7 @@
"@nocobase/plugin-users": "0.8.0-alpha.13", "@nocobase/plugin-users": "0.8.0-alpha.13",
"@nocobase/plugin-verification": "0.8.0-alpha.13", "@nocobase/plugin-verification": "0.8.0-alpha.13",
"@nocobase/plugin-workflow": "0.8.0-alpha.13", "@nocobase/plugin-workflow": "0.8.0-alpha.13",
"@nocobase/plugin-map": "0.8.0-alpha.13",
"@nocobase/server": "0.8.0-alpha.13" "@nocobase/server": "0.8.0-alpha.13"
}, },
"repository": { "repository": {

View File

@ -6,28 +6,28 @@ export class PresetNocoBase extends Plugin {
const builtInPlugins = process.env.PRESET_NOCOBASE_PLUGINS const builtInPlugins = process.env.PRESET_NOCOBASE_PLUGINS
? process.env.PRESET_NOCOBASE_PLUGINS.split(',') ? process.env.PRESET_NOCOBASE_PLUGINS.split(',')
: [ : [
'error-handler', 'error-handler',
'collection-manager', 'collection-manager',
'ui-schema-storage', 'ui-schema-storage',
'ui-routes-storage', 'ui-routes-storage',
'file-manager', 'file-manager',
'system-settings', 'system-settings',
'verification', 'verification',
'users', 'users',
'acl', 'acl',
'china-region', 'china-region',
'workflow', 'workflow',
'client', 'client',
'export', 'export',
'import', 'import',
'audit-logs', 'audit-logs',
]; ];
await this.app.pm.add(builtInPlugins, { await this.app.pm.add(builtInPlugins, {
enabled: true, enabled: true,
builtIn: true, builtIn: true,
installed: true, installed: true,
}); });
const localPlugins = ['sample-hello', 'oidc', 'saml']; const localPlugins = ['sample-hello', 'oidc', 'saml', 'map'];
await this.app.pm.add(localPlugins, {}); await this.app.pm.add(localPlugins, {});
await this.app.reload(); await this.app.reload();
} }

View File

@ -97,6 +97,16 @@
"@types/xml2js" "^0.4.5" "@types/xml2js" "^0.4.5"
xml2js "^0.4.22" xml2js "^0.4.22"
"@amap/amap-jsapi-loader@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz#9ec4b4d5d2467eac451f6c852e35db69e9f9f0c0"
integrity sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==
"@amap/amap-jsapi-types@^0.0.10":
version "0.0.10"
resolved "https://registry.yarnpkg.com/@amap/amap-jsapi-types/-/amap-jsapi-types-0.0.10.tgz#0e8e69ac8921ed3dde74da209dbb7e04b830debd"
integrity sha512-znvqLGPBy9NRCr1/3650o9vL1aYl/f1YK0+UGn8lBUvHJXND6uMDJGJsl43cEYglw9/tblwIRxjm4pIotOvSCQ==
"@ampproject/remapping@^2.1.0": "@ampproject/remapping@^2.1.0":
version "2.2.0" version "2.2.0"
resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"