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 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) => {
|
||||||
|
@ -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,
|
||||||
}}
|
}}
|
||||||
|
@ -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,28 +19,39 @@ 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)
|
||||||
|
.map((groupName) => {
|
||||||
|
const group = groups[groupName];
|
||||||
return {
|
return {
|
||||||
label: groupLabels[groupName],
|
...group,
|
||||||
key: groupName,
|
key: groupName,
|
||||||
children: Object.keys(fields[groupName] || {})
|
children: Object.keys(fields[groupName] || {})
|
||||||
.map((type) => {
|
.map((type) => {
|
||||||
@ -48,7 +65,8 @@ export const getOptions = () => {
|
|||||||
})
|
})
|
||||||
.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();
|
||||||
|
@ -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
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-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": {
|
||||||
|
@ -27,7 +27,7 @@ export class PresetNocoBase extends Plugin {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user