mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 05:25:52 +00:00
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:
parent
63581688e9
commit
a593720c81
1
packages/app/client/src/plugins/map.ts
Normal file
1
packages/app/client/src/plugins/map.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-map/client';
|
@ -13,7 +13,6 @@ const InternalField: React.FC = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { name, interface: interfaceType, uiSchema, defaultValue } = useCollectionField();
|
||||
const collectionField = useCollectionField();
|
||||
|
||||
const component = useComponent(uiSchema?.['x-component']);
|
||||
const compile = useCompile();
|
||||
const setFieldProps = (key, value) => {
|
||||
@ -43,7 +42,7 @@ const InternalField: React.FC = (props) => {
|
||||
if (ctx?.form) {
|
||||
setFieldProps('initialValue', fieldSchema.default || defaultValue);
|
||||
}
|
||||
|
||||
|
||||
if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) {
|
||||
const concatSchema = concat([], uiSchema['x-validator'] || [], fieldSchema['x-validator'] || []);
|
||||
field.validator = concatSchema;
|
||||
|
@ -189,6 +189,7 @@ export const OverridingFieldAction = (props) => {
|
||||
useCancelAction,
|
||||
showReverseFieldConfig: !data?.reverseField,
|
||||
createOnly: true,
|
||||
isOverride: true,
|
||||
targetScope: { name: childCollections },
|
||||
...scope,
|
||||
}}
|
||||
|
@ -5,7 +5,13 @@ import * as types from '../interfaces';
|
||||
export const interfaces = new Map<string, ISchema>();
|
||||
|
||||
const fields = {};
|
||||
const groupLabels = {};
|
||||
const groups: Record<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
order: number;
|
||||
}
|
||||
> = {};
|
||||
|
||||
export function registerField(group: string, type: string, schema) {
|
||||
fields[group] = fields[group] || {};
|
||||
@ -13,42 +19,54 @@ export function registerField(group: string, type: string, schema) {
|
||||
interfaces.set(type, schema);
|
||||
}
|
||||
|
||||
export function registerGroupLabel(key: string, label: string) {
|
||||
groupLabels[key] = label;
|
||||
export function registerGroup(key: string, label: string | { label: string; order?: number }) {
|
||||
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) => {
|
||||
const schema = types[type];
|
||||
registerField(schema.group || 'others', type, { order: 0, ...schema });
|
||||
});
|
||||
|
||||
registerGroupLabel('basic', '{{t("Basic")}}');
|
||||
registerGroupLabel('choices', '{{t("Choices")}}');
|
||||
registerGroupLabel('media', '{{t("Media")}}');
|
||||
registerGroupLabel('datetime', '{{t("Date & Time")}}');
|
||||
registerGroupLabel('relation', '{{t("Relation")}}');
|
||||
registerGroupLabel('advanced', '{{t("Advanced type")}}');
|
||||
registerGroupLabel('systemInfo', '{{t("System info")}}');
|
||||
registerGroupLabel('others', '{{t("Others")}}');
|
||||
registerGroup('basic', '{{t("Basic")}}');
|
||||
registerGroup('choices', '{{t("Choices")}}');
|
||||
registerGroup('media', '{{t("Media")}}');
|
||||
registerGroup('datetime', '{{t("Date & Time")}}');
|
||||
registerGroup('relation', '{{t("Relation")}}');
|
||||
registerGroup('advanced', '{{t("Advanced type")}}');
|
||||
registerGroup('systemInfo', '{{t("System info")}}');
|
||||
registerGroup('others', '{{t("Others")}}');
|
||||
|
||||
export const getOptions = () => {
|
||||
return Object.keys(groupLabels).map((groupName) => {
|
||||
return {
|
||||
label: groupLabels[groupName],
|
||||
key: groupName,
|
||||
children: Object.keys(fields[groupName] || {})
|
||||
.map((type) => {
|
||||
const field = fields[groupName][type];
|
||||
return {
|
||||
value: type,
|
||||
label: field.title,
|
||||
name: type,
|
||||
...fields[groupName][type],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.order - b.order),
|
||||
};
|
||||
});
|
||||
return Object.keys(groups)
|
||||
.map((groupName) => {
|
||||
const group = groups[groupName];
|
||||
return {
|
||||
...group,
|
||||
key: groupName,
|
||||
children: Object.keys(fields[groupName] || {})
|
||||
.map((type) => {
|
||||
const field = fields[groupName][type];
|
||||
return {
|
||||
value: type,
|
||||
label: field.title,
|
||||
name: type,
|
||||
...fields[groupName][type],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.order - b.order),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.order - b.order);
|
||||
};
|
||||
|
||||
export const options = getOptions();
|
||||
|
@ -6,7 +6,7 @@ export * from './CollectionManagerSchemaComponentProvider';
|
||||
export * from './CollectionManagerShortcut';
|
||||
export * from './CollectionProvider';
|
||||
export * from './Configuration';
|
||||
export { registerField, registerGroupLabel } from './Configuration/interfaces';
|
||||
export { registerField, registerGroup, registerGroupLabel } from './Configuration/interfaces';
|
||||
export * from './context';
|
||||
export * from './hooks';
|
||||
export * as interfacesProperties from './interfaces/properties';
|
||||
@ -15,4 +15,3 @@ export * from './ResourceActionProvider';
|
||||
export { getConfigurableProperties } from './templates/properties';
|
||||
export * from './templates/types';
|
||||
export * from './types';
|
||||
|
||||
|
4
packages/plugins/map/client.d.ts
vendored
Executable file
4
packages/plugins/map/client.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/client';
|
||||
export { default } from './lib/client';
|
||||
|
30
packages/plugins/map/client.js
Executable file
30
packages/plugins/map/client.js
Executable 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];
|
||||
}
|
||||
});
|
||||
});
|
18
packages/plugins/map/package.json
Normal file
18
packages/plugins/map/package.json
Normal 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
4
packages/plugins/map/server.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/server';
|
||||
export { default } from './lib/server';
|
||||
|
30
packages/plugins/map/server.js
Executable file
30
packages/plugins/map/server.js
Executable 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];
|
||||
}
|
||||
});
|
||||
});
|
343
packages/plugins/map/src/client/components/AMap.tsx
Normal file
343
packages/plugins/map/src/client/components/AMap.tsx
Normal 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;
|
91
packages/plugins/map/src/client/components/Configuration.tsx
Normal file
91
packages/plugins/map/src/client/components/Configuration.tsx
Normal 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;
|
260
packages/plugins/map/src/client/components/Designer.tsx
Normal file
260
packages/plugins/map/src/client/components/Designer.tsx
Normal 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;
|
29
packages/plugins/map/src/client/components/Map.tsx
Normal file
29
packages/plugins/map/src/client/components/Map.tsx
Normal 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;
|
34
packages/plugins/map/src/client/components/ReadPretty.tsx
Normal file
34
packages/plugins/map/src/client/components/ReadPretty.tsx
Normal 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;
|
93
packages/plugins/map/src/client/components/Search.tsx
Normal file
93
packages/plugins/map/src/client/components/Search.tsx
Normal 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;
|
6
packages/plugins/map/src/client/constants.ts
Normal file
6
packages/plugins/map/src/client/constants.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { generateNTemplate } from "./locales";
|
||||
|
||||
export const MapTypes = [
|
||||
{ label: generateNTemplate('AMap'), value: 'amap' },
|
||||
{ label: generateNTemplate('Google Maps'), value: 'google' },
|
||||
]
|
23
packages/plugins/map/src/client/fields/circle.ts
Normal file
23
packages/plugins/map/src/client/fields/circle.ts
Normal 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,
|
||||
};
|
16
packages/plugins/map/src/client/fields/index.ts
Normal file
16
packages/plugins/map/src/client/fields/index.ts
Normal 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
|
||||
}, {})
|
23
packages/plugins/map/src/client/fields/lineString.ts
Normal file
23
packages/plugins/map/src/client/fields/lineString.ts
Normal 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,
|
||||
};
|
24
packages/plugins/map/src/client/fields/point.ts
Normal file
24
packages/plugins/map/src/client/fields/point.ts
Normal 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,
|
||||
};
|
23
packages/plugins/map/src/client/fields/polygon.ts
Normal file
23
packages/plugins/map/src/client/fields/polygon.ts
Normal 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,
|
||||
};
|
57
packages/plugins/map/src/client/fields/schema.ts
Normal file
57
packages/plugins/map/src/client/fields/schema.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
1
packages/plugins/map/src/client/hooks/index.ts
Normal file
1
packages/plugins/map/src/client/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './useMapConfiguration'
|
15
packages/plugins/map/src/client/hooks/useMapConfiguration.ts
Normal file
15
packages/plugins/map/src/client/hooks/useMapConfiguration.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
43
packages/plugins/map/src/client/index.tsx
Normal file
43
packages/plugins/map/src/client/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
33
packages/plugins/map/src/client/initialize.tsx
Normal file
33
packages/plugins/map/src/client/initialize.tsx
Normal 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>;
|
||||
};
|
5
packages/plugins/map/src/client/locales/en-US.ts
Normal file
5
packages/plugins/map/src/client/locales/en-US.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const locale = {
|
||||
|
||||
}
|
||||
|
||||
export default locale;
|
22
packages/plugins/map/src/client/locales/index.ts
Normal file
22
packages/plugins/map/src/client/locales/index.ts
Normal 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);
|
||||
}
|
45
packages/plugins/map/src/client/locales/zh-CN.ts
Normal file
45
packages/plugins/map/src/client/locales/zh-CN.ts
Normal 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;
|
1
packages/plugins/map/src/index.ts
Normal file
1
packages/plugins/map/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './server';
|
163
packages/plugins/map/src/server/__tests__/fields.test.ts
Normal file
163
packages/plugins/map/src/server/__tests__/fields.test.ts
Normal 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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
46
packages/plugins/map/src/server/actions/index.ts
Normal file
46
packages/plugins/map/src/server/actions/index.ts
Normal 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();
|
||||
};
|
@ -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
|
1
packages/plugins/map/src/server/constants.ts
Normal file
1
packages/plugins/map/src/server/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const MapConfigurationCollectionName = 'mapConfiguration';
|
0
packages/plugins/map/src/server/fields/.gitkeep
Normal file
0
packages/plugins/map/src/server/fields/.gitkeep
Normal file
50
packages/plugins/map/src/server/fields/circle.ts
Normal file
50
packages/plugins/map/src/server/fields/circle.ts
Normal 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';
|
||||
}
|
4
packages/plugins/map/src/server/fields/index.ts
Normal file
4
packages/plugins/map/src/server/fields/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './point'
|
||||
export * from './lineString'
|
||||
export * from './polygon'
|
||||
export * from './circle'
|
56
packages/plugins/map/src/server/fields/lineString.ts
Normal file
56
packages/plugins/map/src/server/fields/lineString.ts
Normal 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';
|
||||
}
|
59
packages/plugins/map/src/server/fields/point.ts
Normal file
59
packages/plugins/map/src/server/fields/point.ts
Normal 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';
|
||||
}
|
56
packages/plugins/map/src/server/fields/polygon.ts
Normal file
56
packages/plugins/map/src/server/fields/polygon.ts
Normal 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';
|
||||
}
|
25
packages/plugins/map/src/server/helpers/index.ts
Normal file
25
packages/plugins/map/src/server/helpers/index.ts
Normal 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';
|
||||
};
|
1
packages/plugins/map/src/server/index.ts
Normal file
1
packages/plugins/map/src/server/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './plugin';
|
46
packages/plugins/map/src/server/plugin.ts
Normal file
46
packages/plugins/map/src/server/plugin.ts
Normal 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;
|
@ -29,6 +29,7 @@
|
||||
"@nocobase/plugin-users": "0.8.0-alpha.13",
|
||||
"@nocobase/plugin-verification": "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"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -6,28 +6,28 @@ export class PresetNocoBase extends Plugin {
|
||||
const builtInPlugins = process.env.PRESET_NOCOBASE_PLUGINS
|
||||
? process.env.PRESET_NOCOBASE_PLUGINS.split(',')
|
||||
: [
|
||||
'error-handler',
|
||||
'collection-manager',
|
||||
'ui-schema-storage',
|
||||
'ui-routes-storage',
|
||||
'file-manager',
|
||||
'system-settings',
|
||||
'verification',
|
||||
'users',
|
||||
'acl',
|
||||
'china-region',
|
||||
'workflow',
|
||||
'client',
|
||||
'export',
|
||||
'import',
|
||||
'audit-logs',
|
||||
];
|
||||
'error-handler',
|
||||
'collection-manager',
|
||||
'ui-schema-storage',
|
||||
'ui-routes-storage',
|
||||
'file-manager',
|
||||
'system-settings',
|
||||
'verification',
|
||||
'users',
|
||||
'acl',
|
||||
'china-region',
|
||||
'workflow',
|
||||
'client',
|
||||
'export',
|
||||
'import',
|
||||
'audit-logs',
|
||||
];
|
||||
await this.app.pm.add(builtInPlugins, {
|
||||
enabled: true,
|
||||
builtIn: 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.reload();
|
||||
}
|
||||
|
10
yarn.lock
10
yarn.lock
@ -97,6 +97,16 @@
|
||||
"@types/xml2js" "^0.4.5"
|
||||
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":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
|
||||
|
Loading…
Reference in New Issue
Block a user