Merge branch 'develop' into 'master'
feat(zhst/meta,zhst/map): zhst/map:拓展地图api,加入tools组件、cluster组价、marker组件 See merge request web-project/zhst-lambo!34
3
.vscode/settings.json
vendored
@ -10,6 +10,9 @@
|
||||
"flvjs",
|
||||
"indicatorsize",
|
||||
"lambo",
|
||||
"mapbox",
|
||||
"maxzoom",
|
||||
"minzoom",
|
||||
"remuxer",
|
||||
"stylelint",
|
||||
"transmuxer",
|
||||
|
@ -1,40 +1,81 @@
|
||||
---
|
||||
hero:
|
||||
title: lambo
|
||||
description: 致力于提升前端开发效率与规范
|
||||
description: 致力于提升前端开发效率与规范(开发前请先阅读开发流程)
|
||||
actions:
|
||||
- text: 快速上手
|
||||
link: /bizs
|
||||
features:
|
||||
- title: biz
|
||||
emoji: 🍑
|
||||
description: 业务库
|
||||
- title: hooks
|
||||
emoji: 💎
|
||||
description: hooks
|
||||
- title: func
|
||||
emoji: 🌈
|
||||
description: 常用函数库
|
||||
- title: meta
|
||||
emoji: ☀️
|
||||
description: 原子组件库
|
||||
- title: constants
|
||||
emoji: 🈶️
|
||||
description: 静态定义库
|
||||
- title: request
|
||||
emoji: 🥣
|
||||
description: 网络请求库
|
||||
- title: types
|
||||
emoji: 🈸
|
||||
description: typescript 声明库
|
||||
- title: material
|
||||
emoji: 🥱
|
||||
description: 物料库
|
||||
- title: cli
|
||||
emoji: 🐔
|
||||
description: 脚手架
|
||||
# features:
|
||||
# - title: biz
|
||||
# emoji: 🍑
|
||||
# description: 业务库
|
||||
# - title: hooks
|
||||
# emoji: 💎
|
||||
# description: hooks
|
||||
# - title: func
|
||||
# emoji: 🌈
|
||||
# description: 常用函数库
|
||||
# - title: meta
|
||||
# emoji: ☀️
|
||||
# description: 原子组件库
|
||||
# - title: constants
|
||||
# emoji: 🈶️
|
||||
# description: 静态定义库
|
||||
# - title: request
|
||||
# emoji: 🥣
|
||||
# description: 网络请求库
|
||||
# - title: types
|
||||
# emoji: 🈸
|
||||
# description: typescript 声明库
|
||||
# - title: material
|
||||
# emoji: 🥱
|
||||
# description: 物料库
|
||||
# - title: cli
|
||||
# emoji: 🐔
|
||||
# description: 脚手架
|
||||
---
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 1. 确定需求
|
||||
|
||||
从 gitlab 上的 [issue](http://10.0.0.88/web-project/zhst-lambo/boards) 模块找到对应的需求。将 Assignee 负责人指派为自己(如果多人协同开发可以将需求拆分为多个需求,分别指派),然后将 Labels 标签改为 doing 状态。(截止日期选填)
|
||||
|
||||
> issuse 命名规则:@zhst/{包名} - {模块名},然后在详情页描述对应需求。
|
||||
|
||||
### 2. 创建 git 分支
|
||||
|
||||
按照 git flow 规范从 [master](http://10.0.0.88/web-project/zhst-lambo) 上创建分支, 分支的命名规则参考:
|
||||
|
||||
1. feat/XXX: 需求新增
|
||||
2. hotfix/XXX: bug 修复
|
||||
|
||||
### 3. 开始开发
|
||||
|
||||
进入项目文件夹,在 packages 下找到对应的 npm 包, 然后在 src 目录下按已有的格式进行开发,如果是功能变更就找到对应的页面进行修改
|
||||
|
||||
### 4. 提交代码,并提交 mr 到 develop 分支
|
||||
|
||||
完成开发后,给代码提交 commit,格式参考 ${行为}(${影响范围}): ${变更内容}, 例如:
|
||||
|
||||
> feat(package.json): 修改版本号
|
||||
> fix(app.ts): 修改环境变量
|
||||
|
||||
对应的变更会在最终的 npm 包版本号体现:a.b.c - a 对应重构(一般用不上) - b 对应 feat(功能新增) - c 对应 hotfix(一般是 bug 修复)
|
||||
|
||||
push 完代码之后,在 gitlab 上提交一个 mr 到 develop 分支,指定给对应的人员审核(@江志雄),合并成功之后,将 [issue](http://10.0.0.88/web-project/zhst-lambo/boards) 对应的 Labels 状态改为 waittingPublish。
|
||||
|
||||
### 5. 发布成功
|
||||
|
||||
发布成功之后,会有两个行为:
|
||||
|
||||
1. 在钉钉群通知发布成功。
|
||||
2. 生成线上预览[说明文档](http://10.0.0.204:30080)
|
||||
|
||||
一旦触发了钉钉通知,则需要去到 [issue](http://10.0.0.88/web-project/zhst-lambo/boards) 板块将对应的需求 **close** 掉。
|
||||
|
||||
这就是 npm 包整个开发链路。
|
||||
|
||||
## 目录结构
|
||||
|
||||
<Tree>
|
||||
|
3
global.d.ts
vendored
@ -1 +1,4 @@
|
||||
declare module '*.less';
|
||||
declare module '*.png';
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
|
@ -1,5 +1,16 @@
|
||||
# @zhst/map
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- zhst/map:拓展地图 api,加入 tools 组件、cluster 组价、marker 组件、draw 组件、popup 组件;zhst/mata:添加 floatButton 组件
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @zhst/meta@0.22.0
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
## 介绍
|
||||
|
||||
地图库,基于map-box开发
|
||||
地图库,基于map-box开发,配合 @turf/turf 工具能达到地图交互体验感最大化。
|
||||
|
||||
设计的初衷是将业务中常见的一些模块抽离成组件的形式。在不影响地图原有的api的情况下,进行增量定制化开发。
|
||||
|
||||
## 安装
|
||||
|
||||
|
@ -1,52 +1,195 @@
|
||||
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
||||
var _excluded = ["style", "children", "mapRef", "onLoad", "mapCenter", "mapConfig"];
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
var _excluded = ["style", "children", "height", "width", "draw", "markerData", "sluterData", "popUpInfo", "showPopUp", "toolsBarOpen", "showMarker", "showCluster", "clusterProps", "interactiveLayerIds", "buttonList", "onMarkerClick", "onPopUpClose", "customMarkerRender", "onLoad", "onDrawCreate", "onDrawUpdate", "onDrawDelete", "onToolClick"];
|
||||
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
|
||||
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
||||
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
|
||||
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
|
||||
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
||||
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
||||
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
||||
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
|
||||
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
||||
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
||||
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
||||
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
|
||||
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
||||
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
||||
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
|
||||
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import Map from 'react-map-gl';
|
||||
import classnames from 'classnames';
|
||||
import { merge } from '@zhst/func';
|
||||
import Tools from "./components/tools";
|
||||
import DrawControl from "./components/drawControl";
|
||||
import { defaultMapConfig } from "./utils/constants";
|
||||
import "./index.less";
|
||||
import React from 'react';
|
||||
import { merge } from "./utils";
|
||||
import { MAP_CENTER, defaultMapConfig } from "./constants";
|
||||
var MapBox = function MapBox(props) {
|
||||
import mapboxDrawStyle from "./utils/drawStyle";
|
||||
import Marker from "./components/marker";
|
||||
import PopUp from "./components/popup";
|
||||
import Cluster from "./components/clusters/Clusters";
|
||||
import { clusterLayer } from "./components/clusters/layers";
|
||||
var componentName = 'zhst-map';
|
||||
var MapBox = /*#__PURE__*/forwardRef(function (props, ref) {
|
||||
var _ref = props || {},
|
||||
_ref$style = _ref.style,
|
||||
style = _ref$style === void 0 ? {} : _ref$style,
|
||||
children = _ref.children,
|
||||
mapRef = _ref.mapRef,
|
||||
_onLoad = _ref.onLoad,
|
||||
_ref$mapCenter = _ref.mapCenter,
|
||||
mapCenter = _ref$mapCenter === void 0 ? MAP_CENTER : _ref$mapCenter,
|
||||
_ref$mapConfig = _ref.mapConfig,
|
||||
mapConfig = _ref$mapConfig === void 0 ? {} : _ref$mapConfig,
|
||||
_ref$height = _ref.height,
|
||||
height = _ref$height === void 0 ? 600 : _ref$height,
|
||||
_ref$width = _ref.width,
|
||||
width = _ref$width === void 0 ? '100%' : _ref$width,
|
||||
draw = _ref.draw,
|
||||
_ref$markerData = _ref.markerData,
|
||||
markerData = _ref$markerData === void 0 ? [] : _ref$markerData,
|
||||
_ref$sluterData = _ref.sluterData,
|
||||
sluterData = _ref$sluterData === void 0 ? [] : _ref$sluterData,
|
||||
_ref$popUpInfo = _ref.popUpInfo,
|
||||
popUpInfo = _ref$popUpInfo === void 0 ? {
|
||||
longitude: 0,
|
||||
latitude: 0
|
||||
} : _ref$popUpInfo,
|
||||
showPopUp = _ref.showPopUp,
|
||||
toolsBarOpen = _ref.toolsBarOpen,
|
||||
_ref$showMarker = _ref.showMarker,
|
||||
showMarker = _ref$showMarker === void 0 ? true : _ref$showMarker,
|
||||
_ref$showCluster = _ref.showCluster,
|
||||
showCluster = _ref$showCluster === void 0 ? false : _ref$showCluster,
|
||||
clusterProps = _ref.clusterProps,
|
||||
_ref$interactiveLayer = _ref.interactiveLayerIds,
|
||||
interactiveLayerIds = _ref$interactiveLayer === void 0 ? [] : _ref$interactiveLayer,
|
||||
_ref$buttonList = _ref.buttonList,
|
||||
buttonList = _ref$buttonList === void 0 ? [{
|
||||
label: '圆形框选',
|
||||
key: 'circle',
|
||||
icon: 'icon-yuan',
|
||||
onClick: function onClick() {
|
||||
var _drawControlRef$curre, _drawControlRef$curre2;
|
||||
return (_drawControlRef$curre = drawControlRef.current) === null || _drawControlRef$curre === void 0 || (_drawControlRef$curre = _drawControlRef$curre.drawer) === null || _drawControlRef$curre === void 0 || (_drawControlRef$curre2 = _drawControlRef$curre.changeMode) === null || _drawControlRef$curre2 === void 0 ? void 0 : _drawControlRef$curre2.call(_drawControlRef$curre, 'simple_select');
|
||||
}
|
||||
}, {
|
||||
label: '矩形框选',
|
||||
key: 'rect',
|
||||
icon: 'icon-fang',
|
||||
onClick: function onClick() {
|
||||
var _drawControlRef$curre3, _drawControlRef$curre4;
|
||||
return (_drawControlRef$curre3 = drawControlRef.current) === null || _drawControlRef$curre3 === void 0 || (_drawControlRef$curre3 = _drawControlRef$curre3.drawer) === null || _drawControlRef$curre3 === void 0 || (_drawControlRef$curre4 = _drawControlRef$curre3.changeMode) === null || _drawControlRef$curre4 === void 0 ? void 0 : _drawControlRef$curre4.call(_drawControlRef$curre3, 'draw_rect');
|
||||
},
|
||||
popoverProps: {
|
||||
placement: 'bottom',
|
||||
content: '自定义内容'
|
||||
}
|
||||
}, {
|
||||
label: '多边形框选',
|
||||
key: 'more',
|
||||
icon: 'icon-duobianxing',
|
||||
onClick: function onClick() {
|
||||
var _drawControlRef$curre5, _drawControlRef$curre6;
|
||||
return (_drawControlRef$curre5 = drawControlRef.current) === null || _drawControlRef$curre5 === void 0 || (_drawControlRef$curre5 = _drawControlRef$curre5.drawer) === null || _drawControlRef$curre5 === void 0 || (_drawControlRef$curre6 = _drawControlRef$curre5.changeMode) === null || _drawControlRef$curre6 === void 0 ? void 0 : _drawControlRef$curre6.call(_drawControlRef$curre5, 'draw_polygon');
|
||||
}
|
||||
}, {
|
||||
label: '路径框选',
|
||||
key: 'path',
|
||||
icon: 'icon-lujingkuangxuannor',
|
||||
onClick: function onClick() {
|
||||
var _drawControlRef$curre7, _drawControlRef$curre8;
|
||||
return (_drawControlRef$curre7 = drawControlRef.current) === null || _drawControlRef$curre7 === void 0 || (_drawControlRef$curre7 = _drawControlRef$curre7.drawer) === null || _drawControlRef$curre7 === void 0 || (_drawControlRef$curre8 = _drawControlRef$curre7.changeMode) === null || _drawControlRef$curre8 === void 0 ? void 0 : _drawControlRef$curre8.call(_drawControlRef$curre7, 'draw_line_string');
|
||||
}
|
||||
}, {
|
||||
label: '测距',
|
||||
key: 'path',
|
||||
icon: 'icon-ceju',
|
||||
onClick: function onClick() {
|
||||
var _drawControlRef$curre9, _drawControlRef$curre10;
|
||||
return (_drawControlRef$curre9 = drawControlRef.current) === null || _drawControlRef$curre9 === void 0 || (_drawControlRef$curre9 = _drawControlRef$curre9.drawer) === null || _drawControlRef$curre9 === void 0 || (_drawControlRef$curre10 = _drawControlRef$curre9.changeMode) === null || _drawControlRef$curre10 === void 0 ? void 0 : _drawControlRef$curre10.call(_drawControlRef$curre9, 'draw_line_string');
|
||||
}
|
||||
}, {
|
||||
label: '清除',
|
||||
key: 'clear',
|
||||
icon: 'icon-gongjuxiangguanbi',
|
||||
onClick: function onClick() {
|
||||
var _drawControlRef$curre11;
|
||||
return (_drawControlRef$curre11 = drawControlRef.current) === null || _drawControlRef$curre11 === void 0 || (_drawControlRef$curre11 = _drawControlRef$curre11.drawer) === null || _drawControlRef$curre11 === void 0 ? void 0 : _drawControlRef$curre11.deleteAll();
|
||||
}
|
||||
}] : _ref$buttonList,
|
||||
onMarkerClick = _ref.onMarkerClick,
|
||||
onPopUpClose = _ref.onPopUpClose,
|
||||
customMarkerRender = _ref.customMarkerRender,
|
||||
onLoad = _ref.onLoad,
|
||||
onDrawCreate = _ref.onDrawCreate,
|
||||
onDrawUpdate = _ref.onDrawUpdate,
|
||||
onDrawDelete = _ref.onDrawDelete,
|
||||
onToolClick = _ref.onToolClick,
|
||||
others = _objectWithoutProperties(_ref, _excluded);
|
||||
var mapRef = useRef(null);
|
||||
var drawControlRef = useRef(null);
|
||||
|
||||
// 默认绘制配置
|
||||
var _useState = useState({
|
||||
displayControlsDefault: false,
|
||||
position: 'top-left',
|
||||
styles: mapboxDrawStyle,
|
||||
// Select which mapbox-gl-draw control buttons to add to the map.
|
||||
// @ts-ignore
|
||||
controls: false,
|
||||
// The user does not have to click the polygon control button first.
|
||||
defaultMode: 'draw_polygon'
|
||||
}),
|
||||
_useState2 = _slicedToArray(_useState, 1),
|
||||
drawConfig = _useState2[0];
|
||||
var initMarker = useMemo(function () {
|
||||
return markerData.map(function (_item) {
|
||||
return /*#__PURE__*/React.createElement(Marker, _extends({
|
||||
customMarkerRender: customMarkerRender,
|
||||
onMarkerClick: onMarkerClick
|
||||
}, _item));
|
||||
});
|
||||
}, [markerData]);
|
||||
useImperativeHandle(ref, function () {
|
||||
var _drawControlRef$curre12;
|
||||
return {
|
||||
mapRef: mapRef.current,
|
||||
drawer: (_drawControlRef$curre12 = drawControlRef.current) === null || _drawControlRef$curre12 === void 0 ? void 0 : _drawControlRef$curre12.drawer
|
||||
};
|
||||
});
|
||||
return (
|
||||
/*#__PURE__*/
|
||||
//@ts-ignore
|
||||
React.createElement(Map, _extends({
|
||||
ref: function ref(e) {
|
||||
if (mapRef) {
|
||||
mapRef.current = e;
|
||||
}
|
||||
},
|
||||
onLoad: function onLoad(e) {
|
||||
_onLoad && _onLoad(e);
|
||||
},
|
||||
React.createElement("div", {
|
||||
className: classnames("".concat(componentName))
|
||||
}, /*#__PURE__*/React.createElement(Tools, {
|
||||
open: toolsBarOpen,
|
||||
buttonList: buttonList,
|
||||
onToolClick: onToolClick
|
||||
}), /*#__PURE__*/React.createElement(Map, _extends({
|
||||
ref: mapRef,
|
||||
onLoad: onLoad,
|
||||
style: _objectSpread({
|
||||
width: '100%',
|
||||
height: 600
|
||||
}, style)
|
||||
}, merge(defaultMapConfig, mapConfig), {
|
||||
initialViewState: _objectSpread(_objectSpread({}, mapCenter), {}, {
|
||||
zoom: 10
|
||||
})
|
||||
}, others), children)
|
||||
width: width,
|
||||
height: height
|
||||
}, style),
|
||||
interactiveLayerIds: [clusterLayer.id].concat(_toConsumableArray(interactiveLayerIds))
|
||||
}, merge(defaultMapConfig, others)), showMarker && initMarker, showPopUp && popUpInfo && /*#__PURE__*/React.createElement(PopUp, _extends({
|
||||
onClose: onPopUpClose
|
||||
}, popUpInfo)), showCluster && !showMarker && /*#__PURE__*/React.createElement(Cluster, _extends({
|
||||
type: "geojson",
|
||||
cluster: true,
|
||||
clusterMaxZoom: 14,
|
||||
clusterRadius: 50,
|
||||
data: sluterData
|
||||
}, clusterProps)), draw && /*#__PURE__*/React.createElement(DrawControl, _extends({
|
||||
ref: drawControlRef,
|
||||
onCreate: onDrawCreate,
|
||||
onUpdate: onDrawUpdate,
|
||||
onDelete: onDrawDelete,
|
||||
onSelectionChange: function onSelectionChange(e) {
|
||||
return console.log('e', e);
|
||||
}
|
||||
}, drawConfig)), children))
|
||||
);
|
||||
};
|
||||
});
|
||||
export default MapBox;
|
@ -1,47 +0,0 @@
|
||||
export var mapboxAccessToken = 'pk.eyJ1IjoiZGluZ2xpMTIzIiwiYSI6ImNra204ODhjczBobTgyeHJ6MmJpZHMxNWgifQ.NbKrXh_hb2gvjr5CEMDnyQ';
|
||||
export var MAP_CENTER = {
|
||||
longitude: 120.2667694313269,
|
||||
latitude: 30.180942826533766
|
||||
}; //地图中心
|
||||
var MapUrl = 'http://10.0.0.120:30003/map';
|
||||
export var defaultMapConfig = {
|
||||
mapboxAccessToken: mapboxAccessToken,
|
||||
maxZoom: 18,
|
||||
minZoom: 4,
|
||||
dragRotate: false,
|
||||
mapStyle: {
|
||||
version: 8,
|
||||
name: 'Mapbox Streets',
|
||||
// sprite: `${location.origin}/mapbox/sprite`, // 地图图标
|
||||
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
||||
// 字体
|
||||
sources: {
|
||||
//数据源
|
||||
'osm-tiles': {
|
||||
type: 'raster',
|
||||
//栅格切片。vector:矢量切片
|
||||
// Z-瓦片层级,一般支持0-18级,越大代表越清晰;
|
||||
// X-瓦片列号,从西向东(0->360),依次0,1,2,……;
|
||||
// Y-瓦片行号,从北向南(有些也可能是从南向北),依次0,1,2,……;
|
||||
tiles: ["".concat(MapUrl, "/api/tilesets/mapfile/{z}/{x}/{y}.png")],
|
||||
//在线地址,先写死120
|
||||
tileSize: 256 //切片的最小展示尺寸(可选,单位:像素,默认值为 512,即 1024/2
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
// 图层。图层指定了如何渲染数据源提供的数据
|
||||
{
|
||||
id: 'zhstLayer',
|
||||
//唯一id
|
||||
type: 'raster',
|
||||
//类型 栅格。circle,symbol,line...
|
||||
source: 'osm-tiles',
|
||||
// 'source-layer': 'osmtiles',//数据源必须是type:vector
|
||||
minZoom: 4,
|
||||
//最小层级
|
||||
maxZoom: 17,
|
||||
//最大层级
|
||||
renderingMode: '2d'
|
||||
}]
|
||||
}
|
||||
};
|
@ -1 +1,2 @@
|
||||
export { default as MapBox } from "./MapBox";
|
||||
export { default as MapBox } from "./MapBox";
|
||||
export * from 'react-map-gl';
|
@ -1,3 +1,9 @@
|
||||
.mapboxgl-ctrl-attrib-button {
|
||||
display: none;
|
||||
.zhst-map {
|
||||
position: relative;
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
.mapboxgl-ctrl-logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export {};
|
@ -1,36 +0,0 @@
|
||||
var getRawType = function getRawType(val) {
|
||||
return Object.prototype.toString.call(val).slice(8, -1);
|
||||
};
|
||||
var isPlainObjectOrArray = function isPlainObjectOrArray(val) {
|
||||
return isPlainObject(val) || Array.isArray(val);
|
||||
};
|
||||
var isPlainObject = function isPlainObject(val) {
|
||||
return getRawType(val) === 'Object';
|
||||
};
|
||||
export var merge = function merge(object) {
|
||||
for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||
sources[_key - 1] = arguments[_key];
|
||||
}
|
||||
for (var _i = 0, _sources = sources; _i < _sources.length; _i++) {
|
||||
var source = _sources[_i];
|
||||
for (var key in source) {
|
||||
if (source[key] === undefined && key in object) {
|
||||
continue;
|
||||
}
|
||||
if (isPlainObjectOrArray(source[key])) {
|
||||
if (getRawType(object[key] === getRawType(source[key]))) {
|
||||
if (isPlainObject(object[key])) {
|
||||
merge(object[key], source[key]);
|
||||
} else {
|
||||
object[key] = object[key].concat(source[key]);
|
||||
}
|
||||
} else {
|
||||
object[key] = source[key];
|
||||
}
|
||||
} else {
|
||||
object[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@zhst/map",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"description": "地图库",
|
||||
"keywords": [
|
||||
"map",
|
||||
@ -37,7 +37,21 @@
|
||||
"registry": "http://10.0.0.77:4874"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-map-gl": "^7.1.7",
|
||||
"mapbox-gl": "^2.15.0"
|
||||
"@mapbox/mapbox-gl-draw": "^1.4.3",
|
||||
"@mapbox/mapbox-gl-draw-static-mode": "^1.0.1",
|
||||
"@turf/turf": "^6.5.0",
|
||||
"@zhst/hooks": "workspace:^0.13.1",
|
||||
"@zhst/icon": "workspace:^0.5.0",
|
||||
"@zhst/meta": "workspace:^",
|
||||
"classnames": "^2.5.1",
|
||||
"mapbox-gl": "^2.15.0",
|
||||
"mapbox-gl-draw-circle": "^1.1.2",
|
||||
"mapbox-gl-draw-geodesic": "^2.3.1",
|
||||
"mapbox-gl-draw-rectangle-mode": "^1.0.4",
|
||||
"react-map-gl": "^7.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mapbox__mapbox-gl-draw": "^1.4.6",
|
||||
"axios": "^1.7.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +1,218 @@
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import Map from 'react-map-gl';
|
||||
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import Map, { PopupEvent } from 'react-map-gl';
|
||||
import { CSSProperties } from "react";
|
||||
import { MapRef, MapStyle, MapProps as MapBoxProps } from "react-map-gl";
|
||||
import classnames from 'classnames'
|
||||
import { merge } from '@zhst/func'
|
||||
import Tools, { ToolsProps } from './components/tools'
|
||||
import DrawControl, { DrawControlProps, DrawControlRefProps } from './components/drawControl';
|
||||
import { defaultMapConfig } from './utils/constants';
|
||||
import './index.less';
|
||||
import React from 'react';
|
||||
import { MapProps } from './interface';
|
||||
import { merge } from './utils';
|
||||
import { MAP_CENTER, defaultMapConfig } from './constants';
|
||||
import mapboxDrawStyle from './utils/drawStyle';
|
||||
import Marker, { MarkerProps } from './components/marker';
|
||||
import PopUp, { PopUpProps } from './components/popup';
|
||||
import Cluster, { ClusterProps } from './components/clusters/Clusters';
|
||||
import { clusterLayer } from './components/clusters/layers';
|
||||
|
||||
const MapBox: React.FC<MapProps> = (props) => {
|
||||
const componentName = 'zhst-map'
|
||||
|
||||
export interface MapProps extends MapBoxProps {
|
||||
mapboxAccessToken?: string //token
|
||||
markerData?: MarkerProps[]
|
||||
minZoom?: number //最小层级
|
||||
maxZoom?: number //最大层级
|
||||
mapStyle?: MapStyle //地图样式
|
||||
height?: number | string
|
||||
width?: string | number
|
||||
mapRef?: MapRef
|
||||
style?: CSSProperties
|
||||
children?: JSX.Element | JSX.Element[] | Array<JSX.Element | undefined>
|
||||
sluterData?: any;
|
||||
draw?: boolean
|
||||
showMarker?: boolean // 显示标记点
|
||||
showCluster?: boolean // 显示范围统计
|
||||
buttonList?: ToolsProps['buttonList']
|
||||
popUpInfo?: PopUpProps
|
||||
showPopUp?: boolean
|
||||
clusterProps?: ClusterProps
|
||||
toolsBarOpen?: boolean
|
||||
customMarkerRender?: MarkerProps['customMarkerRender']
|
||||
onLoad?: (e: mapboxgl.MapboxEvent<undefined>) => void
|
||||
onDrawCreate?: (e: { features: object[], [key: string]: any }) => void
|
||||
onDrawUpdate?: (e: { features: object[], [key: string]: any }) => void
|
||||
onDrawDelete?: (e: { features: object[], [key: string]: any }) => void
|
||||
onMarkerClick?: MarkerProps['onMarkerClick']
|
||||
onPopUpClose?: (e: PopupEvent) => void;
|
||||
onToolClick?: ToolsProps['onToolClick']
|
||||
}
|
||||
|
||||
export interface MapRefProps {
|
||||
}
|
||||
|
||||
const MapBox = forwardRef<MapRefProps, MapProps>((props, ref) => {
|
||||
const {
|
||||
style = {},
|
||||
children,
|
||||
mapRef,
|
||||
height = 600,
|
||||
width = '100%',
|
||||
draw,
|
||||
markerData = [],
|
||||
sluterData = [],
|
||||
popUpInfo = {
|
||||
longitude: 0,
|
||||
latitude: 0
|
||||
},
|
||||
showPopUp,
|
||||
toolsBarOpen,
|
||||
showMarker = true,
|
||||
showCluster = false,
|
||||
clusterProps,
|
||||
interactiveLayerIds = [],
|
||||
buttonList = [
|
||||
{
|
||||
label: '圆形框选',
|
||||
key: 'circle',
|
||||
icon: 'icon-yuan',
|
||||
onClick: () => drawControlRef.current?.drawer?.changeMode?.('simple_select')
|
||||
},
|
||||
{
|
||||
label: '矩形框选',
|
||||
key: 'rect',
|
||||
icon: 'icon-fang',
|
||||
onClick: () => drawControlRef.current?.drawer?.changeMode?.('draw_rect'),
|
||||
popoverProps: {
|
||||
placement: 'bottom',
|
||||
content: '自定义内容'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '多边形框选',
|
||||
key: 'more',
|
||||
icon: 'icon-duobianxing',
|
||||
onClick: () => drawControlRef.current?.drawer?.changeMode?.('draw_polygon')
|
||||
},
|
||||
{
|
||||
label: '路径框选',
|
||||
key: 'path',
|
||||
icon: 'icon-lujingkuangxuannor',
|
||||
onClick: () => drawControlRef.current?.drawer?.changeMode?.('draw_line_string')
|
||||
},
|
||||
{
|
||||
label: '测距',
|
||||
key: 'path',
|
||||
icon: 'icon-ceju',
|
||||
onClick: () => drawControlRef.current?.drawer?.changeMode?.('draw_line_string')
|
||||
},
|
||||
{
|
||||
label: '清除',
|
||||
key: 'clear',
|
||||
icon: 'icon-gongjuxiangguanbi',
|
||||
onClick: () => drawControlRef.current?.drawer?.deleteAll()
|
||||
}
|
||||
],
|
||||
onMarkerClick,
|
||||
onPopUpClose,
|
||||
customMarkerRender,
|
||||
onLoad,
|
||||
mapCenter = MAP_CENTER,
|
||||
mapConfig = {},
|
||||
onDrawCreate,
|
||||
onDrawUpdate,
|
||||
onDrawDelete,
|
||||
onToolClick,
|
||||
...others
|
||||
} = props || {};
|
||||
const mapRef = useRef<MapRef>(null)
|
||||
const drawControlRef = useRef<DrawControlRefProps>(null)
|
||||
|
||||
// 默认绘制配置
|
||||
const [drawConfig] = useState<DrawControlProps>({
|
||||
displayControlsDefault: false,
|
||||
position: 'top-left',
|
||||
|
||||
styles: mapboxDrawStyle,
|
||||
// Select which mapbox-gl-draw control buttons to add to the map.
|
||||
// @ts-ignore
|
||||
controls: false,
|
||||
// The user does not have to click the polygon control button first.
|
||||
defaultMode: 'draw_polygon',
|
||||
})
|
||||
|
||||
const initMarker = useMemo(
|
||||
() => {
|
||||
return markerData.map((_item) => (
|
||||
<Marker
|
||||
customMarkerRender={customMarkerRender}
|
||||
onMarkerClick={onMarkerClick}
|
||||
{..._item}
|
||||
/>
|
||||
))
|
||||
}, [markerData]
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
mapRef: mapRef.current,
|
||||
drawer: drawControlRef.current?.drawer,
|
||||
}))
|
||||
|
||||
return (
|
||||
//@ts-ignore
|
||||
<Map
|
||||
ref={(e) => {
|
||||
if (mapRef) {
|
||||
mapRef.current = e!;
|
||||
}
|
||||
}}
|
||||
onLoad={(e) => {
|
||||
onLoad && onLoad(e);
|
||||
}}
|
||||
style={{ width: '100%', height: 600, ...style }}
|
||||
{...merge(defaultMapConfig, mapConfig)}
|
||||
initialViewState={{ ...mapCenter, zoom: 10 }}
|
||||
{...others}
|
||||
>
|
||||
{children}
|
||||
</Map>
|
||||
<div className={classnames(`${componentName}`)}>
|
||||
<Tools
|
||||
open={toolsBarOpen}
|
||||
buttonList={buttonList}
|
||||
onToolClick={onToolClick}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
<Map
|
||||
ref={mapRef}
|
||||
onLoad={onLoad}
|
||||
style={{ width: width, height: height, ...style }}
|
||||
interactiveLayerIds={[clusterLayer.id!, ...interactiveLayerIds]}
|
||||
{...merge(defaultMapConfig, others)}
|
||||
>
|
||||
{/* 标记点位 */}
|
||||
{showMarker && initMarker}
|
||||
|
||||
{/* 全局弹框弹框 */}
|
||||
{showPopUp && popUpInfo && (
|
||||
<PopUp
|
||||
onClose={onPopUpClose}
|
||||
{...popUpInfo}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* <CustomOverlay
|
||||
>
|
||||
<Button>自定义图层</Button>
|
||||
</CustomOverlay> */}
|
||||
|
||||
{/* 范围统计标点 */}
|
||||
{showCluster && !showMarker && (
|
||||
<Cluster
|
||||
type="geojson"
|
||||
cluster={true}
|
||||
clusterMaxZoom={14}
|
||||
clusterRadius={50}
|
||||
data={sluterData}
|
||||
{...clusterProps}
|
||||
/>
|
||||
)}
|
||||
{/* ---------------绘制图层--------------------- */}
|
||||
{draw && (
|
||||
<DrawControl
|
||||
ref={drawControlRef}
|
||||
onCreate={onDrawCreate}
|
||||
onUpdate={onDrawUpdate}
|
||||
onDelete={onDrawDelete}
|
||||
onSelectionChange={e => console.log('e', e)}
|
||||
{...drawConfig}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Map>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default MapBox;
|
||||
|
BIN
packages/map/src/assets/icons/camera_blue.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
packages/map/src/assets/icons/camera_green.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
packages/map/src/assets/icons/camera_grey.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
packages/map/src/assets/icons/camera_red.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
packages/map/src/assets/icons/camera_red_border.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
packages/map/src/assets/icons/camera_yellow.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
packages/map/src/assets/icons/cluster_blue.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
packages/map/src/assets/icons/cluster_red.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
packages/map/src/assets/icons/cluster_red_border.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
79
packages/map/src/assets/icons/cluterImage.json
Normal file
@ -0,0 +1,79 @@
|
||||
[{
|
||||
"key": "UNCLUTER",
|
||||
"state": [{
|
||||
"type": "NORMAL",
|
||||
"bgImage": "",
|
||||
"bgColor": "#0099ff",
|
||||
"frontImage": "",
|
||||
"frontColor": "red",
|
||||
"ripple": false,
|
||||
"redDot": false,
|
||||
"name": "IMAGE_UNCLUTER_NORMAL"
|
||||
},
|
||||
{
|
||||
"type": "RUNING",
|
||||
"backImage": "",
|
||||
"backColor": "green",
|
||||
"frontImage": "",
|
||||
"frontColor": "red",
|
||||
"ripple": false,
|
||||
"redDot": false,
|
||||
"name": "IMAGE_UNCLUTER_RUNING"
|
||||
},
|
||||
{
|
||||
"type": "NORMAL_SELECT",
|
||||
"backImage": "",
|
||||
"backColor": "yellow",
|
||||
"frontImage": "",
|
||||
"frontColor": "red",
|
||||
"ripple": false,
|
||||
"redDot": false,
|
||||
"name": "IMAGE_UNCLUTER_NORMAL_SELECT"
|
||||
},
|
||||
{
|
||||
"type": "RUNING_SELECT",
|
||||
"backImage": "",
|
||||
"backColor": "red",
|
||||
"frontImage": "",
|
||||
"frontColor": "blue",
|
||||
"ripple": false,
|
||||
"redDot": false,
|
||||
"name": "IMAGE_UNCLUTER_RUNING_SELECT"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "APPEARCLUTER_RED",
|
||||
"state": [{
|
||||
"type": "BLUE",
|
||||
"bgImage": "",
|
||||
"bgColor": "#0099ff",
|
||||
"frontImage": "",
|
||||
"frontColor": "#ffffff",
|
||||
"ripple": false,
|
||||
"redDot": false,
|
||||
"name": "IMAGE_APPEARCLUTER_BLUE"
|
||||
},
|
||||
{
|
||||
"type": "YELLOW",
|
||||
"bgImage": "",
|
||||
"bgColor": "#FF9900",
|
||||
"frontImage": "",
|
||||
"frontColor": "#ffffff",
|
||||
"ripple": false,
|
||||
"redDot": false,
|
||||
"name": "IMAGE_APPEARCLUTER_YELLOW"
|
||||
},
|
||||
{
|
||||
"type": "RED",
|
||||
"bgImage": "",
|
||||
"bgColor": "#F25051",
|
||||
"frontImage": "",
|
||||
"frontColor": "#ffffff",
|
||||
"ripple": false,
|
||||
"redDot": false,
|
||||
"name": "IMAGE_APPEARCLUTER_RED"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
187
packages/map/src/assets/icons/index.ts
Normal file
@ -0,0 +1,187 @@
|
||||
// @ts-nocheck
|
||||
//引入地图所所有点图片
|
||||
const file: { [key: string]: string } = {};
|
||||
|
||||
const modules = import.meta.glob<true, string, any>('./*.png', { eager: true });
|
||||
Object.keys(modules).forEach((key) => {
|
||||
const fileNmae = key.substring(key.lastIndexOf('/') + 1, key.lastIndexOf('.'));
|
||||
file[`${fileNmae}`] = modules[key].default;
|
||||
});
|
||||
|
||||
export const images: { [key: string]: string } = {};
|
||||
//图片点规则
|
||||
export const unClustersImgExpressions: mapboxgl.SymbolLayout['icon-image'] = ['case'];
|
||||
export const clustersImgExpressions: mapboxgl.SymbolLayout['icon-image'] = ['case'];
|
||||
export const feClustersImgExpressions: mapboxgl.SymbolLayout['icon-image'] = ['case'];
|
||||
|
||||
export const feclusterPrefix = 'IMG_FECLUSTER';
|
||||
export const unClusterPrefix = 'IMG_UNCLUSTER';
|
||||
export const clusterPrefix = 'IMG_CLUSTER';
|
||||
|
||||
Object.keys(file).forEach((fileNmae) => {
|
||||
const [prefix, type, status] = fileNmae.split('_');
|
||||
const key = `${type.toUpperCase()}${!!status ? `_${status.toUpperCase()}` : ''}`;
|
||||
if (prefix === 'marker') {
|
||||
const imageId = `${unClusterPrefix}_${key}`;
|
||||
images[imageId] = file[fileNmae];
|
||||
unClustersImgExpressions.push(['==', ['get', 'type'], key]);
|
||||
unClustersImgExpressions.push(imageId);
|
||||
}
|
||||
if (prefix === 'cluster') {
|
||||
const imageId = `${clusterPrefix}_${key}`;
|
||||
images[imageId] = file[fileNmae];
|
||||
clustersImgExpressions.push(['==', ['get', 'type'], key]);
|
||||
clustersImgExpressions.push(imageId);
|
||||
}
|
||||
if (prefix === 'fecluster') {
|
||||
const imageId = `${feclusterPrefix}_${key}`;
|
||||
images[imageId] = file[fileNmae];
|
||||
feClustersImgExpressions.push(['==', ['get', 'type'], key]);
|
||||
feClustersImgExpressions.push(imageId);
|
||||
}
|
||||
});
|
||||
|
||||
unClustersImgExpressions.push('IMG_UNCLUSTER_NORMAL');
|
||||
clustersImgExpressions.push('IMG_CLUSTER_NORMAL');
|
||||
feClustersImgExpressions.push('IMG_FECLUSTER_NORMAL');
|
||||
|
||||
export const imageData = [];
|
||||
export const converImageByJson = (jsonData) => {
|
||||
jsonData.map((v) => {
|
||||
const key = v['key'];
|
||||
v['state'].length > 0 &&
|
||||
v['state'].map((_v) => {
|
||||
const backWidth = 60;
|
||||
const backHeight = 70;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = backWidth;
|
||||
canvas.height = backHeight;
|
||||
const context = canvas.getContext('2d');
|
||||
//context.clearRect(0, 0, backWidth, backHeight);
|
||||
//context.beginPath();
|
||||
context.fillStyle = _v['bgColor'];
|
||||
|
||||
context.font = '50px IconFont';
|
||||
context.fillText(
|
||||
eval(('("' + _v['bgImage']).replace('&#x', '\\u').replace(';', '') + '")'),
|
||||
0,
|
||||
backWidth
|
||||
);
|
||||
context.fillStyle = _v['frontColor'];
|
||||
context.font = '50px IconFont';
|
||||
context.fillText(
|
||||
eval(('("' + _v['frontImage']).replace('&#x', '\\u').replace(';', '') + '")'),
|
||||
0,
|
||||
backWidth
|
||||
);
|
||||
|
||||
//画小红点;
|
||||
if (v['redDot']) {
|
||||
context.fillStyle = 'red';
|
||||
context.arc(20, 5, 3, Math.PI * 2, 0, true);
|
||||
context.fill();
|
||||
}
|
||||
const targetImage = new Image();
|
||||
targetImage.src = canvas.toDataURL('image/png');
|
||||
imageData.push({
|
||||
name: _v['name'],
|
||||
targetImage: targetImage,
|
||||
ripple: _v['ripple'],
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function BezierEllipse2(ctx, x, y, a, b) {
|
||||
ctx.beginPath();
|
||||
const k = 0.5522848;
|
||||
const ox = a * k; // 水平控制点偏移量
|
||||
const oy = b * k; // 垂直控制点偏移量</p> <p> ctx.beginPath();
|
||||
//从椭圆的左端点开始顺时针绘制四条三次贝塞尔曲线
|
||||
ctx.moveTo(x - a, y);
|
||||
ctx.bezierCurveTo(x - a, y - oy, x - ox, y - b, x, y - b);
|
||||
ctx.bezierCurveTo(x + ox, y - b, x + a, y - oy, x + a, y);
|
||||
ctx.bezierCurveTo(x + a, y + oy, x + ox, y + b, x, y + b);
|
||||
ctx.bezierCurveTo(x - ox, y + b, x - a, y + oy, x - a, y);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function rgbToRgba(color, alp) {
|
||||
const rgbaAttr = color.match(/[\d.]+/g);
|
||||
if (rgbaAttr.length >= 3) {
|
||||
const [r, g, b] = rgbaAttr;
|
||||
// r = rgbaAttr[0];
|
||||
// g = rgbaAttr[1];
|
||||
// b = rgbaAttr[2];
|
||||
return 'rgba(' + r + ',' + g + ',' + b + ',' + alp + ')';
|
||||
}
|
||||
}
|
||||
export function convertImageToMap(name, map) {
|
||||
const size = 150;
|
||||
|
||||
const imageObject = imageData.find((v) => v['name'] === name);
|
||||
const initialImage = imageObject.targetImage;
|
||||
const hasRipper = imageObject['ripple'];
|
||||
const pulsingDot = {
|
||||
width: size,
|
||||
height: size,
|
||||
data: new Uint8Array(size * size * 4),
|
||||
|
||||
onAdd: function () {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = this.width;
|
||||
canvas.height = this.height;
|
||||
this.canvas = canvas;
|
||||
this.context = canvas.getContext('2d');
|
||||
},
|
||||
|
||||
render: function () {
|
||||
const duration = 1.5 * 1000; //1.5s
|
||||
const t = (performance.now() % duration) / duration;
|
||||
const alp = 1 - t;
|
||||
// const size = 10;
|
||||
|
||||
const outerRadius = (size / 2) * 1 * t;
|
||||
const outerRadius2 = (size / 2) * 0.6 * t;
|
||||
const context = this.context;
|
||||
// draw outer circle
|
||||
context.clearRect(0, 0, this.width, this.height);
|
||||
|
||||
context.beginPath();
|
||||
context.drawImage(initialImage, 0, 0, this.width, this.height);
|
||||
// context.drawImage('./cluster_escape.png', this.width / 2, this.height / 2, this.width / 2, this.height / 2);
|
||||
// // context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2);
|
||||
if (hasRipper) {
|
||||
BezierEllipse2(context, this.width / 2, this.height / 2, outerRadius, outerRadius / 2);
|
||||
context.fillStyle = rgbToRgba('rgb(255, 100, 100)', alp);
|
||||
context.fill();
|
||||
|
||||
context.beginPath();
|
||||
// context.arc(this.width / 2, this.height / 2, outerRadius2, 0, Math.PI * 2);
|
||||
BezierEllipse2(context, this.width / 2, this.height / 2, outerRadius2, outerRadius2 / 2);
|
||||
context.fillStyle = rgbToRgba('rgb(255, 100, 100)', alp);
|
||||
context.fill();
|
||||
|
||||
// draw inner circle
|
||||
context.beginPath();
|
||||
BezierEllipse2(
|
||||
context,
|
||||
this.width / 2,
|
||||
this.height / 2,
|
||||
(size / 2) * 0.2,
|
||||
((size / 2) * 0.2) / 2
|
||||
);
|
||||
// context.arc(this.width / 2, this.height / 2, (size / 2) * 1 * 0.1, 0, Math.PI * 2);
|
||||
// context.fillStyle = 'rgba(255, 100, 100, 1)';
|
||||
context.fillStyle = rgbToRgba('rgb(255, 100, 100)', 1);
|
||||
context.fill();
|
||||
}
|
||||
|
||||
this.data = context.getImageData(0, 0, this.width, this.height).data;
|
||||
|
||||
map.triggerRepaint();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
return pulsingDot;
|
||||
}
|
BIN
packages/map/src/assets/icons/marker_blue.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
packages/map/src/assets/icons/marker_escape.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
packages/map/src/assets/icons/marker_escape_border.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
packages/map/src/assets/icons/marker_green.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packages/map/src/assets/icons/marker_red_border.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packages/map/src/assets/icons/marker_red_track.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/map/src/assets/icons/marker_yellow.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
38
packages/map/src/components/clusters/Clusters.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/23
|
||||
*/
|
||||
import React, { forwardRef, useImperativeHandle } from 'react'
|
||||
import { Source, Layer, SourceProps } from 'react-map-gl';
|
||||
import { clusterLayer, clusterCountLayer } from './layers';
|
||||
|
||||
// const { ConfigContext } = ConfigProvider
|
||||
|
||||
export interface ClusterProps {
|
||||
prefixCls?: string;
|
||||
}
|
||||
|
||||
export interface ClusterRefProps {
|
||||
}
|
||||
|
||||
const Cluster = forwardRef<ClusterRefProps, ClusterProps & SourceProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
...rest
|
||||
} = props
|
||||
// const { getPrefixCls } = useContext(ConfigContext)
|
||||
// const componentName = getPrefixCls('map-cluster', customizePrefixCls);
|
||||
|
||||
useImperativeHandle(ref, () => ({}))
|
||||
|
||||
return (
|
||||
<Source
|
||||
{...rest}
|
||||
>
|
||||
<Layer {...clusterLayer} />
|
||||
<Layer {...clusterCountLayer} />
|
||||
{/* <Layer {...unclusteredPointLayer} /> */}
|
||||
</Source>
|
||||
)
|
||||
})
|
||||
|
||||
export default Cluster
|
7
packages/map/src/components/clusters/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/23
|
||||
*/
|
||||
import Clusters from './Clusters'
|
||||
// export type { ClustersProps, ClustersRefProps } from './Clusters'
|
||||
|
||||
export default Clusters
|
327
packages/map/src/components/clusters/layers.ts
Normal file
@ -0,0 +1,327 @@
|
||||
import type { LayerProps } from 'react-map-gl';
|
||||
|
||||
// 绘制聚合圆形
|
||||
export const clusterLayer: LayerProps = {
|
||||
id: 'clusters',
|
||||
type: 'circle',
|
||||
source: 'earthquakes',
|
||||
// "metadata": { // 元数据(可选,用于为 layer 附加任意的属性。为避免冲突,建议添加前缀,如 mapbox:)
|
||||
// "mapbox:name": "test"
|
||||
// },
|
||||
// "minzoom": 0, // 最小层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 小于此 minzoom 时,layer 将被隐藏)
|
||||
// "maxzoom": 24, // 最大层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 大于此 maxzoom 时,layer 将被隐藏)
|
||||
filter: ['has', 'point_count'], // 过滤(可选,用特定的表达式过滤指定的数据源的要素。具体的表达式详见 expression)
|
||||
paint: {
|
||||
'circle-color': ['step', ['get', 'point_count'], '#2eacff', 200, '#f90', 750, '#f34e52'],
|
||||
'circle-radius': ['step', ['get', 'point_count'], 20, 200, 30, 750, 40],
|
||||
"circle-stroke-color": "#fff", // 圆点的描边颜色(可选,默认值为 #000000)
|
||||
// "circle-opacity": 1, // 圆点的不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
// "circle-blur": 0, // 圆点的虚化(可选,默认值为 0。当值为 1 时,表示把圆虚化到只有圆心是不透明的)
|
||||
// "circle-translate": [0, 0], // 圆点的平移(可选,通过平移 [x, y] 达到一定的偏移量。默认值为 [0, 0],单位:像素。)
|
||||
// "circle-translate-anchor": "map", // 圆点的平移锚点,即相对的参考物(可选,可选值为 map、viewport,默认为 map)
|
||||
// "circle-pitch-scale": "map", // 地图倾斜时圆点的缩放(可选,可选值为 map、viewport,默认为 map。值为 viewport 时,圆点不会缩放)
|
||||
// "circle-pitch-alignment": "map", // 地图倾斜时圆点的对齐方式(可选,可选值为 map、viewport,默认为 map)
|
||||
"circle-stroke-width": 1, // 圆点的描边宽度(可选,值 >= 0,默认值为 0,单位:像素)
|
||||
// "circle-stroke-opacity": 1 // 圆点的描边不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
},
|
||||
};
|
||||
|
||||
// 单个绘制圆形
|
||||
export const unclusteredPointLayer: LayerProps = {
|
||||
id: 'unclustered-point',
|
||||
type: 'circle',
|
||||
source: 'earthquakes',
|
||||
filter: ['!', ['has', 'point_count']],
|
||||
paint: {
|
||||
'circle-color': '#11b4da',
|
||||
'circle-radius': 4,
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': '#fff'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 绘制填充
|
||||
export const FillLayer: LayerProps = {
|
||||
"id": "fill-id", // 唯一 id (必填)
|
||||
"type": "fill", // 类型(必填)
|
||||
"metadata": { // 元数据(可选,用于为 layer 附加任意的属性。为避免冲突,建议添加前缀,如 mapbox:)
|
||||
"mapbox:name": "test"
|
||||
},
|
||||
"source": "source-name", // 数据源的名称(除了 layer 的 type 为 background 外,source 必填)
|
||||
"source-layer": "source-layer-name", // 数据源的图层(只有数据源 source 的 type 为 vector 时,才能设置 source-layer,其他类型的不可以设置)
|
||||
"minzoom": 0, // 最小层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 小于此 minzoom 时,layer 将被隐藏)
|
||||
"maxzoom": 24, // 最大层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 大于此 maxzoom 时,layer 将被隐藏)
|
||||
"filter": [], // 过滤(可选,用特定的表达式过滤指定的数据源的要素。具体的表达式详见 expression)
|
||||
"layout": { // 布局类属性
|
||||
"visibility": "visible", // 可见性(可选,可选值为 none、visible,默认值为 visible)
|
||||
},
|
||||
"paint": { // 绘制类属性
|
||||
"fill-antialias": true, // 填充时是否反锯齿(可选,默认值为 true)
|
||||
"fill-opacity": 1, // 填充的不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
"fill-pattern": "", // 填充用的图案(可选,这里填写在 sprite 雪碧图中图标名称。为了图案能无缝填充,图标的高宽需要是 2 的倍数)
|
||||
"fill-color": "#000000", // 填充的颜色(可选,默认值为 #000000。如果设置了 fill-pattern,则 fill-color 将无效)
|
||||
"fill-outline-color": "#000000", // 描边的颜色(可选,默认和 fill-color 一致。如果设置了 fill-pattern,则 fill-outline-color 将无效。为了使用此属性,还需要设置 fill-antialias 为 true)
|
||||
"fill-translate": [0, 0], // 填充的平移(可选,通过平移 [x, y] 达到一定的偏移量。默认值为 [0, 0],单位:像素。)
|
||||
"fill-translate-anchor": "map" // 平移的锚点,即相对的参考物(可选,可选值为 map、viewport,默认为 map)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------- 绘制线 ----------------------------------------
|
||||
export const LineLayer: LayerProps = {
|
||||
"id": "line-id", // 唯一 id (必填)
|
||||
"type": "line", // 类型(必填)
|
||||
"metadata": { // 元数据(可选,用于为 layer 附加任意的属性。为避免冲突,建议添加前缀,如 mapbox:)
|
||||
"mapbox:name": "test"
|
||||
},
|
||||
"source": "source-name", // 数据源的名称(除了 layer 的 type 为 background 外,source 必填)
|
||||
"source-layer": "source-layer-name", // 数据源的图层(只有数据源 source 的 type 为 vector 时,才能设置 source-layer,其他类型的不可以设置)
|
||||
"minzoom": 0, // 最小层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 小于此 minzoom 时,layer 将被隐藏)
|
||||
"maxzoom": 24, // 最大层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 大于此 maxzoom 时,layer 将被隐藏)
|
||||
"filter": [], // 过滤(可选,用特定的表达式过滤指定的数据源的要素。具体的表达式详见 expression)
|
||||
"layout": { // 布局类属性
|
||||
"visibility": "visible", // 可见性(可选,可选值为 none、visible,默认值为 visible)
|
||||
"line-cap": "butt", // 线末端的显示样式(可选,可选值为 butt、round、square,默认值为 butt)
|
||||
// --- butt:方型末端(仅绘制到线的端点)
|
||||
// --- round:圆型末端(以线宽的 1/2 为半径,以线的端点为圆心,绘制圆型端点,会超出线的端点)
|
||||
// --- square:方型末端(以线宽的 1/2 长度超出线的端点)
|
||||
"line-join": "miter", // 线交叉时的显示样式(可选,可选值为 bevel、round、miter,默认值为 miter)
|
||||
// --- bevel:方型交点(以线宽的 1/2 长度超出线的交点)
|
||||
// --- round:圆型交点(以线宽的 1/2 为半径,以线的交点为圆心,绘制圆型交点,会超出线的交点)
|
||||
// --- miter:尖型交点(以两线段的外沿相交,超出交点绘制)
|
||||
"line-miter-limit": 2, // 最大斜接长度(可选,用来将 miter 尖型交点自动转为 bevel 方型交点,默认值为 2。只有 line-join 为 miter 时,才需要设置此属性)
|
||||
"line-round-limit": 1.05, // 最小圆角半径(可选,用来将 round 圆型交点自动转为 miter 尖型交点,默认值为 1.05。只有 line-join 为 round 时,才需要设置此属性)
|
||||
},
|
||||
"paint": { // 绘制类属性
|
||||
"line-opacity": 1, // 线的不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
"line-pattern": "", // 线用的图案(可选,这里填写在 sprite 雪碧图中图标名称。为了图案能无缝填充,图标的高宽需要是 2 的倍数)
|
||||
"line-color": "#000000", // 线的颜色(可选,默认值为 #000000。如果设置了 line-pattern,则 line-color 将无效)
|
||||
"line-translate": [0, 0], // 线的平移(可选,通过平移 [x, y] 达到一定的偏移量。默认值为 [0, 0],单位:像素。)
|
||||
"line-translate-anchor": "map", // 线的平移锚点,即相对的参考物(可选,可选值为 map、viewport,默认为 map)
|
||||
"line-width": 1, // 线的宽度(可选,值 >= 0,默认值为 1,单位:像素)
|
||||
"line-gap-width": 0, // 线的外部间距宽度(可选,值 >= 0,默认值为 0,单位:像素。用来在线的外部再绘制一部分,此值表示内间距)
|
||||
"line-offset": 0, // 线的偏移(可选,默认值为 0,单位:像素。对于单线,则是向右的偏移量;对于多边形,正值为内缩 inset,负值为外突 outset)
|
||||
"line-blur": 0, // 线的模糊度(可选,值 >= 0,默认值为 0,单位:像素)
|
||||
"line-dasharray": [0, 0], // 虚线的破折号部分和间隔的长度(可选,默认值为 [0, 0]。如果设置了 line-pattern,则 line-dasharray 将无效)
|
||||
// "line-gradient": "#000000", // 线的渐变色(可选。如果设置了 line-pattern 或 line-dasharray,则 line-gradient 将无效。只有数据源 source 的 type 为 geojson ,且 source 的 lineMetrics 为 true 时,line-gradient 才有效)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制符号
|
||||
export const clusterCountLayer: LayerProps = {
|
||||
id: 'cluster-count',
|
||||
type: 'symbol',
|
||||
source: 'earthquakes',
|
||||
filter: ['has', 'point_count'],
|
||||
layout: {
|
||||
'text-field': '{point_count_abbreviated}',
|
||||
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
|
||||
'text-size': 12,
|
||||
// "visibility": "visible", // 可见性(可选,可选值为 none、visible,默认值为 visible)
|
||||
// "symbol-placement": "point", // 符号的位置(可选,可选值为 point、line、line-center,默认值为 point)
|
||||
// // --- point:符号在几何形状的点上
|
||||
// // --- line:符号在几何形状的线上(几何形状只能为 LineString 或 Polygon)
|
||||
// // --- line-center:符号在几何形状的线的中心点上(几何形状只能为 LineString 或 Polygon)
|
||||
// "symbol-spacing": 250, // 符号之间的距离(可选,值 >= 1,默认值为 250,单位:像素。只有 symbol-placement 为 line 时才有效)
|
||||
// "symbol-avoid-edges": false, // 是否避免边缘冲突(可选,默认值为 false。当为 true 时,符号不会超过切片的边缘)
|
||||
// "symbol-sort-key": 1, // 排序的参考值(可选,无默认值。值越大,越在上方)
|
||||
// "symbol-z-order": 'source', // z 轴上的顺序控制(可选,可选值为 viewport-y、source)
|
||||
|
||||
// // 图标类属性(需要设置 icon-image)
|
||||
// "icon-image": "", // 图标的图片(可选,这里填写在 sprite 雪碧图中图标名称)
|
||||
// "icon-size": 1, // 图标的大小(可选,值 >= 0,默认值为 1。这里实际上是图标对应的原始图片的大小的缩放比例。值为 1 表示图标大小为原始图片的大小)
|
||||
// "icon-padding": 2, // 图标的外边距(可选,值 >= 0,默认值为 2。可用于碰撞检测)
|
||||
// "icon-offset": [0, 0], // 图标的偏移量(可选,默认值为 [0, 0])
|
||||
// "icon-anchor": "center", // 图标与锚点的位置关系(可选,可选值为 center、left、right、top、bottom、top-left、top-right、bottom-left、bottom-right,默认值为 center)
|
||||
// "icon-rotate": 0, // 图标的顺时针旋转角度(可选,默认值为 0,单位:角度)
|
||||
// "icon-allow-overlap": false, // 是否允许图标重叠(可选,默认值为 false。当值为 true 时,图标即使和其他符号触碰也会显示)
|
||||
// "icon-ignore-placement": false, // 是否忽略图标位置(可选,默认值为 false。当值为 true 时,其他符号即使与此图标触碰也会显示)
|
||||
// "icon-optional": false, // 图标是否可不显示(可选,默认值为 false。当值为 true 时,如果图标与文本标签碰撞,则显示文本标签)
|
||||
// "icon-text-fit": "none", // 图标与文本的大小适应关系(可选,可选值为 none、width、height、both,默认值为 none)
|
||||
// // --- none:图标按其本身的比例显示
|
||||
// // --- width:图标在 x 轴上缩放以适应文本的宽度
|
||||
// // --- height:图标在 y 轴上缩放以适应文本的高度
|
||||
// // --- both:图标在 x 和 y 轴上缩放以适应文本的宽高
|
||||
// "icon-text-fit-padding": [0, 0, 0, 0], // 图标与文本的内边距(可选,默认值为 [0,0,0,0],单位:像素)
|
||||
// "icon-keep-upright": false, // 当 icon-rotation-alignment 为 map,且 symbol-placement 为 line 或者 line-center 时,设置为 true 的话,可以避免图标上下颠倒
|
||||
// "icon-rotation-alignment": "auto", // 地图旋转时图标的对齐方式(可选,可选值为 map、viewport、auto,默认值为 auto)
|
||||
// // --- map:当 symbol-placement 为 point 时,图标与地图的东西方向对齐;当 symbol-placement 为 line 时,图标的 x 轴和线对齐
|
||||
// // --- viewport:图标的 x 轴和视口的 x 轴对齐
|
||||
// // --- auto:当 symbol-placement 为 point 时,和 viewport 一致;当 symbol-placement 为 line 时,和 map 一致
|
||||
// "icon-pitch-alignment": "auto", // 地图倾斜时图标的对齐方式(可选,可选值为 map、viewport、auto,默认值为 auto)
|
||||
// // --- map:图标的 x 轴与地图平面对齐
|
||||
// // --- viewport:图标的 x 轴和视口的 x 轴对齐
|
||||
// // --- auto:当 symbol-placement 为 point 时,和 viewport 一致;当 symbol-placement 为 line 时,和 map 一致
|
||||
|
||||
// // 文本类属性(需要指定 text-field)
|
||||
// "text-rotation-alignment": "auto", // 与 icon-rotation-alignment 类似
|
||||
// "text-pitch-alignment": "auto", // 与 icon-pitch-alignment 类似
|
||||
// "text-field": "", // 文本所对应的字段(可选,默认值为 "")
|
||||
// "text-font": ["Open Sans Regular","Arial Unicode MS Regular"], // 文本的字体集合(可选,默认值为 ["Open Sans Regular","Arial Unicode MS Regular"])
|
||||
// "text-size": 16, // 文本的大小(可选,默认值为 16,单位:像素)
|
||||
// "text-max-width": 10, // 文本的最大宽度,超过则折行(可选,默认值为 10,单位:ems)
|
||||
// "text-line-height": 1.2, // 文本的行高(可选,默认值为 1.2,单位:ems)
|
||||
// "text-letter-spacing": 0, // 文本的字符间距(可选,默认值为 0,单位:ems)
|
||||
// "text-justify": "center", // 文本的水平对齐方式(可选,可选值为 auto、left、center、right。默认值为 center)
|
||||
// "text-anchor": "center", // 文本与锚点的位置关系(可选,可选值为 center、left、right、top、bottom、top-left、top-right、bottom-left、bottom-right,默认值为 center)
|
||||
// "text-max-angle": 45, // 当 symbol-placement 为 line 或 line-center 时,文本相邻字符的最大夹角,默认 45 度
|
||||
// "text-rotate": 0, // 文本的顺时针旋转角度(可选,默认值为 0,单位:角度)
|
||||
// "text-padding": 2, // 文本的外边距(可选,值 >= 0,默认值为 2。可用于碰撞检测)
|
||||
// "text-keep-upright": false, // 当 icon-rotation-alignment 为 map,且 symbol-placement 为 line 或者 line-center 时,设置为 true 的话,可以避免文本上下颠倒
|
||||
// "text-transform": "none", // 文本大小写转换(可选,可选值为 none、uppercase、lowercase,默认值为 none)
|
||||
// "text-offset": [0, 0], // 图标的偏移量(可选,默认值为 [0, 0])
|
||||
// "text-radial-offset": 0, // 文本的径向偏移量,优先级比 text-offset 高
|
||||
// "text-allow-overlap": false, // 是否允许文本重叠(可选,默认值为 false。当值为 true 时,文本即使和其他符号触碰也会显示)
|
||||
// "text-ignore-placement": false, // 是否忽略文本位置(可选,默认值为 false。当值为 true 时,其他符号即使与此文本触碰也会显示)
|
||||
// "text-optional": false // 文本是否可不显示(可选,默认值为 false。当值为 true 时,如果文本与图标碰撞,则显示图标)
|
||||
},
|
||||
"paint": { // 绘制类属性
|
||||
// // 图标类属性(需要设置 icon-image)
|
||||
// "icon-opacity": 1, // 图标的不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
"icon-color": "#fff", // 图标的颜色(可选,默认值为 #000000)
|
||||
// "icon-halo-color": "rgba(0,0,0,0)", // 图标的光晕颜色(可选,默认值为 rgba(0,0,0,0))
|
||||
// "icon-halo-width": 0, // 图标的光晕宽度(可选,值 >= 0,默认值为 0,单位:像素)
|
||||
// "icon-halo-blur": 0, // 图标的光晕模糊宽度(可选,值 >= 0,默认值为 0,单位:像素)
|
||||
// "icon-translate": [0, 0], // 图标的平移(可选,通过平移 [x, y] 达到一定的偏移量。默认值为 [0, 0],单位:像素。)
|
||||
// "icon-translate-anchor": "map", // 图标的平移锚点,即相对的参考物(可选,可选值为 map、viewport,默认为 map)
|
||||
|
||||
// // 文本类属性(需要设置 text-field)
|
||||
// "text-opacity": 1, // 文本的不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
"text-color": "#fff", // 文本的颜色(可选,默认值为 #000000)
|
||||
// "text-halo-color": "rgba(0,0,0,0)", // 文本的光晕颜色(可选,默认值为 rgba(0,0,0,0))
|
||||
// "text-halo-width": 0, // 文本的光晕宽度(可选,值 >= 0,默认值为 0,单位:像素)
|
||||
// "text-halo-blur": 0, // 文本的光晕模糊宽度(可选,值 >= 0,默认值为 0,单位:像素)
|
||||
// "text-translate": [0, 0], // 文本的平移(可选,通过平移 [x, y] 达到一定的偏移量。默认值为 [0, 0],单位:像素。)
|
||||
// "text-translate-anchor": "map", // 文本的平移锚点,即相对的参考物(可选,可选值为 map、viewport,默认为 map)
|
||||
}
|
||||
};
|
||||
|
||||
// 背景
|
||||
export const backgroundLayer: LayerProps = {
|
||||
"id": "background-id", // 唯一 id (必填)
|
||||
"type": "background", // 类型(必填)
|
||||
"metadata": { // 元数据(可选,用于为 layer 附加任意的属性。为避免冲突,建议添加前缀,如 mapbox:)
|
||||
"mapbox:name": "test"
|
||||
},
|
||||
"minzoom": 0, // 最小层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 小于此 minzoom 时,layer 将被隐藏)
|
||||
"maxzoom": 24, // 最大层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 大于此 maxzoom 时,layer 将被隐藏)
|
||||
"layout": { // 布局类属性
|
||||
"visibility": "visible", // 可见性(可选,可选值为 none、visible,默认值为 visible)
|
||||
},
|
||||
"paint": { // 绘制类属性
|
||||
"background-color": "#000000", // 背景颜色(可选,默认值为 #000000。如果设置了 background-pattern,则 background-color 将无效)
|
||||
"background-pattern": "", // 背景图案(可选,这里填写在 sprite 雪碧图中图标名称。为了背景图案能无缝填充,图标的高宽需要是 2 的倍数)
|
||||
"background-opacity": 1 // 背景不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 栅格布局
|
||||
export const rasterLayer: LayerProps = {
|
||||
"id": "raster-id", // 唯一 id (必填)
|
||||
"type": "raster", // 类型(必填)
|
||||
"metadata": { // 元数据(可选,用于为 layer 附加任意的属性。为避免冲突,建议添加前缀,如 mapbox:)
|
||||
"mapbox:name": "test"
|
||||
},
|
||||
"source": "source-name", // 数据源的名称(除了 layer 的 type 为 background 外,source 必填)
|
||||
"source-layer": "source-layer-name", // 数据源的图层(只有数据源 source 的 type 为 vector 时,才能设置 source-layer,其他类型的不可以设置)
|
||||
"minzoom": 0, // 最小层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 小于此 minzoom 时,layer 将被隐藏)
|
||||
"maxzoom": 24, // 最大层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 大于此 maxzoom 时,layer 将被隐藏)
|
||||
"filter": [], // 过滤(可选,用特定的表达式过滤指定的数据源的要素。具体的表达式详见 expression)
|
||||
"layout": { // 布局类属性
|
||||
"visibility": "visible", // 可见性(可选,可选值为 none、visible,默认值为 visible)
|
||||
},
|
||||
"paint": { // 绘制类属性
|
||||
"raster-opacity": 1, // 图片的不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
"raster-hue-rotate": 0, // 在色轮上旋转色相的角度(可选,默认值为 0,单位:角度)
|
||||
"raster-brightness-min": 0, // 图片的最小亮度(可选,取值范围为 0 ~ 1,默认值为 0)
|
||||
"raster-brightness-max": 1, // 图片的最大亮度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
"raster-saturation": 0, // 图片的饱和度(可选,取值范围为 -1 ~ 1,默认值为 0)
|
||||
"raster-contrast": 0, // 图片的对比度(可选,取值范围为 -1 ~ 1,默认值为 0)
|
||||
"raster-resampling": "linear", // 采样方式(可选,可选值为 linear、nearest,默认值为 linear)
|
||||
"raster-fade-duration": 300 // 切换瓦片时的渐隐时间(可选,默认值为 300,单位:毫秒)
|
||||
}
|
||||
}
|
||||
|
||||
// 热力图
|
||||
export const heatmapLayer: LayerProps = {
|
||||
"id": "heatmap-id", // 唯一 id (必填)
|
||||
"type": "heatmap", // 类型(必填)
|
||||
"metadata": { // 元数据(可选,用于为 layer 附加任意的属性。为避免冲突,建议添加前缀,如 mapbox:)
|
||||
"mapbox:name": "test"
|
||||
},
|
||||
"source": "source-name", // 数据源的名称(除了 layer 的 type 为 background 外,source 必填)
|
||||
"source-layer": "source-layer-name", // 数据源的图层(只有数据源 source 的 type 为 vector 时,才能设置 source-layer,其他类型的不可以设置)
|
||||
"minzoom": 0, // 最小层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 小于此 minzoom 时,layer 将被隐藏)
|
||||
"maxzoom": 24, // 最大层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 大于此 maxzoom 时,layer 将被隐藏)
|
||||
"filter": [], // 过滤(可选,用特定的表达式过滤指定的数据源的要素。具体的表达式详见 expression)
|
||||
"layout": { // 布局类属性
|
||||
"visibility": "visible", // 可见性(可选,可选值为 none、visible,默认值为 visible)
|
||||
},
|
||||
"paint": { // 绘制类属性
|
||||
"heatmap-opacity": 1, // 热力图的不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
"heatmap-radius": 30, // 一个热力图点的影响半径(可选,值 >= 1,默认值为 30,单位:像素)
|
||||
"heatmap-weight": 1, // 一个热力图点的权重(可选,值 >= 0,默认值为 1)
|
||||
"heatmap-intensity": 1, // 热力图的强度,控制了所有的热力图点(可选,值 >= 0,默认值为 1)
|
||||
"heatmap-color": [ // 热力图的颜色变化(可选,默认值如下)
|
||||
"interpolate", ["linear"], ["heatmap-density"],
|
||||
0, "rgba(0, 0, 255, 0)",
|
||||
0.1, "royalblue",
|
||||
0.3, "cyan",
|
||||
0.5, "lime",
|
||||
0.7, "yellow",
|
||||
1, "red"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 坡面阴影
|
||||
export const hillshadeLayer: LayerProps = {
|
||||
"id": "hillshade-id", // 唯一 id (必填)
|
||||
"type": "hillshade", // 类型(必填)
|
||||
"metadata": { // 元数据(可选,用于为 layer 附加任意的属性。为避免冲突,建议添加前缀,如 mapbox:)
|
||||
"mapbox:name": "test"
|
||||
},
|
||||
"source": "source-name", // 数据源的名称(除了 layer 的 type 为 background 外,source 必填)
|
||||
"source-layer": "source-layer-name", // 数据源的图层(只有数据源 source 的 type 为 vector 时,才能设置 source-layer,其他类型的不可以设置)
|
||||
"minzoom": 0, // 最小层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 小于此 minzoom 时,layer 将被隐藏)
|
||||
"maxzoom": 24, // 最大层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 大于此 maxzoom 时,layer 将被隐藏)
|
||||
"filter": [], // 过滤(可选,用特定的表达式过滤指定的数据源的要素。具体的表达式详见 expression)
|
||||
"layout": { // 布局类属性
|
||||
"visibility": "visible", // 可见性(可选,可选值为 none、visible,默认值为 visible)
|
||||
},
|
||||
"paint": { // 绘制类属性
|
||||
"hillshade-illumination-direction": 335, // 光照的方向(可选,取值范围为 0 ~ 359,默认值为 335,单位:角度)
|
||||
"hillshade-illumination-anchor": "viewport", // 光照的锚点(可选,可选值为 map、viewport,默认值为 viewport)
|
||||
"hillshade-exaggeration": 0.5, // 阴影的强度(可选,取值范围为 0 ~ 1,默认值为 0.5)
|
||||
"hillshade-shadow-color": "#000000", // 阴影的颜色(可选,默认值为 #000000)
|
||||
"hillshade-highlight-color": "#ffffff", // 光照部分的颜色(可选,默认值为 #ffffff)
|
||||
"hillshade-accent-color": "#000000" // 用于强调地形的颜色(可选,默认值为 #000000)
|
||||
}
|
||||
}
|
||||
|
||||
// 三维填充
|
||||
export const FillExtrusionLayer: LayerProps = {
|
||||
"id": "fill-extrusion-id", // 唯一 id (必填)
|
||||
"type": "fill-extrusion", // 类型(必填)
|
||||
"metadata": { // 元数据(可选,用于为 layer 附加任意的属性。为避免冲突,建议添加前缀,如 mapbox:)
|
||||
"mapbox:name": "test"
|
||||
},
|
||||
"source": "source-name", // 数据源的名称(除了 layer 的 type 为 background 外,source 必填)
|
||||
"source-layer": "source-layer-name", // 数据源的图层(只有数据源 source 的 type 为 vector 时,才能设置 source-layer,其他类型的不可以设置)
|
||||
"minzoom": 0, // 最小层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 小于此 minzoom 时,layer 将被隐藏)
|
||||
"maxzoom": 24, // 最大层级(可选,取值范围为 0 ~ 24。当 style 的 zoom 大于此 maxzoom 时,layer 将被隐藏)
|
||||
"filter": [], // 过滤(可选,用特定的表达式过滤指定的数据源的要素。具体的表达式详见 expression)
|
||||
"layout": { // 布局类属性
|
||||
"visibility": "visible", // 可见性(可选,可选值为 none、visible,默认值为 visible)
|
||||
},
|
||||
"paint": { // 绘制类属性
|
||||
"fill-extrusion-opacity": 1, // 三维填充的不透明度(可选,取值范围为 0 ~ 1,默认值为 1)
|
||||
"fill-extrusion-pattern": "", // 三维填充的图案(可选,这里填写在 sprite 雪碧图中图标名称。为了图案能无缝填充,图标的高宽需要是 2 的倍数)
|
||||
"fill-extrusion-color": "#000000", // 三维填充的颜色(可选,默认值为 #000000)
|
||||
"fill-extrusion-translate": [0, 0], // 三维填充的平移(可选,通过平移 [x, y] 达到一定的偏移量。默认值为 [0, 0],单位:像素。)
|
||||
"fill-extrusion-translate-anchor": "map", // 平移的锚点,即相对的参考物(可选,可选值为 map、viewport,默认为 map)
|
||||
"fill-extrusion-height": 0, // 三维填充的高度(可选,值 >= 0,默认值为 0,单位:米)
|
||||
"fill-extrusion-base": 0, // 三维填充的底部高度(可选,值 >= 0,默认值为 0,单位:米。值必须小于等于 fill-extrusion-height)
|
||||
"fill-extrusion-vertical-gradient": true, // 是否开启垂直渐变(可选,默认值为 true)
|
||||
}
|
||||
}
|
60
packages/map/src/components/customOverlay/CustomOverlay.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
// @ts-nocheck
|
||||
import * as React from 'react';
|
||||
import {useState, cloneElement} from 'react';
|
||||
import {useControl} from 'react-map-gl';
|
||||
import {createPortal} from 'react-dom';
|
||||
import type { MapboxMap, IControl } from 'react-map-gl';
|
||||
|
||||
// Based on template in https://docs.mapbox.com/mapbox-gl-js/api/markers/#icontrol
|
||||
class OverlayControl implements IControl {
|
||||
_map: MapboxMap | undefined;
|
||||
_container: HTMLElement | undefined;
|
||||
_redraw: () => void;
|
||||
|
||||
constructor(redraw: () => void) {
|
||||
this._redraw = redraw;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
onAdd(map) {
|
||||
this._map = map;
|
||||
map.on('move', this._redraw);
|
||||
/* global document */
|
||||
this._container = document.createElement('div');
|
||||
this._redraw();
|
||||
return this._container;
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
this._container?.remove();
|
||||
this._map?.off('move', this._redraw);
|
||||
this._map = undefined;
|
||||
}
|
||||
|
||||
getMap() {
|
||||
return this._map;
|
||||
}
|
||||
|
||||
getElement() {
|
||||
return this._container;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom control that rerenders arbitrary React content whenever the camera changes
|
||||
*/
|
||||
function CustomOverlay(props: {children: React.ReactElement}) {
|
||||
const [, setVersion] = useState(0);
|
||||
|
||||
const ctrl = useControl<OverlayControl>(() => {
|
||||
const forceUpdate = () => setVersion(v => v + 1);
|
||||
return new OverlayControl(forceUpdate);
|
||||
});
|
||||
|
||||
const map = ctrl.getMap();
|
||||
|
||||
// @ts-ignore
|
||||
return map && createPortal(cloneElement(props.children, {map}), ctrl.getElement());
|
||||
}
|
||||
|
||||
export default React.memo(CustomOverlay);
|
6
packages/map/src/components/customOverlay/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/21
|
||||
*/
|
||||
import CustomOverlay from './CustomOverlay'
|
||||
|
||||
export default CustomOverlay
|
100
packages/map/src/components/drawControl/Draw/constants.js
Normal file
@ -0,0 +1,100 @@
|
||||
export const classes = {
|
||||
CONTROL_BASE: 'mapboxgl-ctrl',
|
||||
CONTROL_PREFIX: 'mapboxgl-ctrl-',
|
||||
CONTROL_BUTTON: 'mapbox-gl-draw_ctrl-draw-btn',
|
||||
CONTROL_BUTTON_LINE: 'mapbox-gl-draw_line',
|
||||
CONTROL_BUTTON_POLYGON: 'mapbox-gl-draw_polygon',
|
||||
CONTROL_BUTTON_POINT: 'mapbox-gl-draw_point',
|
||||
CONTROL_BUTTON_TRASH: 'mapbox-gl-draw_trash',
|
||||
CONTROL_BUTTON_COMBINE_FEATURES: 'mapbox-gl-draw_combine',
|
||||
CONTROL_BUTTON_UNCOMBINE_FEATURES: 'mapbox-gl-draw_uncombine',
|
||||
CONTROL_GROUP: 'mapboxgl-ctrl-group',
|
||||
ATTRIBUTION: 'mapboxgl-ctrl-attrib',
|
||||
ACTIVE_BUTTON: 'active',
|
||||
BOX_SELECT: 'mapbox-gl-draw_boxselect',
|
||||
};
|
||||
|
||||
export const sources = {
|
||||
HOT: 'mapbox-gl-draw-hot',
|
||||
COLD: 'mapbox-gl-draw-cold',
|
||||
};
|
||||
|
||||
export const cursors = {
|
||||
ADD: 'add',
|
||||
MOVE: 'move',
|
||||
DRAG: 'drag',
|
||||
POINTER: 'pointer',
|
||||
NONE: 'none',
|
||||
};
|
||||
|
||||
export const types = {
|
||||
POLYGON: 'polygon',
|
||||
LINE: 'line_string',
|
||||
POINT: 'point',
|
||||
};
|
||||
|
||||
export const geojsonTypes = {
|
||||
FEATURE: 'Feature',
|
||||
POLYGON: 'Polygon',
|
||||
LINE_STRING: 'LineString',
|
||||
POINT: 'Point',
|
||||
FEATURE_COLLECTION: 'FeatureCollection',
|
||||
MULTI_PREFIX: 'Multi',
|
||||
MULTI_POINT: 'MultiPoint',
|
||||
MULTI_LINE_STRING: 'MultiLineString',
|
||||
MULTI_POLYGON: 'MultiPolygon',
|
||||
};
|
||||
|
||||
export const modes = {
|
||||
DRAW_LINE_STRING: 'draw_line_string',
|
||||
DRAW_POLYGON: 'draw_polygon',
|
||||
DRAW_POINT: 'draw_point',
|
||||
SIMPLE_SELECT: 'simple_select',
|
||||
DIRECT_SELECT: 'direct_select',
|
||||
STATIC: 'static',
|
||||
};
|
||||
|
||||
export const events = {
|
||||
CREATE: 'draw.create',
|
||||
DELETE: 'draw.delete',
|
||||
UPDATE: 'draw.update',
|
||||
SELECTION_CHANGE: 'draw.selectionchange',
|
||||
MODE_CHANGE: 'draw.modechange',
|
||||
ACTIONABLE: 'draw.actionable',
|
||||
RENDER: 'draw.render',
|
||||
COMBINE_FEATURES: 'draw.combine',
|
||||
UNCOMBINE_FEATURES: 'draw.uncombine',
|
||||
};
|
||||
|
||||
export const updateActions = {
|
||||
MOVE: 'move',
|
||||
CHANGE_COORDINATES: 'change_coordinates',
|
||||
};
|
||||
|
||||
export const meta = {
|
||||
FEATURE: 'feature',
|
||||
MIDPOINT: 'midpoint',
|
||||
VERTEX: 'vertex',
|
||||
};
|
||||
|
||||
export const activeStates = {
|
||||
ACTIVE: 'true',
|
||||
INACTIVE: 'false',
|
||||
};
|
||||
|
||||
export const interactions = [
|
||||
'scrollZoom',
|
||||
'boxZoom',
|
||||
'dragRotate',
|
||||
'dragPan',
|
||||
'keyboard',
|
||||
'doubleClickZoom',
|
||||
'touchZoomRotate',
|
||||
];
|
||||
|
||||
export const LAT_MIN = -90;
|
||||
export const LAT_RENDERED_MIN = -85;
|
||||
export const LAT_MAX = 90;
|
||||
export const LAT_RENDERED_MAX = 85;
|
||||
export const LNG_MIN = -270;
|
||||
export const LNG_MAX = 270;
|
@ -0,0 +1,24 @@
|
||||
const doubleClickZoom = {
|
||||
enable(ctx) {
|
||||
setTimeout(() => {
|
||||
if (
|
||||
!ctx.map ||
|
||||
!ctx.map.doubleClickZoom ||
|
||||
!ctx._ctx ||
|
||||
!ctx._ctx.store ||
|
||||
!ctx._ctx.store.getInitialConfigValue
|
||||
)
|
||||
return;
|
||||
if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return;
|
||||
ctx.map.doubleClickZoom.enable();
|
||||
}, 0);
|
||||
},
|
||||
disable(ctx) {
|
||||
setTimeout(() => {
|
||||
if (!ctx.map || !ctx.map.doubleClickZoom) return;
|
||||
ctx.map.doubleClickZoom.disable();
|
||||
}, 0);
|
||||
},
|
||||
};
|
||||
|
||||
export default doubleClickZoom;
|
@ -0,0 +1,69 @@
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||
import doubleClickZoom from './doubleClickZoom';
|
||||
import * as turf from '@turf/turf';
|
||||
const { circle, distance, helpers: turfHelpers } = turf;
|
||||
const drawCircleMode = { ...MapboxDraw.modes.draw_polygon };
|
||||
drawCircleMode.onSetup = function () {
|
||||
const polygon = this.newFeature({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
isCircle: true,
|
||||
center: [],
|
||||
},
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [],
|
||||
},
|
||||
});
|
||||
|
||||
this.addFeature(polygon);
|
||||
|
||||
this.clearSelectedFeatures();
|
||||
doubleClickZoom.disable(this);
|
||||
// dragPan.disable(this);
|
||||
this.updateUIClasses({ mouse: 'add' });
|
||||
this.activateUIButton('Polygon');
|
||||
this.setActionableState({
|
||||
trash: true,
|
||||
});
|
||||
|
||||
return {
|
||||
polygon,
|
||||
currentVertexPosition: 0,
|
||||
};
|
||||
};
|
||||
drawCircleMode.onClick = drawCircleMode.onTap = function (state, e) {
|
||||
const currentCenter = state.polygon.properties.center;
|
||||
if (currentCenter.length === 0) {
|
||||
// dragPan.disable(this)
|
||||
state.polygon.properties.center = [e.lngLat.lng, e.lngLat.lat];
|
||||
} else {
|
||||
// dragPan.enable(this);
|
||||
return this.changeMode('simple_select', { featureIds: [state.polygon.id] });
|
||||
}
|
||||
};
|
||||
drawCircleMode.onDrag = drawCircleMode.onMouseMove = function (state, e) {
|
||||
const center = state.polygon.properties.center;
|
||||
if (center.length > 0) {
|
||||
const distanceInKm = distance(
|
||||
turfHelpers.point(center),
|
||||
turfHelpers.point([e.lngLat.lng, e.lngLat.lat]),
|
||||
{
|
||||
units: 'kilometers',
|
||||
}
|
||||
);
|
||||
const circleFeature = circle(center, distanceInKm);
|
||||
state.polygon.incomingCoords(circleFeature.geometry.coordinates);
|
||||
state.polygon.properties.radiusInKm = distanceInKm;
|
||||
state.polygon.properties.lastClickCoord = [e.lngLat.lng, e.lngLat.lat];
|
||||
}
|
||||
};
|
||||
//它决定当前 Drew 数据存储中的哪些特性将在地图上呈现。
|
||||
//所有传递给“显示”的特性都将被渲染,因此可以为每个内部特性传递多个显示特性。
|
||||
//有关如何制作显示特性的建议,请参阅‘ styling-pull’in‘ API.md’
|
||||
drawCircleMode.toDisplayFeatures = function (state, geojson, display) {
|
||||
const isActivePolygon = geojson.properties.id === state.polygon.id;
|
||||
geojson.properties.active = isActivePolygon ? 'true' : 'false';
|
||||
display(geojson);
|
||||
};
|
||||
export default drawCircleMode;
|
@ -0,0 +1,131 @@
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||
import createSupplementaryPoints from '@mapbox/mapbox-gl-draw';
|
||||
import moveFeatures from '@mapbox/mapbox-gl-draw';
|
||||
import constrainFeatureMovement from '@mapbox/mapbox-gl-draw';
|
||||
import createVertex from '@mapbox/mapbox-gl-draw';
|
||||
import * as turf from '@turf/turf';
|
||||
const { constants } = MapboxDraw;
|
||||
const { circle, distance, helpers: turfHelpers } = turf;
|
||||
|
||||
function createSupplementaryPointsForCircle(geojson) {
|
||||
const { properties, geometry } = geojson;
|
||||
|
||||
if (!properties.user_isCircle) return null;
|
||||
|
||||
const supplementaryPoints = [];
|
||||
const vertices = geometry.coordinates[0].slice(0, -1);
|
||||
for (let index = 0; index < vertices.length; index += Math.round(vertices.length / 4)) {
|
||||
supplementaryPoints.push(createVertex(properties.id, vertices[index], `0.${index}`, false));
|
||||
}
|
||||
return supplementaryPoints;
|
||||
}
|
||||
const drawDirectMode = { ...MapboxDraw.modes.direct_select };
|
||||
|
||||
drawDirectMode.dragFeature = function (state, e, delta) {
|
||||
moveFeatures(this.getSelected(), delta);
|
||||
this.getSelected()
|
||||
.filter((feature) => feature.properties.isCircle)
|
||||
.map((circle) => circle.properties.center)
|
||||
.forEach((center) => {
|
||||
center[0] += delta.lng;
|
||||
center[1] += delta.lat;
|
||||
});
|
||||
state.dragMoveLocation = e.lngLat;
|
||||
};
|
||||
|
||||
drawDirectMode.dragVertex = function (state, e, delta) {
|
||||
//圆处理
|
||||
if (state.feature.properties.isCircle) {
|
||||
const center = state.feature.properties.center;
|
||||
const movedVertex = [e.lngLat.lng, e.lngLat.lat];
|
||||
const radius = distance(turfHelpers.point(center), turfHelpers.point(movedVertex), {
|
||||
units: 'kilometers',
|
||||
});
|
||||
const circleFeature = circle(center, radius);
|
||||
state.feature.incomingCoords(circleFeature.geometry.coordinates);
|
||||
state.feature.properties.radiusInKm = radius;
|
||||
return;
|
||||
}
|
||||
//矩形处理
|
||||
if (state.feature.properties.isRect) {
|
||||
state.selectedCoordPaths.forEach((coordPath) => {
|
||||
const selectCoord = state.feature.getCoordinate(coordPath);
|
||||
//更新边缘2点
|
||||
const [featureIndex, coordIndex] = coordPath.split('.');
|
||||
const coordinates = state.feature.getCoordinates()[featureIndex];
|
||||
//对立点判断
|
||||
const coordPosMap = {
|
||||
1: '3',
|
||||
2: '0',
|
||||
3: '1',
|
||||
0: '2',
|
||||
};
|
||||
const mapCoord = state.feature.getCoordinate(`${featureIndex}.${coordPosMap[coordIndex]}`);
|
||||
//如果对立点和坐标x||y 一致 则返回
|
||||
if (mapCoord[0] === e.lngLat.lng || mapCoord[1] === e.lngLat.lat) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const coord = coordinates[i];
|
||||
if (coordIndex == i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (coord[0] === selectCoord[0]) {
|
||||
state.feature.updateCoordinate(`${featureIndex}.${i}`, e.lngLat.lng, coord[1]);
|
||||
|
||||
continue;
|
||||
}
|
||||
if (coord[1] === selectCoord[1]) {
|
||||
state.feature.updateCoordinate(`${featureIndex}.${i}`, coord[0], e.lngLat.lat);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
//更新拖拽的点
|
||||
state.feature.updateCoordinate(coordPath, e.lngLat.lng, e.lngLat.lat);
|
||||
});
|
||||
return;
|
||||
}
|
||||
//其他走回默认
|
||||
const selectedCoords = state.selectedCoordPaths.map((coordPath) =>
|
||||
state.feature.getCoordinate(coordPath)
|
||||
);
|
||||
const selectedCoordPoints = selectedCoords.map((coords) => ({
|
||||
type: constants.geojsonTypes.FEATURE,
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: constants.geojsonTypes.POINT,
|
||||
coordinates: coords,
|
||||
},
|
||||
}));
|
||||
|
||||
const constrainedDelta = constrainFeatureMovement(selectedCoordPoints, delta);
|
||||
for (let i = 0; i < selectedCoords.length; i++) {
|
||||
const coord = selectedCoords[i];
|
||||
state.feature.updateCoordinate(
|
||||
state.selectedCoordPaths[i],
|
||||
coord[0] + constrainedDelta.lng,
|
||||
coord[1] + constrainedDelta.lat
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
drawDirectMode.toDisplayFeatures = function (state, geojson, push) {
|
||||
if (state.featureId === geojson.properties.id) {
|
||||
geojson.properties.active = constants.activeStates.ACTIVE;
|
||||
push(geojson);
|
||||
const supplementaryPoints = geojson.properties.user_isCircle
|
||||
? createSupplementaryPointsForCircle(geojson)
|
||||
: createSupplementaryPoints(geojson, {
|
||||
map: this.map,
|
||||
midpoints: true,
|
||||
selectedPaths: state.selectedCoordPaths,
|
||||
});
|
||||
supplementaryPoints.forEach(push);
|
||||
} else {
|
||||
geojson.properties.active = constants.activeStates.INACTIVE;
|
||||
push(geojson);
|
||||
}
|
||||
this.fireActionable(state);
|
||||
};
|
||||
export default drawDirectMode;
|
@ -0,0 +1,165 @@
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||
import doubleClickZoom from './doubleClickZoom';
|
||||
const { constants, lib } = MapboxDraw;
|
||||
const drawLineSelectMode = {
|
||||
//当模式启动时,这个函数将被调用。
|
||||
//draw.changeMode(drawLineSelectMode,params}); 切换模式时,params = opts
|
||||
//返回的值应该是一个对象,并将传递给所有其他生命周期函数
|
||||
onSetup: function (opts) {
|
||||
const featureId = opts.featureId;
|
||||
let line;
|
||||
let currentVertexPosition = 0;
|
||||
let direction = 'forward';
|
||||
if (featureId) {
|
||||
line = this.getFeature(featureId);
|
||||
if (!line) {
|
||||
throw new Error('Could not find a feature with the provided featureId');
|
||||
}
|
||||
let from = opts.from;
|
||||
if (from && from.type === 'Feature' && from.geometry && from.geometry.type === 'Point') {
|
||||
from = from.geometry;
|
||||
}
|
||||
if (from && from.type === 'Point' && from.coordinates && from.coordinates.length === 2) {
|
||||
from = from.coordinates;
|
||||
}
|
||||
if (!from || !Array.isArray(from)) {
|
||||
throw new Error(
|
||||
'Please use the `from` property to indicate which point to continue the line from'
|
||||
);
|
||||
}
|
||||
const lastCoord = line.coordinates.length - 1;
|
||||
if (
|
||||
line.coordinates[lastCoord][0] === from[0] &&
|
||||
line.coordinates[lastCoord][1] === from[1]
|
||||
) {
|
||||
currentVertexPosition = lastCoord + 1;
|
||||
line.addCoordinate(currentVertexPosition, ...line.coordinates[lastCoord]);
|
||||
} else if (line.coordinates[0][0] === from[0] && line.coordinates[0][1] === from[1]) {
|
||||
direction = 'backwards';
|
||||
currentVertexPosition = 0;
|
||||
line.addCoordinate(currentVertexPosition, ...line.coordinates[0]);
|
||||
} else {
|
||||
throw new Error(
|
||||
'`from` should match the point at either the start or the end of the provided LineString'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
line = this.newFeature({
|
||||
type: constants.geojsonTypes.FEATURE,
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: constants.geojsonTypes.LINE_STRING,
|
||||
coordinates: [],
|
||||
},
|
||||
});
|
||||
currentVertexPosition = 0;
|
||||
this.addFeature(line);
|
||||
}
|
||||
this.clearSelectedFeatures();
|
||||
doubleClickZoom.disable(this);
|
||||
this.updateUIClasses({ mouse: 'add' }); //"+"
|
||||
this.setActionableState({
|
||||
//添加地图事件'draw.actionable'
|
||||
trash: true,
|
||||
combineFeatures: false,
|
||||
uncombineFeatures: false,
|
||||
});
|
||||
return {
|
||||
line,
|
||||
currentVertexPosition,
|
||||
direction,
|
||||
};
|
||||
},
|
||||
clickAnywhere: function (state, e) {
|
||||
if (
|
||||
(state.currentVertexPosition > 0 &&
|
||||
lib.isEventAtCoordinates(e, state.line.coordinates[state.currentVertexPosition - 1])) ||
|
||||
(state.direction === 'backwards' &&
|
||||
lib.isEventAtCoordinates(e, state.line.coordinates[state.currentVertexPosition + 1]))
|
||||
) {
|
||||
return this.changeMode(constants.modes.SIMPLE_SELECT, {
|
||||
featureIds: [state.line.id],
|
||||
});
|
||||
}
|
||||
|
||||
this.updateUIClasses({ mouse: constants.cursors.ADD });
|
||||
state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat);
|
||||
if (state.direction === 'forward') {
|
||||
state.currentVertexPosition++;
|
||||
state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat);
|
||||
} else {
|
||||
state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat);
|
||||
}
|
||||
},
|
||||
clickOnVertex: function (state) {
|
||||
//点击在已经有点上
|
||||
state.line.properties.name = 'line_select';
|
||||
return this.changeMode(constants.modes.SIMPLE_SELECT, { featureIds: [state.line.id] });
|
||||
},
|
||||
onMouseMove: function (state, e) {
|
||||
state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat);
|
||||
if (lib.CommonSelectors.isVertex(e)) {
|
||||
//再次将鼠标放在之前已经有的点上,改变鼠标样式
|
||||
this.updateUIClasses({ mouse: constants.cursors.POINTER });
|
||||
}
|
||||
},
|
||||
onClick: function (state, e) {
|
||||
//再次将鼠标放在之前已经有的点上
|
||||
if (lib.CommonSelectors.isVertex(e)) return this.clickOnVertex(state, e);
|
||||
this.clickAnywhere(state, e);
|
||||
},
|
||||
onKeyUp: function (state, e) {
|
||||
if (lib.CommonSelectors.isEnterKey(e)) {
|
||||
this.changeMode(constants.modes.SIMPLE_SELECT, { featureIds: [state.line.id] });
|
||||
} else if (lib.CommonSelectors.isEscapeKey(e)) {
|
||||
this.deleteFeature([state.line.id], { silent: true });
|
||||
this.changeMode(constants.modes.SIMPLE_SELECT);
|
||||
}
|
||||
},
|
||||
onStop: function (state) {
|
||||
doubleClickZoom.enable(this);
|
||||
if (this.getFeature(state.line.id) === undefined) return;
|
||||
state.line.removeCoordinate(`${state.currentVertexPosition}`); //双击停止时,最后两个点位是一样的
|
||||
if (state.line.isValid()) {
|
||||
//'draw.create'
|
||||
this.map.fire(constants.events.CREATE, {
|
||||
features: [state.line.toGeoJSON()],
|
||||
});
|
||||
} else {
|
||||
this.deleteFeature([state.line.id], { silent: true });
|
||||
this.changeMode(constants.modes.SIMPLE_SELECT, {}, { silent: true });
|
||||
}
|
||||
},
|
||||
onTrash: function (state) {
|
||||
this.deleteFeature([state.line.id], { silent: true });
|
||||
this.changeMode(constants.modes.SIMPLE_SELECT);
|
||||
},
|
||||
//它决定当前 Drew 数据存储中的哪些特性将在地图上呈现。
|
||||
//所有传递给“显示”的特性都将被渲染,因此可以为每个内部特性传递多个显示特性。
|
||||
//有关如何制作显示特性的建议,请参阅‘ styling-pull’in‘ API.md’
|
||||
toDisplayFeatures: function (state, geojson, display) {
|
||||
const isActiveLine = geojson.properties.id === state.line.id;
|
||||
geojson.properties.active = isActiveLine
|
||||
? constants.activeStates.ACTIVE
|
||||
: constants.activeStates.INACTIVE;
|
||||
if (!isActiveLine) {
|
||||
display(geojson);
|
||||
return;
|
||||
}
|
||||
if (geojson.geometry.coordinates.length < 2) return;
|
||||
geojson.properties.meta = 'line_distance';
|
||||
geojson.properties.name = 'line_distance';
|
||||
display(
|
||||
lib.createVertex(
|
||||
state.line.id,
|
||||
geojson.geometry.coordinates[
|
||||
state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1
|
||||
],
|
||||
`${state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1}`,
|
||||
false
|
||||
)
|
||||
);
|
||||
display(geojson);
|
||||
},
|
||||
};
|
||||
export default drawLineSelectMode;
|
@ -0,0 +1,125 @@
|
||||
const doubleClickZoom = {
|
||||
enable: (ctx) => {
|
||||
setTimeout(() => {
|
||||
// First check we've got a map and some context.
|
||||
if (
|
||||
!ctx.map ||
|
||||
!ctx.map.doubleClickZoom ||
|
||||
!ctx._ctx ||
|
||||
!ctx._ctx.store ||
|
||||
!ctx._ctx.store.getInitialConfigValue
|
||||
)
|
||||
return;
|
||||
// Now check initial state wasn't false (we leave it disabled if so)
|
||||
if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return;
|
||||
ctx.map.doubleClickZoom.enable();
|
||||
}, 0);
|
||||
},
|
||||
disable(ctx) {
|
||||
setTimeout(() => {
|
||||
if (!ctx.map || !ctx.map.doubleClickZoom) return;
|
||||
// Always disable here, as it's necessary in some cases.
|
||||
ctx.map.doubleClickZoom.disable();
|
||||
}, 0);
|
||||
},
|
||||
};
|
||||
|
||||
const DrawRectangle = {
|
||||
// When the mode starts this function will be called.
|
||||
onSetup: function () {
|
||||
const rectangle = this.newFeature({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
isRect: true,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [[]],
|
||||
},
|
||||
});
|
||||
this.addFeature(rectangle);
|
||||
this.clearSelectedFeatures();
|
||||
doubleClickZoom.disable(this);
|
||||
this.updateUIClasses({ mouse: 'add' });
|
||||
this.setActionableState({
|
||||
trash: true,
|
||||
});
|
||||
return {
|
||||
rectangle,
|
||||
};
|
||||
},
|
||||
// support mobile taps
|
||||
onTap: function (state, e) {
|
||||
// emulate 'move mouse' to update feature coords
|
||||
if (state.startPoint) this.onMouseMove(state, e);
|
||||
// emulate onClick
|
||||
this.onClick(state, e);
|
||||
},
|
||||
// Whenever a user clicks on the map, Draw will call `onClick`
|
||||
onClick: function (state, e) {
|
||||
// if state.startPoint exist, means its second click
|
||||
//change to simple_select mode
|
||||
if (
|
||||
state.startPoint &&
|
||||
state.startPoint[0] !== e.lngLat.lng &&
|
||||
state.startPoint[1] !== e.lngLat.lat
|
||||
) {
|
||||
this.updateUIClasses({ mouse: 'pointer' });
|
||||
state.endPoint = [e.lngLat.lng, e.lngLat.lat];
|
||||
this.changeMode('simple_select', { featuresId: state.rectangle.id });
|
||||
}
|
||||
// on first click, save clicked point coords as starting for rectangle
|
||||
const startPoint = [e.lngLat.lng, e.lngLat.lat];
|
||||
state.startPoint = startPoint;
|
||||
},
|
||||
onMouseMove: function (state, e) {
|
||||
// if startPoint, update the feature coordinates, using the bounding box concept
|
||||
// we are simply using the startingPoint coordinates and the current Mouse Position
|
||||
// coordinates to calculate the bounding box on the fly, which will be our rectangle
|
||||
if (state.startPoint) {
|
||||
state.rectangle.updateCoordinate('0.0', state.startPoint[0], state.startPoint[1]); //minX, minY - the starting point
|
||||
state.rectangle.updateCoordinate('0.1', e.lngLat.lng, state.startPoint[1]); // maxX, minY
|
||||
state.rectangle.updateCoordinate('0.2', e.lngLat.lng, e.lngLat.lat); // maxX, maxY
|
||||
state.rectangle.updateCoordinate('0.3', state.startPoint[0], e.lngLat.lat); // minX,maxY
|
||||
state.rectangle.updateCoordinate('0.4', state.startPoint[0], state.startPoint[1]); //minX,minY - ending point (equals to starting point)
|
||||
}
|
||||
},
|
||||
// Whenever a user clicks on a key while focused on the map, it will be sent here
|
||||
onKeyUp: function (state, e) {
|
||||
if (e.keyCode === 27) return this.changeMode('simple_select');
|
||||
},
|
||||
onStop: function (state) {
|
||||
doubleClickZoom.enable(this);
|
||||
this.updateUIClasses({ mouse: 'none' });
|
||||
this.activateUIButton();
|
||||
|
||||
// check to see if we've deleted this feature
|
||||
if (this.getFeature(state.rectangle.id) === undefined) return;
|
||||
|
||||
//remove last added coordinate
|
||||
state.rectangle.removeCoordinate('0.4');
|
||||
if (state.rectangle.isValid()) {
|
||||
this.map.fire('draw.create', {
|
||||
features: [state.rectangle.toGeoJSON()],
|
||||
});
|
||||
} else {
|
||||
this.deleteFeature([state.rectangle.id], { silent: true });
|
||||
this.changeMode('simple_select', {}, { silent: true });
|
||||
}
|
||||
},
|
||||
toDisplayFeatures: function (state, geojson, display) {
|
||||
const isActivePolygon = geojson.properties.id === state.rectangle.id;
|
||||
geojson.properties.active = isActivePolygon ? 'true' : 'false';
|
||||
if (!isActivePolygon) return display(geojson);
|
||||
|
||||
// Only render the rectangular polygon if it has the starting point
|
||||
if (!state.startPoint) return;
|
||||
return display(geojson);
|
||||
},
|
||||
onTrash: function (state) {
|
||||
this.deleteFeature([state.rectangle.id], { silent: true });
|
||||
this.changeMode('simple_select');
|
||||
},
|
||||
};
|
||||
|
||||
export default DrawRectangle;
|
@ -0,0 +1,86 @@
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||
import createSupplementaryPoints from '@mapbox/mapbox-gl-draw';
|
||||
import moveFeatures from '@mapbox/mapbox-gl-draw';
|
||||
import createVertex from '@mapbox/mapbox-gl-draw';
|
||||
import { lineToPoly } from './utils';
|
||||
const { constants } = MapboxDraw;
|
||||
|
||||
function createSupplementaryPointsForCircle(geojson) {
|
||||
const { properties, geometry } = geojson;
|
||||
|
||||
if (!properties.user_isCircle) return null;
|
||||
|
||||
const supplementaryPoints = [];
|
||||
const vertices = geometry.coordinates[0].slice(0, -1);
|
||||
for (let index = 0; index < vertices.length; index += Math.round(vertices.length / 4)) {
|
||||
supplementaryPoints.push(createVertex(properties.id, vertices[index], `0.${index}`, false));
|
||||
}
|
||||
return supplementaryPoints;
|
||||
}
|
||||
const drawSimpleSelectMode = { ...MapboxDraw.modes.simple_select };
|
||||
|
||||
drawSimpleSelectMode.dragMove = function (state, e) {
|
||||
// Dragging when drag move is enabled
|
||||
state.dragMoving = true;
|
||||
e.originalEvent.stopPropagation();
|
||||
|
||||
const delta = {
|
||||
lng: e.lngLat.lng - state.dragMoveLocation.lng,
|
||||
lat: e.lngLat.lat - state.dragMoveLocation.lat,
|
||||
};
|
||||
|
||||
moveFeatures(this.getSelected(), delta);
|
||||
|
||||
this.getSelected()
|
||||
.filter((feature) => feature.properties.isCircle)
|
||||
.map((circle) => circle.properties.center)
|
||||
.forEach((center) => {
|
||||
center[0] += delta.lng;
|
||||
center[1] += delta.lat;
|
||||
});
|
||||
|
||||
state.dragMoveLocation = e.lngLat;
|
||||
};
|
||||
|
||||
drawSimpleSelectMode.toDisplayFeatures = function (state, geojson, display) {
|
||||
geojson.properties.active = this.isSelected(geojson.properties.id)
|
||||
? constants.activeStates.ACTIVE
|
||||
: constants.activeStates.INACTIVE;
|
||||
|
||||
if (geojson.properties.user_name === 'line_select') {
|
||||
const union = lineToPoly(geojson);
|
||||
display(union);
|
||||
}
|
||||
display(geojson);
|
||||
this.fireActionable();
|
||||
//如果是线 每次都创建点
|
||||
if (
|
||||
geojson?.properties.active !== constants.activeStates.ACTIVE &&
|
||||
geojson.geometry.type === constants.geojsonTypes.LINE_STRING &&
|
||||
geojson.properties.user_name !== 'line_select'
|
||||
) {
|
||||
const points = createSupplementaryPoints(geojson);
|
||||
points.forEach(display);
|
||||
}
|
||||
|
||||
if (
|
||||
geojson.properties.active !== constants.activeStates.ACTIVE ||
|
||||
geojson.geometry.type === constants.geojsonTypes.POINT
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let supplementaryPoints;
|
||||
if (geojson.properties.user_isCircle) {
|
||||
supplementaryPoints = createSupplementaryPointsForCircle(geojson);
|
||||
} else {
|
||||
supplementaryPoints = createSupplementaryPoints(geojson);
|
||||
}
|
||||
supplementaryPoints.forEach(display);
|
||||
|
||||
// if(geojson.properties.)
|
||||
};
|
||||
|
||||
//阻止框选图形拖拽
|
||||
drawSimpleSelectMode.onTap = drawSimpleSelectMode.onClick = function () {};
|
||||
export default drawSimpleSelectMode;
|
@ -0,0 +1,14 @@
|
||||
import doubleClickZoom from './doubleClickZoom';
|
||||
|
||||
var StaticMode = {};
|
||||
|
||||
StaticMode.onSetup = function () {
|
||||
this.setActionableState(); // default actionable state is false for all actions
|
||||
doubleClickZoom.disable(this); //静态model 不运行双击
|
||||
return {};
|
||||
};
|
||||
|
||||
StaticMode.toDisplayFeatures = function (state, geojson, display) {
|
||||
display(geojson);
|
||||
};
|
||||
export default StaticMode;
|
75
packages/map/src/components/drawControl/Draw/index.ts
Normal file
@ -0,0 +1,75 @@
|
||||
// @ts-nocheck
|
||||
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||
import type { DrawModes } from '@mapbox/mapbox-gl-draw';
|
||||
import type { MapboxMap } from 'react-map-gl';
|
||||
import drawLineSelectMode from './drawLineSelectMode.draw.js';
|
||||
import drawCircleMode from './drawCircleMode.draw.js';
|
||||
import drawRectMode from './drawRectMode.draw.js';
|
||||
import drawDirectMode from './drawDirectMode.draw.js';
|
||||
import drawSimpleSelectMode from './drawSimpleSelectMode.draw.js';
|
||||
import drawStaticMode from './drawStaticMode.draw.js';
|
||||
import mapboxDrawStyle from './drawStyle';
|
||||
|
||||
interface DrawControl {
|
||||
modes?: DrawModes;
|
||||
}
|
||||
function noop(): void {
|
||||
/* do nothing */
|
||||
}
|
||||
//mapbox-gl-draw api
|
||||
//https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/MODES.md#life-cycle-functions
|
||||
|
||||
function drawControl(
|
||||
map: MapboxMap,
|
||||
params: DrawControl = {},
|
||||
defaultMode = 'static',
|
||||
onDrawCreate = () => {}
|
||||
) {
|
||||
const { modes: paramModes = {} } = params;
|
||||
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* MapboxDraw 里面有的
|
||||
* draw_line_string 画线
|
||||
* draw_polygon 多边形
|
||||
* draw_point
|
||||
* simple_select
|
||||
* direct_select
|
||||
*
|
||||
* 需要自定义的
|
||||
* draw_line_select; 路径框选1
|
||||
* direct_select;
|
||||
* simple_select;
|
||||
* draw_circle;1
|
||||
* draw_rect;1
|
||||
*
|
||||
* 用MapboxDraw的
|
||||
* draw_polygon 多边形
|
||||
* draw_line_string 画线
|
||||
*/
|
||||
|
||||
const draw = new MapboxDraw({
|
||||
defaultMode: defaultMode,
|
||||
displayControlsDefault: false, // 取消默认的按钮
|
||||
styles: mapboxDrawStyle,
|
||||
modes: {
|
||||
...MapboxDraw.modes,
|
||||
draw_line_select: drawLineSelectMode,
|
||||
draw_line_string: drawLineSelectMode,
|
||||
draw_circle: drawCircleMode,
|
||||
draw_rect: drawRectMode,
|
||||
direct_select: drawDirectMode,
|
||||
simple_select: drawSimpleSelectMode,
|
||||
static: drawStaticMode,
|
||||
...paramModes,
|
||||
},
|
||||
});
|
||||
map.addControl(draw);
|
||||
map.on('draw.create', onDrawCreate || noop);
|
||||
return draw;
|
||||
}
|
||||
|
||||
export default drawControl;
|
107
packages/map/src/components/drawControl/DrawControl.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/21
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useRef, } from 'react'
|
||||
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
||||
import {
|
||||
CircleMode,
|
||||
DragCircleMode,
|
||||
DirectMode,
|
||||
SimpleSelectMode
|
||||
// @ts-ignore
|
||||
} from 'mapbox-gl-draw-circle'
|
||||
// @ts-ignore
|
||||
import drawRectMode from 'mapbox-gl-draw-rectangle-mode'
|
||||
// @ts-ignore
|
||||
import drawStaticMode from '@mapbox/mapbox-gl-draw-static-mode'
|
||||
import { useControl } from 'react-map-gl';
|
||||
import type { ControlPosition } from 'react-map-gl';
|
||||
import { MapContextValue } from 'react-map-gl/dist/esm/components/map';
|
||||
|
||||
export type DrawControlProps = ConstructorParameters<typeof MapboxDraw>[0] & {
|
||||
position?: ControlPosition;
|
||||
|
||||
onCreate?: (evt: {features: object[]}) => void;
|
||||
onUpdate?: (evt: {features: object[]; action: string}) => void;
|
||||
onDelete?: (evt: {features: object[]}) => void;
|
||||
onRender?: (evt: {features: object[]}) => void;
|
||||
onCombine?: (evt: {features: object[]}) => void;
|
||||
onUncombine?: (evt: {features: object[]}) => void;
|
||||
onModeChange?: (evt: {features: object[]}) => void;
|
||||
onActionable?: (evt: {features: object[]}) => void;
|
||||
onSelectionChange?: (evt: {features: object[]}) => void;
|
||||
};
|
||||
|
||||
export interface DrawControlRefProps {
|
||||
drawer?: MapboxDraw & {
|
||||
modes: MapboxDraw.Modes | 'static' | 'draw_rect' | 'drag_circle' | 'draw_circle' | 'direct_select' | 'simple_select'
|
||||
}
|
||||
}
|
||||
|
||||
const DrawControl = forwardRef<DrawControlRefProps, DrawControlProps>((props, ref) => {
|
||||
const drawRef = useRef<DrawControlRefProps['drawer']>(null)
|
||||
|
||||
useControl<MapboxDraw>(
|
||||
() => {
|
||||
let draw = new MapboxDraw(
|
||||
{
|
||||
modes: {
|
||||
...MapboxDraw.modes,
|
||||
// draw_line_select: drawLineSelectMode,
|
||||
draw_rect: drawRectMode,
|
||||
drag_circle: DragCircleMode,
|
||||
draw_circle : CircleMode,
|
||||
direct_select: DirectMode,
|
||||
simple_select: SimpleSelectMode,
|
||||
static: drawStaticMode,
|
||||
},
|
||||
...props
|
||||
}
|
||||
)
|
||||
// @ts-ignore
|
||||
drawRef.current = draw
|
||||
return draw
|
||||
},
|
||||
(context: MapContextValue) => {
|
||||
const { map } = context
|
||||
map.on('draw.create', e => props.onCreate?.(e));
|
||||
map.on('draw.update', e => props.onUpdate?.(e));
|
||||
map.on('draw.delete', e => props.onDelete?.(e));
|
||||
map.on('draw.render', e => props.onRender?.(e));
|
||||
map.on('draw.combine', e => props.onCombine?.(e));
|
||||
map.on('draw.uncombine', e => props.onUncombine?.(e));
|
||||
map.on('draw.modechange', e => props.onModeChange?.(e));
|
||||
map.on('draw.actionable', e => props.onActionable?.(e));
|
||||
map.on('draw.selectionchange', e => props.onSelectionChange?.(e));
|
||||
},
|
||||
(context: MapContextValue) => {
|
||||
const { map } = context
|
||||
map.off('draw.create', props.onCreate);
|
||||
map.off('draw.update', props.onUpdate);
|
||||
map.off('draw.delete', props.onDelete);
|
||||
map.off('draw.render', e => props.onRender?.(e));
|
||||
map.off('draw.combine', e => props.onCombine?.(e));
|
||||
map.off('draw.uncombine', e => props.onUncombine?.(e));
|
||||
map.off('draw.modechange', e => props.onModeChange?.(e));
|
||||
map.off('draw.actionable', e => props.onActionable?.(e));
|
||||
map.off('draw.selectionchange', e => props.onSelectionChange?.(e));
|
||||
},
|
||||
{
|
||||
position: props.position
|
||||
}
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
drawer: drawRef.current!
|
||||
}))
|
||||
|
||||
return null;
|
||||
})
|
||||
|
||||
DrawControl.defaultProps = {
|
||||
onCreate: () => {},
|
||||
onUpdate: () => {},
|
||||
onDelete: () => {}
|
||||
};
|
||||
|
||||
export default DrawControl
|
8
packages/map/src/components/drawControl/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/21
|
||||
*/
|
||||
import DrawControl from './DrawControl'
|
||||
|
||||
export type { DrawControlProps, DrawControlRefProps } from './DrawControl'
|
||||
|
||||
export default DrawControl
|
114
packages/map/src/components/marker/Marker.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/23
|
||||
*/
|
||||
import React, { forwardRef, ReactNode, useContext, useImperativeHandle, useState } from 'react'
|
||||
import {
|
||||
Marker as MapboxMarker,
|
||||
MarkerProps as MapboxMarkerProps,
|
||||
MarkerEvent
|
||||
} from 'react-map-gl'
|
||||
import { Checkbox, ConfigProvider, Image } from '@zhst/meta'
|
||||
import classNames from 'classnames'
|
||||
import cameraBlue from '../../assets/icons/camera_blue.png'
|
||||
import cameraGreen from '../../assets/icons/camera_green.png'
|
||||
import cameraGrey from '../../assets/icons/camera_grey.png'
|
||||
import cameraRed from '../../assets/icons/camera_red.png'
|
||||
import cameraYellow from '../../assets/icons/camera_yellow.png'
|
||||
import './index.less'
|
||||
|
||||
const { ConfigContext } = ConfigProvider
|
||||
|
||||
const PIC_MAP = new Map([
|
||||
['camera_blue', cameraBlue],
|
||||
['camera_green', cameraGreen],
|
||||
['camera_grey', cameraGrey],
|
||||
['camera_red', cameraRed],
|
||||
['camera_yellow', cameraYellow],
|
||||
])
|
||||
|
||||
// @ts-ignore
|
||||
export interface MarkerProps extends MapboxMarkerProps {
|
||||
prefixCls?: string;
|
||||
key: string;
|
||||
id: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
showCheckBox?: boolean
|
||||
showTooltip?: boolean
|
||||
type?: 'camera' | 'cluster' | 'marker';
|
||||
population?: string;
|
||||
status?: 'blue' | 'green' | 'yellow' | 'grey' | 'red_border' | 'escape' | 'escape_border' | 'red_track'; // 摄像头状态
|
||||
onClick?: (e?: MarkerEvent, data?: MarkerProps) => void;
|
||||
onMarkerClick?: (e?: MouseEvent, checked?: boolean, data?: MarkerProps) => void;
|
||||
customMarkerRender?: (data: MarkerProps) => ReactNode
|
||||
}
|
||||
|
||||
export interface MarkerRefProps {
|
||||
}
|
||||
|
||||
const Marker = forwardRef<MarkerRefProps, MarkerProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
checked = false,
|
||||
type = 'camera',
|
||||
status = 'blue',
|
||||
disabled,
|
||||
showCheckBox,
|
||||
showTooltip,
|
||||
onClick,
|
||||
onMarkerClick,
|
||||
customMarkerRender,
|
||||
...rest
|
||||
} = props
|
||||
const { getPrefixCls } = useContext(ConfigContext)
|
||||
const componentName = getPrefixCls('-map-marker', customizePrefixCls);
|
||||
const [showTitle, setShowTitle] = useState(showTooltip)
|
||||
|
||||
useImperativeHandle(ref, () => ({}))
|
||||
|
||||
return (
|
||||
<MapboxMarker
|
||||
anchor="bottom"
|
||||
// @ts-ignore
|
||||
onClick={e => {
|
||||
e.originalEvent.stopPropagation();
|
||||
// @ts-ignore
|
||||
onClick?.(e, props)
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{/* 自定义marker */}
|
||||
{customMarkerRender?.(props) || (
|
||||
<div className={componentName}>
|
||||
{showCheckBox && (
|
||||
<Checkbox
|
||||
className={classNames(`${componentName}-checkbox`)}
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
// @ts-ignore
|
||||
onMarkerClick?.(e, checked, props)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
onMouseEnter={() => setShowTitle(true)}
|
||||
onMouseLeave={() => setShowTitle(false)}
|
||||
src={PIC_MAP.get(`${type}_${status}`)}
|
||||
preview={false}
|
||||
/>
|
||||
{showTitle && (
|
||||
<div className={classNames(`${componentName}-title`)}>
|
||||
{props.title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</MapboxMarker>
|
||||
)
|
||||
})
|
||||
|
||||
export default Marker
|
35
packages/map/src/components/marker/index.less
Normal file
@ -0,0 +1,35 @@
|
||||
.zhst--map-marker {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&-checkbox {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-title {
|
||||
padding: 3px 6px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -36px;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 3px 3px 6px 3px rgba(0, 0, 0, 20%);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 6px solid;
|
||||
border-color: transparent transparent #fff;
|
||||
}
|
||||
}
|
||||
}
|
8
packages/map/src/components/marker/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/23
|
||||
*/
|
||||
import Marker from './Marker'
|
||||
|
||||
export type { MarkerProps, MarkerRefProps } from './Marker'
|
||||
|
||||
export default Marker
|
21
packages/map/src/components/marker/pin.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const ICON = `M20.2,15.7L20.2,15.7c1.1-1.6,1.8-3.6,1.8-5.7c0-5.6-4.5-10-10-10S2,4.5,2,10c0,2,0.6,3.9,1.6,5.4c0,0.1,0.1,0.2,0.2,0.3
|
||||
c0,0,0.1,0.1,0.1,0.2c0.2,0.3,0.4,0.6,0.7,0.9c2.6,3.1,7.4,7.6,7.4,7.6s4.8-4.5,7.4-7.5c0.2-0.3,0.5-0.6,0.7-0.9
|
||||
C20.1,15.8,20.2,15.8,20.2,15.7z`;
|
||||
|
||||
const pinStyle = {
|
||||
cursor: 'pointer',
|
||||
fill: '#d00',
|
||||
stroke: 'none'
|
||||
};
|
||||
|
||||
function Pin({size = 20}) {
|
||||
return (
|
||||
<svg height={size} viewBox="0 0 24 24" style={pinStyle}>
|
||||
<path d={ICON} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Pin);
|
58
packages/map/src/components/popup/PopUp.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/23
|
||||
*/
|
||||
import React, { forwardRef, useContext, useImperativeHandle } from 'react'
|
||||
import {
|
||||
Popup as MapboxPopUp,
|
||||
PopupProps as MapboxPopupProps
|
||||
} from 'react-map-gl'
|
||||
import { ConfigProvider, Image } from '@zhst/meta'
|
||||
import './index.less'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const { ConfigContext } = ConfigProvider
|
||||
|
||||
export interface PopUpProps extends MapboxPopupProps {
|
||||
prefixCls?: string;
|
||||
size?: number;
|
||||
title?: string;
|
||||
url?: string;
|
||||
content?: string;
|
||||
contentRender?: (props: PopUpProps) => void;
|
||||
}
|
||||
|
||||
export interface PopUpRefProps {
|
||||
}
|
||||
|
||||
const PopUp = forwardRef<PopUpRefProps, PopUpProps>((props, ref) => {
|
||||
const {
|
||||
title,
|
||||
content,
|
||||
url,
|
||||
contentRender,
|
||||
prefixCls: customizePrefixCls,
|
||||
...rest
|
||||
} = props
|
||||
const { getPrefixCls } = useContext(ConfigContext)
|
||||
const componentName = getPrefixCls('map-popup', customizePrefixCls);
|
||||
|
||||
useImperativeHandle(ref, () => ({}))
|
||||
|
||||
return (
|
||||
<MapboxPopUp
|
||||
anchor="top"
|
||||
className={componentName}
|
||||
{...rest}
|
||||
>
|
||||
{contentRender?.(props) || (
|
||||
<div className={classNames(`${componentName}-container`)}>
|
||||
{title && <h2>{title}</h2>}
|
||||
{url && <Image src={url} />}
|
||||
{content && <p>{content}</p>}
|
||||
</div>
|
||||
)}
|
||||
</MapboxPopUp>
|
||||
)
|
||||
})
|
||||
|
||||
export default PopUp
|
11
packages/map/src/components/popup/index.less
Normal file
@ -0,0 +1,11 @@
|
||||
.zhst-map-popup {
|
||||
.mapboxgl-popup-content {
|
||||
padding: 12px 6px 6px;
|
||||
}
|
||||
|
||||
&-container {
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
7
packages/map/src/components/popup/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/23
|
||||
*/
|
||||
import PopUp from './PopUp'
|
||||
export type { PopUpProps, PopUpRefProps } from './PopUp'
|
||||
|
||||
export default PopUp
|
67
packages/map/src/components/tools/Tools.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/21
|
||||
*/
|
||||
|
||||
import React, { forwardRef, ReactNode, useContext, useImperativeHandle } from 'react'
|
||||
import { Button, ConfigProvider } from '@zhst/meta'
|
||||
import classNames from 'classnames'
|
||||
import { IconFont } from '@zhst/icon'
|
||||
import './index.less'
|
||||
|
||||
const { ConfigContext } = ConfigProvider
|
||||
|
||||
export interface ToolsProps {
|
||||
prefixCls?: string
|
||||
open?: boolean;
|
||||
onToolClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void
|
||||
buttonList?: {
|
||||
label: string
|
||||
key: string
|
||||
icon?: ReactNode | string
|
||||
onClick?: () => void
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface ToolsRefProps {
|
||||
}
|
||||
|
||||
const Tools = forwardRef<ToolsRefProps, ToolsProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
buttonList = [],
|
||||
onToolClick,
|
||||
open
|
||||
} = props
|
||||
const { getPrefixCls } = useContext(ConfigContext)
|
||||
const componentName = getPrefixCls('map-tools', customizePrefixCls)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className={componentName}>
|
||||
<ul className={classNames(`${componentName}-navs`, { [`${componentName}-navs_active`]: open })}>
|
||||
{buttonList.map((item) => (
|
||||
<li onClick={item.onClick} className={classNames(`${componentName}-navs-item`)} key={item.key}>
|
||||
{typeof item.icon === 'string' ? (
|
||||
<IconFont className={classNames(`${componentName}-navs-item-icon`)} size={24} icon={item.icon} />
|
||||
) : (
|
||||
item.icon
|
||||
)}
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
size='large'
|
||||
shape="circle"
|
||||
type="primary"
|
||||
onClick={onToolClick}
|
||||
icon={<IconFont icon={open ? 'icon-guanbi' : 'icon-kuangxuangongju'} />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Tools
|
64
packages/map/src/components/tools/index.less
Normal file
@ -0,0 +1,64 @@
|
||||
.zhst-map-tools {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
|
||||
&-navs {
|
||||
display: none;
|
||||
position: relative;
|
||||
left: 24px;
|
||||
margin: 0;
|
||||
margin-right: 12px;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
font-size: 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid #09f;
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
transition: .3s ease all;
|
||||
|
||||
&_active {
|
||||
display: block;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
padding: 6px 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #09f;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 1px;
|
||||
height: 36%;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
background-color: #f0f0f0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
packages/map/src/components/tools/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Created by jiangzhixiong on 2024/05/21
|
||||
*/
|
||||
import Tools from './Tools'
|
||||
|
||||
export type { ToolsProps, ToolsRefProps } from './Tools'
|
||||
|
||||
export default Tools
|
46
packages/map/src/demo/.data/bart-station.json
Normal file
@ -0,0 +1,46 @@
|
||||
[
|
||||
{"name":"Lafayette (LAFY)","coordinates":[-122.123801,37.893394]},
|
||||
{"name":"12th St. Oakland City Center (12TH)","coordinates":[-122.271604,37.803664]},
|
||||
{"name":"16th St. Mission (16TH)","coordinates":[-122.419694,37.765062]},
|
||||
{"name":"19th St. Oakland (19TH)","coordinates":[-122.269029,37.80787]},
|
||||
{"name":"24th St. Mission (24TH)","coordinates":[-122.418466,37.752254]},
|
||||
{"name":"Ashby (ASHB)","coordinates":[-122.26978,37.853024]},
|
||||
{"name":"Balboa Park (BALB)","coordinates":[-122.447414,37.721981]},
|
||||
{"name":"Bay Fair (BAYF)","coordinates":[-122.126871,37.697185]},
|
||||
{"name":"Castro Valley (CAST)","coordinates":[-122.075567,37.690754]},
|
||||
{"name":"Civic Center/UN Plaza (CIVC)","coordinates":[-122.413756,37.779528]},
|
||||
{"name":"Colma (COLM)","coordinates":[-122.466233,37.684638]},
|
||||
{"name":"Coliseum/Oakland Airport (COLS)","coordinates":[-122.197273,37.754006]},
|
||||
{"name":"Concord (CONC)","coordinates":[-122.029095,37.973737]},
|
||||
{"name":"Daly City (DALY)","coordinates":[-122.469081,37.706121]},
|
||||
{"name":"Downtown Berkeley (DBRK)","coordinates":[-122.268045,37.869867]},
|
||||
{"name":"El Cerrito del Norte (DELN)","coordinates":[-122.317269,37.925655]},
|
||||
{"name":"Dublin/Pleasanton (DUBL)","coordinates":[-121.900367,37.701695]},
|
||||
{"name":"Embarcadero (EMBR)","coordinates":[-122.396742,37.792976]},
|
||||
{"name":"Fremont (FRMT)","coordinates":[-121.9764,37.557355]},
|
||||
{"name":"Fruitvale (FTVL)","coordinates":[-122.224274,37.774963]},
|
||||
{"name":"Glen Park (GLEN)","coordinates":[-122.434092,37.732921]},
|
||||
{"name":"Hayward (HAYW)","coordinates":[-122.087967,37.670399]},
|
||||
{"name":"Lake Merritt (LAKE)","coordinates":[-122.265609,37.797484]},
|
||||
{"name":"MacArthur (MCAR)","coordinates":[-122.267227,37.828415]},
|
||||
{"name":"Millbrae (MLBR)","coordinates":[-122.38666,37.599787]},
|
||||
{"name":"Montgomery St. (MONT)","coordinates":[-122.401407,37.789256]},
|
||||
{"name":"North Berkeley (NBRK)","coordinates":[-122.283451,37.87404]},
|
||||
{"name":"North Concord/Martinez (NCON)","coordinates":[-122.024597,38.003275]},
|
||||
{"name":"Orinda (ORIN)","coordinates":[-122.183791,37.878361]},
|
||||
{"name":"Pleasant Hill/Contra Costa Centre (PHIL)","coordinates":[-122.056013,37.928403]},
|
||||
{"name":"Pittsburg/Bay Point (PITT)","coordinates":[-121.945154,38.018914]},
|
||||
{"name":"El Cerrito Plaza (PLZA)","coordinates":[-122.299272,37.903059]},
|
||||
{"name":"Powell St. (POWL)","coordinates":[-122.406857,37.784991]},
|
||||
{"name":"Richmond (RICH)","coordinates":[-122.353165,37.936887]},
|
||||
{"name":"Rockridge (ROCK)","coordinates":[-122.251793,37.844601]},
|
||||
{"name":"San Leandro (SANL)","coordinates":[-122.161311,37.722619]},
|
||||
{"name":"San Bruno (SBRN)","coordinates":[-122.416038,37.637753]},
|
||||
{"name":"San Francisco Int'l Airport (SFIA)","coordinates":[-122.392612,37.616035]},
|
||||
{"name":"South Hayward (SHAY)","coordinates":[-122.057551,37.6348]},
|
||||
{"name":"South San Francisco (SSAN)","coordinates":[-122.444116,37.664174]},
|
||||
{"name":"Union City (UCTY)","coordinates":[-122.017867,37.591208]},
|
||||
{"name":"Walnut Creek (WCRK)","coordinates":[-122.067423,37.905628]},
|
||||
{"name":"West Dublin/Pleasanton (WDUB)","coordinates":[-121.928099,37.699759]},
|
||||
{"name":"West Oakland (WOAK)","coordinates":[-122.294582,37.804675]}
|
||||
]
|
2262
packages/map/src/demo/.data/feature-example-sf.json
Normal file
240
packages/map/src/demo/.data/mock.ts
Normal file
@ -0,0 +1,240 @@
|
||||
export const sluterData = {
|
||||
"features": [
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-151.5129,
|
||||
63.1016,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-150.4048,
|
||||
63.1224,
|
||||
105.5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-151.3597,
|
||||
63.0781,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-118.497,
|
||||
34.299667,
|
||||
7.64
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-87.6901,
|
||||
12.0623,
|
||||
46.41
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-151.5053,
|
||||
63.0719,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-178.4576,
|
||||
-20.2873,
|
||||
614.26
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-148.789,
|
||||
63.1725,
|
||||
7.5
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-120.993164,
|
||||
36.421833,
|
||||
6.37
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-117.0155,
|
||||
33.656333,
|
||||
12.37
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-151.512,
|
||||
63.0879,
|
||||
10.8
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-151.4378,
|
||||
63.0933,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-149.6538,
|
||||
63.2272,
|
||||
96.8
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-151.5325,
|
||||
63.0844,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-149.4752,
|
||||
61.8518,
|
||||
54.3
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-150.8597,
|
||||
61.6214,
|
||||
50.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-149.7142,
|
||||
62.9656,
|
||||
93.6
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-151.2484,
|
||||
61.2705,
|
||||
69.1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-152.0732,
|
||||
65.5942,
|
||||
14.8
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-90.5445,
|
||||
13.5146,
|
||||
54.36
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-118.819504,
|
||||
37.605499,
|
||||
4.14
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-118.930168,
|
||||
37.636833,
|
||||
-0.71
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
-117.509167,
|
||||
34.1555,
|
||||
16.34
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
3109
packages/map/src/demo/.data/us-election-2016.json
Normal file
56
packages/map/src/demo/.data/us-income.geojson
Normal file
@ -1,24 +1,191 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { MapBox } from '@zhst/map';
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { MapBox, Marker } from '@zhst/map';
|
||||
import axios from 'axios';
|
||||
import { FloatButton, Switch } from '@zhst/meta';
|
||||
import { MarkerProps } from '../components/marker';
|
||||
import { getDistancesByStringLine, lineToPoly } from '../utils';
|
||||
// import { sluterData } from './.data/mock.ts'
|
||||
|
||||
const demo = () => {
|
||||
const [markerData, setMarkerData] = useState<MarkerProps[]>([])
|
||||
const [showMarker, setShowMarker] = useState(true)
|
||||
const [showCluster, setShowCluster] = useState(true)
|
||||
const [canDraw, setCanDraw] = useState(false)
|
||||
const [toolsBarOpen, setToolsBarOpen] = useState(false)
|
||||
const [popupInfo, setPopupInfo] = useState()
|
||||
const [sluterData, setSluterData] = useState({})
|
||||
const [showMouseTag, setShowMouseTag] = useState(false)
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||
const [rangingList, setRangingList] = useState([])
|
||||
const mapRef = useRef(null);
|
||||
|
||||
// 初始化
|
||||
const handleMapLoad = (e: mapboxgl.MapboxEvent<undefined>) => {
|
||||
const map = e.target;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!map) return
|
||||
|
||||
map.flyTo({
|
||||
center: [120,30],
|
||||
zoom: map?.getMaxZoom(),
|
||||
// center: [120,30],
|
||||
// zoom: map?.getMaxZoom(),
|
||||
});
|
||||
};
|
||||
|
||||
const getData = async () => {
|
||||
let res = await axios({
|
||||
method: 'post',
|
||||
url: 'http://10.0.0.120:30003/singer.DeviceService/ListCamera',
|
||||
headers: {
|
||||
Authorization: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTcwNDAzNzEsImp0aSI6IjExMjgiLCJpYXQiOjE3MTY3ODExNzEsInVzZXJJZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImRhdGFSaWdodCI6MiwiY2FtZXJhUmlnaHQiOjEsImdwdVJpZ2h0IjoxLCJ1c2VybGVhZGVySWQiOjAsIm9yZ2FuaXphdGlvbklkIjoxLCJyb2xlSWQiOjF9.XHbXIkXkfUuvqV6_qSV4d20xj-s7I0qOQZgL-zspMDc'
|
||||
},
|
||||
data: {"labelData":[],"filter":{"realtimeProcessingFilter":0,"cameraFilter":[{"opt":"ORNOT","cameraOpt":"CAMERAOPT_TYPE","value":"100"}]},"maxResults":50}
|
||||
});
|
||||
if (res.status === 200) {
|
||||
let markers = []
|
||||
let sluters = {
|
||||
features: []
|
||||
}
|
||||
res.data.cameras?.forEach(camera => {
|
||||
markers.push({
|
||||
key: camera.id,
|
||||
id: camera.id,
|
||||
title: camera.name,
|
||||
population: camera.id,
|
||||
checked: false,
|
||||
disabled: false,
|
||||
showCheckBox: true,
|
||||
status: ['blue', 'yellow', 'red'][Math.floor(Math.random() * 3)],
|
||||
latitude: camera.latitude,
|
||||
longitude: camera.longitude
|
||||
})
|
||||
sluters.features.push({
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
camera.longitude,
|
||||
camera.latitude,
|
||||
50
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
setMarkerData(markers)
|
||||
setSluterData(sluters)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MapBox onLoad={handleMapLoad}
|
||||
mapRef={mapRef}
|
||||
style={{ width: '100%', height: 300 }}/>
|
||||
</div>
|
||||
<div>
|
||||
<FloatButton>
|
||||
<Switch value={true}/>
|
||||
</FloatButton>
|
||||
<MapBox
|
||||
onLoad={handleMapLoad}
|
||||
ref={mapRef}
|
||||
draw={canDraw}
|
||||
toolsBarOpen={toolsBarOpen}
|
||||
onToolClick={e => {
|
||||
setCanDraw(pre => !pre)
|
||||
setToolsBarOpen(pre => !pre)
|
||||
}}
|
||||
onDrawCreate={e => {
|
||||
const geojson = e.features[0]
|
||||
const polygonJson = getDistancesByStringLine(geojson) || []
|
||||
setRangingList(polygonJson.distanceArr)
|
||||
}}
|
||||
onDrawDelete={e => console.log('Delete', e)}
|
||||
onDrawUpdate={e => {
|
||||
const geojson = e.features[0]
|
||||
const polygonJson = getDistancesByStringLine(geojson) || []
|
||||
setRangingList(polygonJson.distanceArr)
|
||||
}}
|
||||
width='100%'
|
||||
height='100vh'
|
||||
mapStyle={{
|
||||
sources: {
|
||||
'osm-tiles': {
|
||||
'type': 'raster',
|
||||
tiles: [`http://10.0.0.120:30003/map/api/tilesets/mapfile/{z}/{x}/{y}.png`], //在线地址,先写死120
|
||||
}
|
||||
},
|
||||
}}
|
||||
markerData={markerData || []}
|
||||
showMarker={showMarker}
|
||||
showPopUp
|
||||
sluterData={sluterData}
|
||||
showCluster={showCluster}
|
||||
// popUpInfo={popupInfo}
|
||||
onPopUpClose={e => setPopupInfo(null)}
|
||||
onMarkerClick={(e, status, data) => {
|
||||
setMarkerData(pre => pre.map(item => {
|
||||
if (item.id === data.id) {
|
||||
item.checked = !data?.checked
|
||||
}
|
||||
return item
|
||||
}))
|
||||
// setPopupInfo({
|
||||
// longitude: data?.longitude,
|
||||
// latitude: data?.latitude,
|
||||
// content: '测试'
|
||||
// })
|
||||
}}
|
||||
onZoomEnd={map => {
|
||||
let zoom = map.target.getZoom() // 画面层级,用来判断标签是否显示隐藏
|
||||
// 摄像头显示/隐藏
|
||||
setShowMarker(zoom > 5)
|
||||
// 范围统计显示/隐藏
|
||||
setShowCluster(zoom < 5)
|
||||
}}
|
||||
onClick={e => setShowMouseTag(true)}
|
||||
onDblClick={e => {
|
||||
setShowMouseTag(false)
|
||||
// setCanDraw(false)
|
||||
}}
|
||||
onMouseMove={e => {
|
||||
setMousePosition({ x: e.point.x, y: e.point.y })
|
||||
}}
|
||||
// customMarkerRender={(_data) => <div>自定义标记</div>}
|
||||
>
|
||||
{canDraw && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
fontSize: '16px',
|
||||
top: mousePosition.y,
|
||||
left: mousePosition.x,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>{showMouseTag ? '双击完成绘制' : '单击以确定起点'}</div>
|
||||
)}
|
||||
{rangingList?.map((item, index) => {
|
||||
return (
|
||||
<Marker
|
||||
key={index}
|
||||
anchor="bottom"
|
||||
longitude={item.to[0]}
|
||||
latitude={item.to[1]}
|
||||
onClick={e => e.originalEvent.stopPropagation()}
|
||||
>
|
||||
<div style={{ padding: '3px 6px', background: '#fff', border: '1px solid #000' }} >{(item.totalLength)}km</div>
|
||||
</Marker>
|
||||
)
|
||||
})}
|
||||
<Marker
|
||||
anchor="bottom"
|
||||
longitude={120}
|
||||
latitude={31}
|
||||
onClick={e => {
|
||||
e.originalEvent.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '12px', background: 'red', color: '#fff', border: '1px solid #000' }} >我是自定义的DOM!</div>
|
||||
</Marker>
|
||||
</MapBox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
0
packages/map/src/hooks/useDraw.ts
Normal file
@ -1,3 +1,9 @@
|
||||
.mapboxgl-ctrl-attrib-button {
|
||||
display: none;
|
||||
.zhst-map {
|
||||
position: relative;
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
||||
.mapboxgl-ctrl-logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ title: 快速上手
|
||||
|
||||
<embed src="../README.md" ></embed>
|
||||
<code src="./demo/basic.tsx">基本用法</code>
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
@ -18,3 +19,13 @@ title: 快速上手
|
||||
| children | 内部元素 | JSX.Element或JSX.Element[]或Array<JSX.Element或undefined> | {} | - |
|
||||
| mapConfig | 地图配置 | MapConfigProps | defaultMapConfig | - |
|
||||
| onLoad | 地图加载事件 | function | ()=>{} | - |
|
||||
|
||||
## 参考文档
|
||||
|
||||
[官方文档](https://docs.mapbox.com/mapbox-gl-js/example)
|
||||
[react-map-gl](https://visgl.github.io/react-map-gl/examples)
|
||||
[mapbox-gl-draw](https://github.com/mapbox/mapbox-gl-draw)
|
||||
[draw 绘制模式插件](https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/MODES.md#available-custom-modes)
|
||||
[mapboxJS](https://www.naivemap.com/mapbox-gl-js-cookbook/starter/handlers/control.html)
|
||||
[mapboxAPI详解](https://juejin.cn/post/6907068992607928328)
|
||||
[mapbox Expressions语法学习](https://docs.mapbox.com/help/tutorials/mapbox-gl-js-expressions/)
|
||||
|
@ -1 +1,2 @@
|
||||
export { default as MapBox } from './MapBox';
|
||||
export * from 'react-map-gl'
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { MapRef, MapStyle } from "react-map-gl";
|
||||
|
||||
export interface MapProps {
|
||||
onLoad?: (e: mapboxgl.MapboxEvent<undefined>) => void;
|
||||
mapRef?: React.MutableRefObject<MapRef | undefined>;
|
||||
style?: CSSProperties;
|
||||
children?: JSX.Element | JSX.Element[] | Array<JSX.Element | undefined>;
|
||||
mapConfig?: MapConfigProps
|
||||
mapCenter: {longitude: number, latitude: number}
|
||||
}
|
||||
|
||||
export interface MapConfigProps {
|
||||
mapboxAccessToken?: string; //token
|
||||
minZoom?: number; //最小层级
|
||||
maxZoom?: number; //最大层级
|
||||
dragRotate?: boolean; //是否支持拖拽旋转
|
||||
mapStyle?: MapStyle; //地图样式
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
const getRawType = (val: any) => {
|
||||
return Object.prototype.toString.call(val).slice(8, -1)
|
||||
}
|
||||
|
||||
const isPlainObjectOrArray = (val: any) => {
|
||||
return isPlainObject(val) || Array.isArray(val)
|
||||
}
|
||||
|
||||
const isPlainObject = (val: any) => {
|
||||
return getRawType(val) === 'Object'
|
||||
}
|
||||
|
||||
export const merge = (object: any, ...sources: any) => {
|
||||
for(const source of sources) {
|
||||
for(const key in source) {
|
||||
if(source[key] === undefined && key in object) {
|
||||
continue
|
||||
}
|
||||
if(isPlainObjectOrArray(source[key])) {
|
||||
if(getRawType(object[key] === getRawType(source[key]))) {
|
||||
if(isPlainObject(object[key])) {
|
||||
merge(object[key], source[key])
|
||||
} else {
|
||||
object[key] = object[key].concat(source[key])
|
||||
}
|
||||
} else {
|
||||
object[key] = source[key]
|
||||
}
|
||||
} else {
|
||||
object[key] = source[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
@ -1,18 +1,22 @@
|
||||
import { MapProps } from 'react-map-gl'
|
||||
|
||||
export const mapboxAccessToken =
|
||||
'pk.eyJ1IjoiZGluZ2xpMTIzIiwiYSI6ImNra204ODhjczBobTgyeHJ6MmJpZHMxNWgifQ.NbKrXh_hb2gvjr5CEMDnyQ';
|
||||
export const MAP_CENTER = {
|
||||
longitude: 120.2667694313269,
|
||||
latitude: 30.180942826533766,
|
||||
}; //地图中心
|
||||
const MapUrl = 'http://10.0.0.120:30003/map';
|
||||
export const defaultMapConfig = {
|
||||
|
||||
const mapSourceUrl = 'http://10.0.0.120:30003/map';
|
||||
|
||||
export const defaultMapConfig: MapProps = {
|
||||
mapboxAccessToken,
|
||||
maxZoom: 18,
|
||||
minZoom: 4,
|
||||
initialViewState: {
|
||||
longitude: 120.2667694313269,
|
||||
latitude: 30.180942826533766,
|
||||
},
|
||||
dragRotate: false,
|
||||
mapStyle: {
|
||||
version: 8,
|
||||
name: 'Mapbox Streets',
|
||||
name: 'zhst mapbox',
|
||||
// sprite: `${location.origin}/mapbox/sprite`, // 地图图标
|
||||
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf', // 字体
|
||||
sources: {
|
||||
@ -22,7 +26,7 @@ export const defaultMapConfig = {
|
||||
// Z-瓦片层级,一般支持0-18级,越大代表越清晰;
|
||||
// X-瓦片列号,从西向东(0->360),依次0,1,2,……;
|
||||
// Y-瓦片行号,从北向南(有些也可能是从南向北),依次0,1,2,……;
|
||||
tiles: [`${MapUrl}/api/tilesets/mapfile/{z}/{x}/{y}.png`], //在线地址,先写死120
|
||||
tiles: [`${mapSourceUrl}/api/tilesets/mapfile/{z}/{x}/{y}.png`], //在线地址,先写死120
|
||||
tileSize: 256, //切片的最小展示尺寸(可选,单位:像素,默认值为 512,即 1024/2
|
||||
},
|
||||
},
|
||||
@ -33,10 +37,11 @@ export const defaultMapConfig = {
|
||||
type: 'raster', //类型 栅格。circle,symbol,line...
|
||||
source: 'osm-tiles',
|
||||
// 'source-layer': 'osmtiles',//数据源必须是type:vector
|
||||
minZoom: 4, //最小层级
|
||||
maxZoom: 17, //最大层级
|
||||
minzoom: 4, //最小层级
|
||||
maxzoom: 17, //最大层级
|
||||
// @ts-ignore
|
||||
renderingMode: '2d',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
144
packages/map/src/utils/drawStyle.ts
Normal file
@ -0,0 +1,144 @@
|
||||
//自定义画框样式
|
||||
const mapboxDrawStyle = [
|
||||
// ACTIVE (being drawn)
|
||||
// line stroke
|
||||
|
||||
{
|
||||
id: 'gl-draw-line',
|
||||
type: 'line',
|
||||
filter: [
|
||||
'all',
|
||||
['==', '$type', 'LineString'],
|
||||
['!=', 'mode', 'static'],
|
||||
['==', 'active', 'true'],
|
||||
],
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-color': 'rgba(246,67,72,1)',
|
||||
'line-dasharray': [0.2, 2],
|
||||
'line-width': 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gl-draw-line-not-active',
|
||||
type: 'line',
|
||||
filter: ['all', ['==', '$type', 'LineString'], ['!=', 'active', 'true']],
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-color': 'rgba(246,67,72,1)',
|
||||
'line-width': 2,
|
||||
},
|
||||
},
|
||||
|
||||
// polygon fill
|
||||
{
|
||||
id: 'gl-draw-polygon-fill',
|
||||
type: 'fill',
|
||||
filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
|
||||
paint: {
|
||||
'fill-color': 'rgba(246,67,72,0.2)',
|
||||
'fill-outline-color': 'rgba(246,67,72,0.2)',
|
||||
},
|
||||
},
|
||||
// polygon outline stroke
|
||||
// This doesn't style the first edge of the polygon, which uses the line stroke styling instead
|
||||
{
|
||||
id: 'gl-draw-polygon-stroke-active',
|
||||
type: 'line',
|
||||
filter: [
|
||||
'all',
|
||||
['==', '$type', 'Polygon'],
|
||||
['!=', 'mode', 'static'],
|
||||
['==', 'active', 'false'],
|
||||
],
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-color': 'rgba(246,67,72,1)',
|
||||
'line-width': 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gl-draw-polygon-stroke-active-select',
|
||||
type: 'line',
|
||||
filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static'], ['==', 'active', 'true']],
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-color': 'rgba(246,67,72,1)',
|
||||
'line-dasharray': [0.2, 2],
|
||||
'line-width': 2,
|
||||
},
|
||||
},
|
||||
// vertex point halos
|
||||
{
|
||||
id: 'gl-draw-polygon-and-line-vertex-halo-active',
|
||||
type: 'circle',
|
||||
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
|
||||
paint: {
|
||||
'circle-radius': 5,
|
||||
'circle-color': '#FFF',
|
||||
},
|
||||
},
|
||||
// vertex points
|
||||
{
|
||||
id: 'gl-draw-polygon-and-line-vertex-active',
|
||||
type: 'circle',
|
||||
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
|
||||
paint: {
|
||||
'circle-radius': 3,
|
||||
'circle-color': 'rgba(246,67,72,1)',
|
||||
},
|
||||
},
|
||||
|
||||
// INACTIVE (static, already drawn)
|
||||
// line stroke
|
||||
{
|
||||
id: 'gl-draw-line-static',
|
||||
type: 'line',
|
||||
filter: ['all', ['==', '$type', 'LineString'], ['==', 'mode', 'static']],
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-color': '#E13F3F',
|
||||
'line-width': 3,
|
||||
},
|
||||
},
|
||||
// polygon fill
|
||||
{
|
||||
id: 'gl-draw-polygon-fill-static',
|
||||
type: 'fill',
|
||||
filter: ['all', ['==', '$type', 'Polygon'], ['==', 'mode', 'static']],
|
||||
paint: {
|
||||
'fill-color': 'rgba(225, 63, 63, 0.2)',
|
||||
'fill-outline-color': 'rgba(225, 63, 63, 0.2)',
|
||||
},
|
||||
},
|
||||
// polygon outline
|
||||
{
|
||||
id: 'gl-draw-polygon-stroke-static',
|
||||
type: 'line',
|
||||
filter: ['all', ['==', '$type', 'Polygon'], ['==', 'mode', 'static']],
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-color': '#E13F3F',
|
||||
'line-width': 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
export default mapboxDrawStyle;
|
0
packages/map/src/utils/formater.ts
Normal file
286
packages/map/src/utils/index.ts
Normal file
@ -0,0 +1,286 @@
|
||||
// @ts-ignore
|
||||
import * as turf from '@turf/turf';
|
||||
|
||||
// 判断参数类型
|
||||
const getRawType = (val: any) => {
|
||||
return Object.prototype.toString.call(val).slice(8, -1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为对象或者数组
|
||||
* @param val
|
||||
* @returns
|
||||
*/
|
||||
const isPlainObjectOrArray = (val: any) => {
|
||||
return isPlainObject(val) || Array.isArray(val)
|
||||
}
|
||||
|
||||
// 判断参数是否为对象
|
||||
const isPlainObject = (val: any) => {
|
||||
return getRawType(val) === 'Object'
|
||||
}
|
||||
|
||||
export const merge = (object: any, ...sources: any) => {
|
||||
for(const source of sources) {
|
||||
for(const key in source) {
|
||||
if(source[key] === undefined && key in object) {
|
||||
continue
|
||||
}
|
||||
if(isPlainObjectOrArray(source[key])) {
|
||||
if(getRawType(object[key] === getRawType(source[key]))) {
|
||||
if(isPlainObject(object[key])) {
|
||||
merge(object[key], source[key])
|
||||
} else {
|
||||
object[key] = object[key].concat(source[key])
|
||||
}
|
||||
} else {
|
||||
object[key] = source[key]
|
||||
}
|
||||
} else {
|
||||
object[key] = source[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
// 获取 2 个点的距离
|
||||
export const getDistance = (point1: { x: any; y: any }, point2: { x: any; y: any }) => {
|
||||
const { x: x1, y: y1 } = point1;
|
||||
const { x: x2, y: y2 } = point2;
|
||||
return Math.abs(Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)));
|
||||
};
|
||||
|
||||
//获取两点(格式例如:[120,30])间物理像素距离
|
||||
export function getPixelDistance(map: { transform: { locationPoint: (arg0: any) => any } }, pointA: any, pointB: any) {
|
||||
const pointAPixel = map.transform.locationPoint(pointA); // A点的像素坐标位置
|
||||
const pointBPixel = map.transform.locationPoint(pointB); // B点的像素坐标位置
|
||||
// A、B两点的像素坐标距离
|
||||
return getDistance(pointAPixel, pointBPixel);
|
||||
}
|
||||
|
||||
//判断是否是0值 给一个偏移量
|
||||
export const isZero = (floatValue: number) => {
|
||||
return floatValue > -0.00001 && floatValue < 0.00001;
|
||||
};
|
||||
|
||||
//判断2个向量是否平行
|
||||
export const isParallel = (vectorA: { x: any; y: any }, vectorB: { x: any; y: any }) => {
|
||||
const { x: x1, y: y1 } = vectorA;
|
||||
const { x: x2, y: y2 } = vectorB;
|
||||
return isZero(x1 * y2 - y1 * x2);
|
||||
};
|
||||
|
||||
//判断一个点是否在线段上 判断向量叉乘是否为0 且x坐标在线段2端点间
|
||||
export const isPointOnLine = (vectorA: { x: number; y: number }, path: any[]) => {
|
||||
const [vectorB, vectorC] = path;
|
||||
|
||||
//与端点重合
|
||||
if (
|
||||
(vectorA.x === vectorB.x && vectorA.y === vectorB.y) ||
|
||||
(vectorA.x === vectorC.x && vectorA.y === vectorC.y)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//在同一竖直方向,线段竖直,点在该线段所在的直线上
|
||||
if (isZero(vectorB.x - vectorC.x) && isZero(vectorB.x - vectorA.x)) {
|
||||
//已判定点在直线上,若点在两端点中间,即点在线段上
|
||||
if (
|
||||
(vectorA.y < vectorC.y && vectorA.y > vectorB.y) ||
|
||||
(vectorA.y < vectorB.y && vectorA.y > vectorC.y)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//在同一水平方向
|
||||
if (isZero(vectorB.y - vectorC.y) && isZero(vectorB.y - vectorA.y)) {
|
||||
if (
|
||||
(vectorA.x < vectorC.x && vectorA.x > vectorB.x) ||
|
||||
(vectorA.x < vectorB.x && vectorA.x > vectorC.x)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 线段倾斜,此时线段所在直线存在斜率
|
||||
// 点在直线上,AB与AC斜率相等,且有共同点A,此时AC与AB重合,即点A在直线BC上
|
||||
if (
|
||||
isZero(
|
||||
(vectorB.y - vectorA.y) / (vectorB.x - vectorA.x) -
|
||||
(vectorA.y - vectorC.y) / (vectorA.x - vectorC.x)
|
||||
)
|
||||
) {
|
||||
if (
|
||||
(vectorB.y - vectorA.y) * (vectorA.y - vectorC.y) > 0 &&
|
||||
(vectorB.x - vectorA.x) * (vectorA.x - vectorC.x) > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
//获取线段重合部分,如果重合返回合并后的线段 如果不重合 返回传入的线段
|
||||
//see https://blog.csdn.net/qq_39108767/article/details/81673921
|
||||
//see https://www.cnblogs.com/tuyang1129/p/9390376.html
|
||||
export const getLineCoincide = (path1: [any, any], path2: [any, any]) => {
|
||||
let paths: string | any[] = [];
|
||||
const [vectorA, vectorB] = path1;
|
||||
const [vectorC, vectorD] = path2;
|
||||
|
||||
const isOnLineA = isPointOnLine(vectorA, [vectorC, vectorD]);
|
||||
const isOnLineB = isPointOnLine(vectorB, [vectorC, vectorD]);
|
||||
const isOnLineC = isPointOnLine(vectorC, [vectorA, vectorB]);
|
||||
const isOnLineD = isPointOnLine(vectorD, [vectorA, vectorB]);
|
||||
|
||||
const isCollinear = isParallel(
|
||||
{ x: vectorA.x - vectorB.x, y: vectorA.y - vectorB.y },
|
||||
{ x: vectorC.x - vectorD.x, y: vectorC.y - vectorD.y }
|
||||
);
|
||||
//下面6中情况代表合并
|
||||
if (isOnLineA && isOnLineC && isCollinear) {
|
||||
paths = [[vectorB, vectorD]];
|
||||
}
|
||||
if (isOnLineA && isOnLineD && isCollinear) {
|
||||
paths = [[vectorB, vectorC]];
|
||||
}
|
||||
if (isOnLineB && isOnLineC && isCollinear) {
|
||||
paths = [[vectorA, vectorD]];
|
||||
}
|
||||
if (isOnLineB && isOnLineD && isCollinear) {
|
||||
paths = [[vectorA, vectorC]];
|
||||
}
|
||||
if (isOnLineA && isOnLineB && isCollinear) {
|
||||
paths = [[vectorC, vectorD]];
|
||||
}
|
||||
if (isOnLineC && isOnLineD && isCollinear) {
|
||||
paths = [[vectorA, vectorB]];
|
||||
}
|
||||
//未匹配到上述情况,则没有重合 原样返回
|
||||
if (paths.length === 0) {
|
||||
paths = [path1, path2];
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
// 计算与纬线的角度,正方向向上
|
||||
const calcAng = (point: number[], p: number[]) => (Math.atan2(point[1] - p[1], point[0] - p[0]) * 180) / Math.PI + 180;
|
||||
|
||||
// 多段折线转polygon,直线左右默认范围50米
|
||||
export const lineToPoly = (geojson: { geometry: { coordinates: string | any[] } }, r = 50) => {
|
||||
const linesPolygon = [];
|
||||
const circlePolygon = [];
|
||||
for (let i = 0; i < geojson.geometry.coordinates.length - 1; i++) {
|
||||
const [pointA, pointB] = [geojson.geometry.coordinates[i], geojson.geometry.coordinates[i + 1]];
|
||||
const line = turf.lineString([pointA, pointB]);
|
||||
const ang = calcAng(pointA, pointB);
|
||||
// 与经线的夹角为偏移方向,右正左负, 右偏移-左偏移 = 180
|
||||
const translatedPolyA = turf.transformTranslate(line, r / 1000, -ang);
|
||||
const translatedPolyB = turf.transformTranslate(line, r / 1000, -ang + 180);
|
||||
const _line = turf.lineString([
|
||||
// 逆时针闭合
|
||||
...translatedPolyA.geometry.coordinates.reverse(),
|
||||
...translatedPolyB.geometry.coordinates,
|
||||
]);
|
||||
const linePolygon = turf.lineToPolygon(_line);
|
||||
linesPolygon.push(linePolygon);
|
||||
circlePolygon.push(turf.circle(pointA, r / 1000));
|
||||
if (i == geojson.geometry.coordinates.length - 2) {
|
||||
circlePolygon.push(turf.circle(pointB, r / 1000));
|
||||
}
|
||||
}
|
||||
let _union;
|
||||
try {
|
||||
//todo: 新版本union和老版本行为不一致 先用老版本 后续观察原因
|
||||
_union = turf.union(...circlePolygon, ...linesPolygon);
|
||||
// _union = union(...circlePolygon, ...linesPolygon);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return _union;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取在图形中的点
|
||||
* @param _points 点坐标集
|
||||
* @param polygon 图形
|
||||
* @returns 在图形中的坐标集
|
||||
*/
|
||||
export const getPointByShape = (_points: [number, number][], polygon: [number, number][]) => {
|
||||
let points = turf.points(_points);
|
||||
// TODO:首未坐标要一致
|
||||
let searchWithin = turf.polygon([polygon]);
|
||||
|
||||
return turf.pointsWithinPolygon(points, searchWithin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过绘制的线段获取距离
|
||||
* @param geojson 点坐标集
|
||||
* @param config 额外的 turf.distance 配置(可选)
|
||||
* @returns distanceArr,totalLength
|
||||
*/
|
||||
export const getDistancesByStringLine = (
|
||||
geojson: { geometry: { coordinates: [number, number][], [key: string]: any }, type: 'line' },
|
||||
config?: any
|
||||
) => {
|
||||
let distanceArr: {
|
||||
distance: number,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
totalLength: number | string
|
||||
}[] = []
|
||||
let totalLength = 0
|
||||
for (let i = 0; i < geojson.geometry.coordinates.length - 1; i++) {
|
||||
const [pointA, pointB] = [geojson.geometry.coordinates[i], geojson.geometry.coordinates[i + 1]];
|
||||
|
||||
const tempDistance = turf.distance(pointA, pointB, config);
|
||||
totalLength += tempDistance
|
||||
distanceArr.push({
|
||||
distance: tempDistance.toFixed(2),
|
||||
totalLength: totalLength.toFixed(2),
|
||||
from: pointA,
|
||||
to: pointB
|
||||
})
|
||||
}
|
||||
return {
|
||||
distanceArr,
|
||||
totalLength: totalLength.toFixed(2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建 node,绑定 node 到对应父级,
|
||||
* @param {string} className 类名
|
||||
* @param {node} container 父级 node
|
||||
* @param {string} code data-code 属性值
|
||||
* @param {func} fn click 事件
|
||||
* @returns {node}
|
||||
*/
|
||||
export function createNode(
|
||||
node: keyof HTMLElementTagNameMap,
|
||||
className: string,
|
||||
textContent: string,
|
||||
container: HTMLElement,
|
||||
code?: string,
|
||||
fn?: EventListener
|
||||
): HTMLElement {
|
||||
let a = document.createElement(node)
|
||||
a.className = className
|
||||
a.textContent = textContent
|
||||
|
||||
if (code) a.setAttribute('data-code', code)
|
||||
|
||||
if (fn) a.addEventListener('click', fn)
|
||||
container.appendChild(a)
|
||||
return a
|
||||
}
|
||||
|
||||
export function removeNode(node: { remove: () => void; removeEventListener: (arg0: string, arg1: any) => void; }, fn: any) {
|
||||
node.remove()
|
||||
if (fn) node.removeEventListener('click', fn)
|
||||
}
|
@ -1,5 +1,16 @@
|
||||
# @zhst/utils
|
||||
|
||||
## 0.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- zhst/map:拓展地图 api,加入 tools 组件、cluster 组价、marker 组件、draw 组件、popup 组件;zhst/mata:添加 floatButton 组件
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @zhst/meta@0.22.0
|
||||
|
||||
## 0.21.2
|
||||
|
||||
### Patch Changes
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@zhst/meta",
|
||||
"version": "0.21.2",
|
||||
"version": "0.22.0",
|
||||
"description": "原子组件",
|
||||
"keywords": [
|
||||
"meta",
|
||||
|
107
packages/meta/src/float-button/BackTop.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import VerticalAlignTopOutlined from '@ant-design/icons/VerticalAlignTopOutlined';
|
||||
import classNames from 'classnames';
|
||||
import CSSMotion from 'rc-motion';
|
||||
import { composeRef } from 'rc-util/lib/ref';
|
||||
|
||||
import getScroll from '../_util/getScroll';
|
||||
import scrollTo from '../_util/scrollTo';
|
||||
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
|
||||
import type { ConfigConsumerProps } from '../config-provider';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import FloatButtonGroupContext from './context';
|
||||
import FloatButton, { floatButtonPrefixCls } from './FloatButton';
|
||||
import type {
|
||||
BackTopProps,
|
||||
FloatButtonElement,
|
||||
FloatButtonProps,
|
||||
FloatButtonRef,
|
||||
FloatButtonShape,
|
||||
} from './interface';
|
||||
|
||||
const BackTop = React.forwardRef<FloatButtonRef, BackTopProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
type = 'default',
|
||||
shape = 'circle',
|
||||
visibilityHeight = 400,
|
||||
icon = <VerticalAlignTopOutlined />,
|
||||
target,
|
||||
onClick,
|
||||
duration = 450,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const [visible, setVisible] = useState<boolean>(visibilityHeight === 0);
|
||||
|
||||
const internalRef = React.useRef<FloatButtonRef['nativeElement']>(null);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
nativeElement: internalRef.current,
|
||||
}));
|
||||
|
||||
const getDefaultTarget = (): HTMLElement | Document | Window =>
|
||||
internalRef.current && internalRef.current.ownerDocument
|
||||
? internalRef.current.ownerDocument
|
||||
: window;
|
||||
|
||||
const handleScroll = throttleByAnimationFrame(
|
||||
(e: React.UIEvent<HTMLElement, UIEvent> | { target: any }) => {
|
||||
const scrollTop = getScroll(e.target, true);
|
||||
setVisible(scrollTop >= visibilityHeight);
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const getTarget = target || getDefaultTarget;
|
||||
const container = getTarget();
|
||||
handleScroll({ target: container });
|
||||
container?.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
handleScroll.cancel();
|
||||
container?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [target]);
|
||||
|
||||
const scrollToTop: React.MouseEventHandler<FloatButtonElement> = (e) => {
|
||||
scrollTo(0, { getContainer: target || getDefaultTarget, duration });
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
|
||||
|
||||
const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls);
|
||||
const rootPrefixCls = getPrefixCls();
|
||||
|
||||
const groupShape = useContext<FloatButtonShape | undefined>(FloatButtonGroupContext);
|
||||
|
||||
const mergedShape = groupShape || shape;
|
||||
|
||||
const contentProps: FloatButtonProps = {
|
||||
prefixCls,
|
||||
icon,
|
||||
type,
|
||||
shape: mergedShape,
|
||||
...restProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<CSSMotion visible={visible} motionName={`${rootPrefixCls}-fade`}>
|
||||
{({ className: motionClassName }, setRef) => (
|
||||
<FloatButton
|
||||
ref={composeRef(internalRef, setRef)}
|
||||
{...contentProps}
|
||||
onClick={scrollToTop}
|
||||
className={classNames(className, motionClassName)}
|
||||
/>
|
||||
)}
|
||||
</CSSMotion>
|
||||
);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
BackTop.displayName = 'BackTop';
|
||||
}
|
||||
|
||||
export default BackTop;
|
126
packages/meta/src/float-button/FloatButton.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import omit from 'rc-util/lib/omit';
|
||||
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
import Badge from '../badge';
|
||||
import type { ConfigConsumerProps } from '../config-provider';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
|
||||
import Tooltip from '../tooltip';
|
||||
import type BackTop from './BackTop';
|
||||
import FloatButtonGroupContext from './context';
|
||||
import Content from './FloatButtonContent';
|
||||
import type FloatButtonGroup from './FloatButtonGroup';
|
||||
import type {
|
||||
FloatButtonBadgeProps,
|
||||
FloatButtonContentProps,
|
||||
FloatButtonElement,
|
||||
FloatButtonProps,
|
||||
FloatButtonShape,
|
||||
} from './interface';
|
||||
import type PurePanel from './PurePanel';
|
||||
import useStyle from './style';
|
||||
|
||||
export const floatButtonPrefixCls = 'float-btn';
|
||||
|
||||
const InternalFloatButton = React.forwardRef<FloatButtonElement, FloatButtonProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
rootClassName,
|
||||
type = 'default',
|
||||
shape = 'circle',
|
||||
icon,
|
||||
description,
|
||||
tooltip,
|
||||
badge = {},
|
||||
...restProps
|
||||
} = props;
|
||||
const { getPrefixCls, direction } = useContext<ConfigConsumerProps>(ConfigContext);
|
||||
const groupShape = useContext<FloatButtonShape | undefined>(FloatButtonGroupContext);
|
||||
const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls);
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
|
||||
|
||||
const mergedShape = groupShape || shape;
|
||||
|
||||
const classString = classNames(
|
||||
hashId,
|
||||
cssVarCls,
|
||||
rootCls,
|
||||
prefixCls,
|
||||
className,
|
||||
rootClassName,
|
||||
`${prefixCls}-${type}`,
|
||||
`${prefixCls}-${mergedShape}`,
|
||||
{
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
);
|
||||
|
||||
// 虽然在 ts 中已经 omit 过了,但是为了防止多余的属性被透传进来,这里再 omit 一遍,以防万一
|
||||
const badgeProps = useMemo<FloatButtonBadgeProps>(
|
||||
() => omit(badge, ['title', 'children', 'status', 'text'] as any[]),
|
||||
[badge],
|
||||
);
|
||||
|
||||
const contentProps = useMemo<FloatButtonContentProps>(
|
||||
() => ({ prefixCls, description, icon, type }),
|
||||
[prefixCls, description, icon, type],
|
||||
);
|
||||
|
||||
let buttonNode = (
|
||||
<div className={`${prefixCls}-body`}>
|
||||
<Content {...contentProps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
if ('badge' in props) {
|
||||
buttonNode = <Badge {...badgeProps}>{buttonNode}</Badge>;
|
||||
}
|
||||
|
||||
if ('tooltip' in props) {
|
||||
buttonNode = (
|
||||
<Tooltip title={tooltip} placement={direction === 'rtl' ? 'right' : 'left'}>
|
||||
{buttonNode}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const warning = devUseWarning('FloatButton');
|
||||
|
||||
warning(
|
||||
!(shape === 'circle' && description),
|
||||
'usage',
|
||||
'supported only when `shape` is `square`. Due to narrow space for text, short sentence is recommended.',
|
||||
);
|
||||
}
|
||||
|
||||
return wrapCSSVar(
|
||||
props.href ? (
|
||||
<a ref={ref} {...restProps} className={classString}>
|
||||
{buttonNode}
|
||||
</a>
|
||||
) : (
|
||||
<button ref={ref} {...restProps} className={classString} type="button">
|
||||
{buttonNode}
|
||||
</button>
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
type CompoundedComponent = typeof InternalFloatButton & {
|
||||
Group: typeof FloatButtonGroup;
|
||||
BackTop: typeof BackTop;
|
||||
_InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
|
||||
};
|
||||
|
||||
const FloatButton = InternalFloatButton as CompoundedComponent;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
FloatButton.displayName = 'FloatButton';
|
||||
}
|
||||
|
||||
export default FloatButton;
|
34
packages/meta/src/float-button/FloatButtonContent.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { memo } from 'react';
|
||||
import FileTextOutlined from '@ant-design/icons/FileTextOutlined';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { FloatButtonContentProps } from './interface';
|
||||
|
||||
const FloatButtonContent: React.FC<FloatButtonContentProps> = (props) => {
|
||||
const { icon, description, prefixCls, className } = props;
|
||||
const defaultElement = (
|
||||
<div className={`${prefixCls}-icon`}>
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
onClick={props.onClick}
|
||||
onFocus={props.onFocus}
|
||||
onMouseEnter={props.onMouseEnter}
|
||||
onMouseLeave={props.onMouseLeave}
|
||||
className={classNames(className, `${prefixCls}-content`)}
|
||||
>
|
||||
{icon || description ? (
|
||||
<>
|
||||
{icon && <div className={`${prefixCls}-icon`}>{icon}</div>}
|
||||
{description && <div className={`${prefixCls}-description`}>{description}</div>}
|
||||
</>
|
||||
) : (
|
||||
defaultElement
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FloatButtonContent);
|
141
packages/meta/src/float-button/FloatButtonGroup.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import React, { memo, useCallback, useContext, useEffect } from 'react';
|
||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import FileTextOutlined from '@ant-design/icons/FileTextOutlined';
|
||||
import classNames from 'classnames';
|
||||
import CSSMotion from 'rc-motion';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
import type { ConfigConsumerProps } from '../config-provider';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
|
||||
import { FloatButtonGroupProvider } from './context';
|
||||
import FloatButton, { floatButtonPrefixCls } from './FloatButton';
|
||||
import type { FloatButtonGroupProps, FloatButtonRef } from './interface';
|
||||
import useStyle from './style';
|
||||
|
||||
const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
style,
|
||||
shape = 'circle',
|
||||
type = 'default',
|
||||
icon = <FileTextOutlined />,
|
||||
closeIcon,
|
||||
description,
|
||||
trigger,
|
||||
children,
|
||||
onOpenChange,
|
||||
open: customOpen,
|
||||
...floatButtonProps
|
||||
} = props;
|
||||
|
||||
const { direction, getPrefixCls, floatButtonGroup } =
|
||||
useContext<ConfigConsumerProps>(ConfigContext);
|
||||
|
||||
const mergedCloseIcon = closeIcon ?? floatButtonGroup?.closeIcon ?? <CloseOutlined />;
|
||||
|
||||
const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls);
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
|
||||
const groupPrefixCls = `${prefixCls}-group`;
|
||||
|
||||
const groupCls = classNames(groupPrefixCls, hashId, cssVarCls, rootCls, className, {
|
||||
[`${groupPrefixCls}-rtl`]: direction === 'rtl',
|
||||
[`${groupPrefixCls}-${shape}`]: shape,
|
||||
[`${groupPrefixCls}-${shape}-shadow`]: !trigger,
|
||||
});
|
||||
|
||||
const wrapperCls = classNames(hashId, `${groupPrefixCls}-wrap`);
|
||||
|
||||
const [open, setOpen] = useMergedState(false, { value: customOpen });
|
||||
|
||||
const floatButtonGroupRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const floatButtonRef = React.useRef<FloatButtonRef['nativeElement']>(null);
|
||||
|
||||
const hoverAction = React.useMemo<React.DOMAttributes<HTMLDivElement>>(() => {
|
||||
const hoverTypeAction = {
|
||||
onMouseEnter() {
|
||||
setOpen(true);
|
||||
onOpenChange?.(true);
|
||||
},
|
||||
onMouseLeave() {
|
||||
setOpen(false);
|
||||
onOpenChange?.(false);
|
||||
},
|
||||
};
|
||||
return trigger === 'hover' ? hoverTypeAction : {};
|
||||
}, [trigger]);
|
||||
|
||||
const handleOpenChange = () => {
|
||||
setOpen((prevState) => {
|
||||
onOpenChange?.(!prevState);
|
||||
return !prevState;
|
||||
});
|
||||
};
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (floatButtonGroupRef.current?.contains(e.target as Node)) {
|
||||
if (floatButtonRef.current?.contains(e.target as Node)) {
|
||||
handleOpenChange();
|
||||
}
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
onOpenChange?.(false);
|
||||
},
|
||||
[trigger],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (trigger === 'click') {
|
||||
document.addEventListener('click', onClick);
|
||||
return () => {
|
||||
document.removeEventListener('click', onClick);
|
||||
};
|
||||
}
|
||||
}, [trigger]);
|
||||
|
||||
// =================== Warning =====================
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const warning = devUseWarning('FloatButton.Group');
|
||||
|
||||
warning(
|
||||
!('open' in props) || !!trigger,
|
||||
'usage',
|
||||
'`open` need to be used together with `trigger`',
|
||||
);
|
||||
}
|
||||
|
||||
return wrapCSSVar(
|
||||
<FloatButtonGroupProvider value={shape}>
|
||||
<div ref={floatButtonGroupRef} className={groupCls} style={style} {...hoverAction}>
|
||||
{trigger && ['click', 'hover'].includes(trigger) ? (
|
||||
<>
|
||||
<CSSMotion visible={open} motionName={`${groupPrefixCls}-wrap`}>
|
||||
{({ className: motionClassName }) => (
|
||||
<div className={classNames(motionClassName, wrapperCls)}>{children}</div>
|
||||
)}
|
||||
</CSSMotion>
|
||||
<FloatButton
|
||||
ref={floatButtonRef}
|
||||
type={type}
|
||||
shape={shape}
|
||||
icon={open ? mergedCloseIcon : icon}
|
||||
description={description}
|
||||
aria-label={props['aria-label']}
|
||||
{...floatButtonProps}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
</FloatButtonGroupProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FloatButtonGroup);
|
46
packages/meta/src/float-button/PurePanel.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import BackTop from './BackTop';
|
||||
import FloatButton, { floatButtonPrefixCls } from './FloatButton';
|
||||
import FloatButtonGroup from './FloatButtonGroup';
|
||||
import type { FloatButtonGroupProps, FloatButtonProps } from './interface';
|
||||
|
||||
export interface PureFloatButtonProps extends Omit<FloatButtonProps, 'target'> {
|
||||
backTop?: boolean;
|
||||
}
|
||||
|
||||
export interface PurePanelProps
|
||||
extends PureFloatButtonProps,
|
||||
Omit<FloatButtonGroupProps, 'children'> {
|
||||
/** Convert to FloatGroup when configured */
|
||||
items?: PureFloatButtonProps[];
|
||||
}
|
||||
|
||||
const PureFloatButton: React.FC<PureFloatButtonProps> = ({ backTop, ...props }) =>
|
||||
backTop ? <BackTop {...props} visibilityHeight={0} /> : <FloatButton {...props} />;
|
||||
|
||||
/** @private Internal Component. Do not use in your production. */
|
||||
const PurePanel: React.FC<PurePanelProps> = ({ className, items, ...props }) => {
|
||||
const { prefixCls: customizePrefixCls } = props;
|
||||
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls);
|
||||
const pureCls = `${prefixCls}-pure`;
|
||||
|
||||
if (items) {
|
||||
return (
|
||||
<FloatButtonGroup className={classNames(className, pureCls)} {...props}>
|
||||
{items.map((item, index) => (
|
||||
<PureFloatButton key={index} {...item} />
|
||||
))}
|
||||
</FloatButtonGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return <PureFloatButton className={classNames(className, pureCls)} {...props} />;
|
||||
};
|
||||
|
||||
export default PurePanel;
|
9
packages/meta/src/float-button/context.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { FloatButtonShape } from './interface';
|
||||
|
||||
const FloatButtonGroupContext = React.createContext<FloatButtonShape | undefined>(undefined);
|
||||
|
||||
export const { Provider: FloatButtonGroupProvider } = FloatButtonGroupContext;
|
||||
|
||||
export default FloatButtonGroupContext;
|
7
packages/meta/src/float-button/demo/back-top.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
返回页面顶部的操作按钮。
|
||||
|
||||
## en-US
|
||||
|
||||
`BackTop` makes it easy to go back to the top of the page.
|
17
packages/meta/src/float-button/demo/back-top.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<div style={{ height: '300vh', padding: 10 }}>
|
||||
<div>Scroll to bottom</div>
|
||||
<div>Scroll to bottom</div>
|
||||
<div>Scroll to bottom</div>
|
||||
<div>Scroll to bottom</div>
|
||||
<div>Scroll to bottom</div>
|
||||
<div>Scroll to bottom</div>
|
||||
<div>Scroll to bottom</div>
|
||||
<FloatButton.BackTop />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/badge-debug.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
调试使用。
|
||||
|
||||
## en-US
|
||||
|
||||
debug use.
|
24
packages/meta/src/float-button/demo/badge-debug.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ConfigProvider, FloatButton, Slider } from 'antd';
|
||||
import type { ConfigProviderProps, GetProp } from 'antd';
|
||||
|
||||
type AliasToken = GetProp<ConfigProviderProps, 'theme'>['token'];
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [radius, setRadius] = useState<number>(0);
|
||||
|
||||
const token: Partial<AliasToken> = {
|
||||
borderRadius: radius,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Slider min={0} max={20} style={{ margin: 16 }} onChange={setRadius} />
|
||||
<ConfigProvider theme={{ token }}>
|
||||
<FloatButton shape="square" badge={{ dot: true }} />
|
||||
</ConfigProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/badge.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
右上角附带圆形徽标数字的悬浮按钮。
|
||||
|
||||
## en-US
|
||||
|
||||
FloatButton with Badge.
|
24
packages/meta/src/float-button/demo/badge.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<FloatButton shape="circle" badge={{ dot: true }} style={{ right: 24 + 70 + 70 }} />
|
||||
<FloatButton.Group shape="circle" style={{ right: 24 + 70 }}>
|
||||
<FloatButton
|
||||
href="https://ant.design/index-cn"
|
||||
tooltip={<div>custom badge color</div>}
|
||||
badge={{ count: 5, color: 'blue' }}
|
||||
/>
|
||||
<FloatButton badge={{ count: 5 }} />
|
||||
</FloatButton.Group>
|
||||
<FloatButton.Group shape="circle">
|
||||
<FloatButton badge={{ count: 12 }} icon={<QuestionCircleOutlined />} />
|
||||
<FloatButton badge={{ count: 123, overflowCount: 999 }} />
|
||||
<FloatButton.BackTop visibilityHeight={0} />
|
||||
</FloatButton.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/basic.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
最简单的用法。
|
||||
|
||||
## en-US
|
||||
|
||||
The most basic usage.
|
6
packages/meta/src/float-button/demo/basic.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => <FloatButton onClick={() => console.log('onClick')} />;
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/controlled.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
通过 `open` 设置组件为受控模式,需要配合 trigger 一起使用。
|
||||
|
||||
## en-US
|
||||
|
||||
Set the component to controlled mode through `open`, which need to be used together with trigger.
|
28
packages/meta/src/float-button/demo/controlled.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CommentOutlined, CustomerServiceOutlined } from '@ant-design/icons';
|
||||
import { FloatButton, Switch } from 'antd';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const onChange = (checked: boolean) => {
|
||||
setOpen(checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FloatButton.Group
|
||||
open={open}
|
||||
trigger="click"
|
||||
style={{ right: 24 }}
|
||||
icon={<CustomerServiceOutlined />}
|
||||
>
|
||||
<FloatButton />
|
||||
<FloatButton icon={<CommentOutlined />} />
|
||||
</FloatButton.Group>
|
||||
<Switch onChange={onChange} checked={open} style={{ margin: 16 }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
11
packages/meta/src/float-button/demo/description.md
Normal file
@ -0,0 +1,11 @@
|
||||
## zh-CN
|
||||
|
||||
可以通过 `description` 设置文字内容。
|
||||
|
||||
> 仅当 `shape` 属性为 `square` 时支持。由于空间较小,推荐使用比较精简的双数文字。
|
||||
|
||||
## en-US
|
||||
|
||||
Setting `description` prop to show FloatButton with description.
|
||||
|
||||
> supported only when `shape` is `square`. Due to narrow space for text, short sentence is recommended.
|
23
packages/meta/src/float-button/demo/description.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { FileTextOutlined } from '@ant-design/icons';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<FloatButton
|
||||
icon={<FileTextOutlined />}
|
||||
description="HELP INFO"
|
||||
shape="square"
|
||||
style={{ right: 24 }}
|
||||
/>
|
||||
<FloatButton description="HELP INFO" shape="square" style={{ right: 94 }} />
|
||||
<FloatButton
|
||||
icon={<FileTextOutlined />}
|
||||
description="HELP"
|
||||
shape="square"
|
||||
style={{ right: 164 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/group-menu.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
设置 `trigger` 属性即可开启菜单模式。提供 `hover` 和 `click` 两种触发方式。
|
||||
|
||||
## en-US
|
||||
|
||||
Open menu mode with `trigger`, which could be `hover` or `click`.
|
28
packages/meta/src/float-button/demo/group-menu.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { CommentOutlined, CustomerServiceOutlined } from '@ant-design/icons';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<FloatButton.Group
|
||||
trigger="click"
|
||||
type="primary"
|
||||
style={{ right: 24 }}
|
||||
icon={<CustomerServiceOutlined />}
|
||||
>
|
||||
<FloatButton />
|
||||
<FloatButton icon={<CommentOutlined />} />
|
||||
</FloatButton.Group>
|
||||
<FloatButton.Group
|
||||
trigger="hover"
|
||||
type="primary"
|
||||
style={{ right: 94 }}
|
||||
icon={<CustomerServiceOutlined />}
|
||||
>
|
||||
<FloatButton />
|
||||
<FloatButton icon={<CommentOutlined />} />
|
||||
</FloatButton.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/group.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
按钮组合使用时,推荐使用 `<FloatButton.Group />`,并通过设置 `shape` 属性改变悬浮按钮组的形状。悬浮按钮组的 `shape` 会覆盖内部 FloatButton 的 `shape` 属性。
|
||||
|
||||
## en-US
|
||||
|
||||
When multiple buttons are used together, `<FloatButton.Group />` is recommended. By setting `shape` of FloatButton.Group, you can change the shape of group. `shape` of FloatButton.Group will override `shape` of FloatButton inside.
|
21
packages/meta/src/float-button/demo/group.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<FloatButton.Group shape="circle" style={{ right: 24 }}>
|
||||
<FloatButton icon={<QuestionCircleOutlined />} />
|
||||
<FloatButton />
|
||||
<FloatButton.BackTop visibilityHeight={0} />
|
||||
</FloatButton.Group>
|
||||
<FloatButton.Group shape="square" style={{ right: 94 }}>
|
||||
<FloatButton icon={<QuestionCircleOutlined />} />
|
||||
<FloatButton />
|
||||
<FloatButton icon={<SyncOutlined />} />
|
||||
<FloatButton.BackTop visibilityHeight={0} />
|
||||
</FloatButton.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/render-panel.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
调试用组件,请勿直接使用。
|
||||
|
||||
## en-US
|
||||
|
||||
Debug usage. Do not use in your production.
|
39
packages/meta/src/float-button/demo/render-panel.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { CustomerServiceOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
/** Test usage. Do not use in your production. */
|
||||
const { _InternalPanelDoNotUseOrYouWillBeFired: InternalFloatButton } = FloatButton;
|
||||
|
||||
const App: React.FC = () => (
|
||||
<div style={{ display: 'flex', columnGap: 16, alignItems: 'center' }}>
|
||||
<InternalFloatButton backTop />
|
||||
<InternalFloatButton icon={<CustomerServiceOutlined />} />
|
||||
<InternalFloatButton
|
||||
icon={<QuestionCircleOutlined />}
|
||||
description="HELP"
|
||||
shape="square"
|
||||
type="primary"
|
||||
/>
|
||||
<InternalFloatButton
|
||||
shape="square"
|
||||
items={[
|
||||
{ icon: <QuestionCircleOutlined /> },
|
||||
{ icon: <CustomerServiceOutlined /> },
|
||||
{ icon: <SyncOutlined /> },
|
||||
]}
|
||||
/>
|
||||
<InternalFloatButton
|
||||
open
|
||||
icon={<CustomerServiceOutlined />}
|
||||
trigger="click"
|
||||
items={[
|
||||
{ icon: <QuestionCircleOutlined /> },
|
||||
{ icon: <CustomerServiceOutlined /> },
|
||||
{ icon: <SyncOutlined /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/shape.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
通过 `shape` 设置不同的形状。
|
||||
|
||||
## en-US
|
||||
|
||||
Change the shape of the FloatButton with `shape`.
|