From 2e753a7259d83c92195535f5ab8b927781ec674b Mon Sep 17 00:00:00 2001 From: jiangzhixiong <710328466@qq.com> Date: Tue, 21 May 2024 19:07:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(map):=20=E5=B7=A5=E5=85=B7=E7=AE=B1?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 99 +++-- packages/map/package.json | 18 +- packages/map/src/MapBox.tsx | 121 ++++- .../customOverlay/CustomOverlay.tsx | 59 +++ .../src/components/customOverlay/index.tsx | 6 + .../drawControl}/Draw/constants.js | 0 .../drawControl}/Draw/doubleClickZoom.js | 0 .../drawControl}/Draw/drawCircleMode.draw.js | 0 .../drawControl}/Draw/drawDirectMode.draw.js | 0 .../Draw/drawLineSelectMode.draw.js | 0 .../drawControl}/Draw/drawRectMode.draw.js | 0 .../Draw/drawSimpleSelectMode.draw.js | 0 .../drawControl}/Draw/drawStaticMode.draw.js | 0 .../drawControl}/Draw/index.ts | 0 .../components/drawControl/DrawControl.tsx | 89 ++++ .../map/src/components/drawControl/index.tsx | 8 + packages/map/src/components/tools/Tools.tsx | 69 +++ packages/map/src/components/tools/index.less | 64 +++ packages/map/src/components/tools/index.tsx | 8 + packages/map/src/hooks/useDraw.ts | 0 packages/map/src/index.less | 10 +- packages/map/src/index.md | 2 + packages/map/src/utils.ts | 35 -- packages/map/src/{ => utils}/constants.ts | 0 .../map/src/utils/{Draw => }/drawStyle.ts | 0 .../map/src/utils/{Draw/utils.js => index.ts} | 95 +++- packages/meta/src/float-button/BackTop.tsx | 107 +++++ .../meta/src/float-button/FloatButton.tsx | 126 ++++++ .../src/float-button/FloatButtonContent.tsx | 34 ++ .../src/float-button/FloatButtonGroup.tsx | 141 ++++++ packages/meta/src/float-button/PurePanel.tsx | 46 ++ packages/meta/src/float-button/context.ts | 9 + .../meta/src/float-button/demo/back-top.md | 7 + .../meta/src/float-button/demo/back-top.tsx | 17 + .../meta/src/float-button/demo/badge-debug.md | 7 + .../src/float-button/demo/badge-debug.tsx | 24 + packages/meta/src/float-button/demo/badge.md | 7 + packages/meta/src/float-button/demo/badge.tsx | 24 + packages/meta/src/float-button/demo/basic.md | 7 + packages/meta/src/float-button/demo/basic.tsx | 6 + .../meta/src/float-button/demo/controlled.md | 7 + .../meta/src/float-button/demo/controlled.tsx | 28 ++ .../meta/src/float-button/demo/description.md | 11 + .../src/float-button/demo/description.tsx | 23 + .../meta/src/float-button/demo/group-menu.md | 7 + .../meta/src/float-button/demo/group-menu.tsx | 28 ++ packages/meta/src/float-button/demo/group.md | 7 + packages/meta/src/float-button/demo/group.tsx | 21 + .../src/float-button/demo/render-panel.md | 7 + .../src/float-button/demo/render-panel.tsx | 39 ++ packages/meta/src/float-button/demo/shape.md | 7 + packages/meta/src/float-button/demo/shape.tsx | 22 + .../meta/src/float-button/demo/tooltip.md | 7 + .../meta/src/float-button/demo/tooltip.tsx | 6 + packages/meta/src/float-button/demo/type.md | 7 + packages/meta/src/float-button/demo/type.tsx | 12 + packages/meta/src/float-button/index.ts | 10 + packages/meta/src/float-button/index.zh-CN.md | 71 +++ packages/meta/src/float-button/interface.ts | 66 +++ packages/meta/src/float-button/style/index.ts | 413 ++++++++++++++++++ packages/meta/src/float-button/util.ts | 10 + packages/meta/src/index.tsx | 8 +- 62 files changed, 1968 insertions(+), 94 deletions(-) create mode 100644 packages/map/src/components/customOverlay/CustomOverlay.tsx create mode 100644 packages/map/src/components/customOverlay/index.tsx rename packages/map/src/{utils => components/drawControl}/Draw/constants.js (100%) rename packages/map/src/{utils => components/drawControl}/Draw/doubleClickZoom.js (100%) rename packages/map/src/{utils => components/drawControl}/Draw/drawCircleMode.draw.js (100%) rename packages/map/src/{utils => components/drawControl}/Draw/drawDirectMode.draw.js (100%) rename packages/map/src/{utils => components/drawControl}/Draw/drawLineSelectMode.draw.js (100%) rename packages/map/src/{utils => components/drawControl}/Draw/drawRectMode.draw.js (100%) rename packages/map/src/{utils => components/drawControl}/Draw/drawSimpleSelectMode.draw.js (100%) rename packages/map/src/{utils => components/drawControl}/Draw/drawStaticMode.draw.js (100%) rename packages/map/src/{utils => components/drawControl}/Draw/index.ts (100%) create mode 100644 packages/map/src/components/drawControl/DrawControl.tsx create mode 100644 packages/map/src/components/drawControl/index.tsx create mode 100644 packages/map/src/components/tools/Tools.tsx create mode 100644 packages/map/src/components/tools/index.less create mode 100644 packages/map/src/components/tools/index.tsx create mode 100644 packages/map/src/hooks/useDraw.ts delete mode 100644 packages/map/src/utils.ts rename packages/map/src/{ => utils}/constants.ts (100%) rename packages/map/src/utils/{Draw => }/drawStyle.ts (100%) rename packages/map/src/utils/{Draw/utils.js => index.ts} (65%) create mode 100644 packages/meta/src/float-button/BackTop.tsx create mode 100644 packages/meta/src/float-button/FloatButton.tsx create mode 100644 packages/meta/src/float-button/FloatButtonContent.tsx create mode 100644 packages/meta/src/float-button/FloatButtonGroup.tsx create mode 100644 packages/meta/src/float-button/PurePanel.tsx create mode 100644 packages/meta/src/float-button/context.ts create mode 100644 packages/meta/src/float-button/demo/back-top.md create mode 100644 packages/meta/src/float-button/demo/back-top.tsx create mode 100644 packages/meta/src/float-button/demo/badge-debug.md create mode 100644 packages/meta/src/float-button/demo/badge-debug.tsx create mode 100644 packages/meta/src/float-button/demo/badge.md create mode 100644 packages/meta/src/float-button/demo/badge.tsx create mode 100644 packages/meta/src/float-button/demo/basic.md create mode 100644 packages/meta/src/float-button/demo/basic.tsx create mode 100644 packages/meta/src/float-button/demo/controlled.md create mode 100644 packages/meta/src/float-button/demo/controlled.tsx create mode 100644 packages/meta/src/float-button/demo/description.md create mode 100644 packages/meta/src/float-button/demo/description.tsx create mode 100644 packages/meta/src/float-button/demo/group-menu.md create mode 100644 packages/meta/src/float-button/demo/group-menu.tsx create mode 100644 packages/meta/src/float-button/demo/group.md create mode 100644 packages/meta/src/float-button/demo/group.tsx create mode 100644 packages/meta/src/float-button/demo/render-panel.md create mode 100644 packages/meta/src/float-button/demo/render-panel.tsx create mode 100644 packages/meta/src/float-button/demo/shape.md create mode 100644 packages/meta/src/float-button/demo/shape.tsx create mode 100644 packages/meta/src/float-button/demo/tooltip.md create mode 100644 packages/meta/src/float-button/demo/tooltip.tsx create mode 100644 packages/meta/src/float-button/demo/type.md create mode 100644 packages/meta/src/float-button/demo/type.tsx create mode 100644 packages/meta/src/float-button/index.ts create mode 100644 packages/meta/src/float-button/index.zh-CN.md create mode 100644 packages/meta/src/float-button/interface.ts create mode 100644 packages/meta/src/float-button/style/index.ts create mode 100644 packages/meta/src/float-button/util.ts diff --git a/docs/index.md b/docs/index.md index 3255e9f..aa15139 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 包整个开发链路。 + ## 目录结构 diff --git a/packages/map/package.json b/packages/map/package.json index 907a3cf..aa0f776 100644 --- a/packages/map/package.json +++ b/packages/map/package.json @@ -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", + "@turf/union": "^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" } } diff --git a/packages/map/src/MapBox.tsx b/packages/map/src/MapBox.tsx index bd8ab17..1e02e7d 100644 --- a/packages/map/src/MapBox.tsx +++ b/packages/map/src/MapBox.tsx @@ -1,10 +1,22 @@ import 'mapbox-gl/dist/mapbox-gl.css'; +import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; +import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import Map from 'react-map-gl'; -import './index.less'; -import React, { forwardRef, useImperativeHandle, useRef } from 'react'; +import { + // MapboxMap, + MapRef, + // MapStyle +} from "react-map-gl"; +import classnames from 'classnames' +import Tools from './components/tools' +import DrawControl, { DrawControlProps, DrawControlRefProps } from './components/drawControl'; import { MapProps } from './interface'; import { merge } from './utils'; -import { MAP_CENTER, defaultMapConfig } from './constants'; +import { MAP_CENTER, defaultMapConfig } from './utils/constants'; +import './index.less'; +import mapboxDrawStyle from './utils/drawStyle'; + +const componentName = 'zhst-map' export interface MapRefProps { @@ -20,23 +32,102 @@ const MapBox = forwardRef((props, ref) => { width = '100%', ...others } = props || {}; - const mapRef = useRef(null) + const mapRef = useRef(null) + const drawControlRef = useRef(null) + + // 默认绘制配置 + const [drawConfig, setConfig] = useState({ + displayControlsDefault: false, + position: 'top-left', + styles: mapboxDrawStyle, + // Select which mapbox-gl-draw control buttons to add to the map. + controls: { + polygon: true, + trash: true + }, + // The user does not have to click the polygon control button first. + defaultMode: 'draw_polygon', + }) + + const handleDrawCreate = e => { + console.log('handleDrawCreate', e) + } + + const handleDrawUpdate = e => { + console.log('handleDrawUpdate', e) + } + + const handleDrawDelete = e => { + console.log('handleDrawDelete', e) + } + + useEffect(() => { + console.log('drawControlRef', drawControlRef.current?.drawer?.deleteAll()) + }, []) useImperativeHandle(ref, () => ({})) return ( //@ts-ignore - { - onLoad && onLoad(e); - }} - style={{ width: width, height: height, ...style }} - {...merge(defaultMapConfig, others)} - > - {children} - +
+ drawControlRef.current?.drawer?.changeMode?.('draw_circle') + }, + { + label: '矩形框选', + key: 'rect', + icon: 'icon-fang', + onClick: () => drawControlRef.current?.drawer?.changeMode?.('draw_rect') + }, + { + 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: 'clear', + icon: 'icon-cuo', + onClick: () => drawControlRef.current?.drawer?.deleteAll() + } + ]} + /> + { + onLoad && onLoad(e); + }} + style={{ width: width, height: height, ...style }} + {...merge(defaultMapConfig, others)} + > + {/* + + */} + + {children} + +
); }); diff --git a/packages/map/src/components/customOverlay/CustomOverlay.tsx b/packages/map/src/components/customOverlay/CustomOverlay.tsx new file mode 100644 index 0000000..6b86c5f --- /dev/null +++ b/packages/map/src/components/customOverlay/CustomOverlay.tsx @@ -0,0 +1,59 @@ +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: HTMLDivElement | undefined; + _redraw: () => void; + + constructor(redraw: () => void) { + this._redraw = redraw; + } + + onAdd(map: MapboxMap) { + 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; + } +} + +/** + * 自定义图层 + */ +const CustomOverlay = (props: {children: React.ReactElement}) => { + const [, setVersion] = useState(0); + + const customControl = useControl(() => { + const forceUpdate = () => setVersion(v => v + 1); + return new OverlayControl(forceUpdate); + }); + + const map = customControl.getMap(); + + // @ts-ignore + return map && createPortal(cloneElement(props.children, { map }), customControl.getElement()); +} + +export default React.memo(CustomOverlay); diff --git a/packages/map/src/components/customOverlay/index.tsx b/packages/map/src/components/customOverlay/index.tsx new file mode 100644 index 0000000..774efb3 --- /dev/null +++ b/packages/map/src/components/customOverlay/index.tsx @@ -0,0 +1,6 @@ +/** + * Created by jiangzhixiong on 2024/05/21 + */ +import CustomOverlay from './CustomOverlay' + +export default CustomOverlay diff --git a/packages/map/src/utils/Draw/constants.js b/packages/map/src/components/drawControl/Draw/constants.js similarity index 100% rename from packages/map/src/utils/Draw/constants.js rename to packages/map/src/components/drawControl/Draw/constants.js diff --git a/packages/map/src/utils/Draw/doubleClickZoom.js b/packages/map/src/components/drawControl/Draw/doubleClickZoom.js similarity index 100% rename from packages/map/src/utils/Draw/doubleClickZoom.js rename to packages/map/src/components/drawControl/Draw/doubleClickZoom.js diff --git a/packages/map/src/utils/Draw/drawCircleMode.draw.js b/packages/map/src/components/drawControl/Draw/drawCircleMode.draw.js similarity index 100% rename from packages/map/src/utils/Draw/drawCircleMode.draw.js rename to packages/map/src/components/drawControl/Draw/drawCircleMode.draw.js diff --git a/packages/map/src/utils/Draw/drawDirectMode.draw.js b/packages/map/src/components/drawControl/Draw/drawDirectMode.draw.js similarity index 100% rename from packages/map/src/utils/Draw/drawDirectMode.draw.js rename to packages/map/src/components/drawControl/Draw/drawDirectMode.draw.js diff --git a/packages/map/src/utils/Draw/drawLineSelectMode.draw.js b/packages/map/src/components/drawControl/Draw/drawLineSelectMode.draw.js similarity index 100% rename from packages/map/src/utils/Draw/drawLineSelectMode.draw.js rename to packages/map/src/components/drawControl/Draw/drawLineSelectMode.draw.js diff --git a/packages/map/src/utils/Draw/drawRectMode.draw.js b/packages/map/src/components/drawControl/Draw/drawRectMode.draw.js similarity index 100% rename from packages/map/src/utils/Draw/drawRectMode.draw.js rename to packages/map/src/components/drawControl/Draw/drawRectMode.draw.js diff --git a/packages/map/src/utils/Draw/drawSimpleSelectMode.draw.js b/packages/map/src/components/drawControl/Draw/drawSimpleSelectMode.draw.js similarity index 100% rename from packages/map/src/utils/Draw/drawSimpleSelectMode.draw.js rename to packages/map/src/components/drawControl/Draw/drawSimpleSelectMode.draw.js diff --git a/packages/map/src/utils/Draw/drawStaticMode.draw.js b/packages/map/src/components/drawControl/Draw/drawStaticMode.draw.js similarity index 100% rename from packages/map/src/utils/Draw/drawStaticMode.draw.js rename to packages/map/src/components/drawControl/Draw/drawStaticMode.draw.js diff --git a/packages/map/src/utils/Draw/index.ts b/packages/map/src/components/drawControl/Draw/index.ts similarity index 100% rename from packages/map/src/utils/Draw/index.ts rename to packages/map/src/components/drawControl/Draw/index.ts diff --git a/packages/map/src/components/drawControl/DrawControl.tsx b/packages/map/src/components/drawControl/DrawControl.tsx new file mode 100644 index 0000000..62788a0 --- /dev/null +++ b/packages/map/src/components/drawControl/DrawControl.tsx @@ -0,0 +1,89 @@ +/** + * Created by jiangzhixiong on 2024/05/21 + */ + +import { forwardRef, useImperativeHandle, useRef, } from 'react' +import MapboxDraw, { MapboxDrawOptions } from '@mapbox/mapbox-gl-draw'; +import { + CircleMode, + DragCircleMode, + DirectMode, + SimpleSelectMode +} from 'mapbox-gl-draw-circle' +import drawRectMode from 'mapbox-gl-draw-rectangle-mode' +import drawStaticMode from '@mapbox/mapbox-gl-draw-static-mode' +// import * as MapboxDrawGeodesic from 'mapbox-gl-draw-geodesic'; +import { useControl } from 'react-map-gl'; +import type { ControlPosition } from 'react-map-gl'; +import { MapContextValue } from 'react-map-gl/dist/esm/components/map'; + +type DrawControlProps = ConstructorParameters[0] & { + position?: ControlPosition; + + onCreate?: (evt: {features: object[]}) => void; + onUpdate?: (evt: {features: object[]; action: string}) => void; + onDelete?: (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((props, ref) => { + const drawRef = useRef(null) + + useControl( + () => { + 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 + } + ) + // 修改模式 + // drawIns?.changeMode('simple_select'); + drawRef.current = draw + return draw + }, + (context: MapContextValue) => { + const { map } = context + map.on('draw.create', props.onCreate); + map.on('draw.update', props.onUpdate); + map.on('draw.delete', props.onDelete); + }, + (context: MapContextValue) => { + const { map } = context + map.off('draw.create', props.onCreate); + map.off('draw.update', props.onUpdate); + map.off('draw.delete', props.onDelete); + }, + { + position: props.position + } + ); + + useImperativeHandle(ref, () => ({ + drawer: drawRef.current + })) + + return null; +}) + +DrawControl.defaultProps = { + onCreate: () => {}, + onUpdate: () => {}, + onDelete: () => {} +}; + +export default DrawControl diff --git a/packages/map/src/components/drawControl/index.tsx b/packages/map/src/components/drawControl/index.tsx new file mode 100644 index 0000000..5a96584 --- /dev/null +++ b/packages/map/src/components/drawControl/index.tsx @@ -0,0 +1,8 @@ +/** + * Created by jiangzhixiong on 2024/05/21 + */ +import DrawControl from './DrawControl' + +export type { DrawControlProps, DrawControlRefProps } from './DrawControl' + +export default DrawControl diff --git a/packages/map/src/components/tools/Tools.tsx b/packages/map/src/components/tools/Tools.tsx new file mode 100644 index 0000000..299a7c5 --- /dev/null +++ b/packages/map/src/components/tools/Tools.tsx @@ -0,0 +1,69 @@ +/** + * Created by jiangzhixiong on 2024/05/21 + */ + +import React, { forwardRef, ReactNode, useContext, useImperativeHandle } from 'react' +import { Button, ConfigProvider } from '@zhst/meta' +import { useToggle } from '@zhst/hooks' +import classNames from 'classnames' +import { IconFont } from '@zhst/icon' +import './index.less' + +const { ConfigContext } = ConfigProvider + +export interface ToolsProps { + prefixCls?: string + defaultValue?: boolean; + buttonList?: { + label: string + key: string + icon?: ReactNode | string + onClick?: () => void + }[] +} + +export interface ToolsRefProps { +} + +const Tools = forwardRef((props, ref) => { + const { + prefixCls: customizePrefixCls, + buttonList = [], + defaultValue + } = props + const { getPrefixCls } = useContext(ConfigContext) + const componentName = getPrefixCls('map-tools', customizePrefixCls) + const [state, { toggle }] = useToggle(defaultValue) + + useImperativeHandle(ref, () => ({ + + })) + + return ( +
+
    + {buttonList.map((item) => ( + <> +
  • + {typeof item.icon === 'string' ? ( + + ) : ( + item.icon + )} + {item.label} +
  • + + ))} +
+
+ ) +}) + +export default Tools diff --git a/packages/map/src/components/tools/index.less b/packages/map/src/components/tools/index.less new file mode 100644 index 0000000..269732b --- /dev/null +++ b/packages/map/src/components/tools/index.less @@ -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; + } + } + } +} diff --git a/packages/map/src/components/tools/index.tsx b/packages/map/src/components/tools/index.tsx new file mode 100644 index 0000000..a01fbac --- /dev/null +++ b/packages/map/src/components/tools/index.tsx @@ -0,0 +1,8 @@ +/** + * Created by jiangzhixiong on 2024/05/21 + */ +import Tools from './Tools' + +export type { ToolsProps, ToolsRefProps } from './Tools' + +export default Tools diff --git a/packages/map/src/hooks/useDraw.ts b/packages/map/src/hooks/useDraw.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/map/src/index.less b/packages/map/src/index.less index ac327e9..3369b02 100644 --- a/packages/map/src/index.less +++ b/packages/map/src/index.less @@ -1,3 +1,9 @@ -.mapboxgl-ctrl-attrib-button { - display: none; +.zhst-map { + position: relative; + width: auto; + height: auto; + + .mapboxgl-ctrl-logo { + display: none; + } } diff --git a/packages/map/src/index.md b/packages/map/src/index.md index f637030..9240a6b 100644 --- a/packages/map/src/index.md +++ b/packages/map/src/index.md @@ -25,3 +25,5 @@ title: 快速上手 [官方文档](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) diff --git a/packages/map/src/utils.ts b/packages/map/src/utils.ts deleted file mode 100644 index f095cdc..0000000 --- a/packages/map/src/utils.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/packages/map/src/constants.ts b/packages/map/src/utils/constants.ts similarity index 100% rename from packages/map/src/constants.ts rename to packages/map/src/utils/constants.ts diff --git a/packages/map/src/utils/Draw/drawStyle.ts b/packages/map/src/utils/drawStyle.ts similarity index 100% rename from packages/map/src/utils/Draw/drawStyle.ts rename to packages/map/src/utils/drawStyle.ts diff --git a/packages/map/src/utils/Draw/utils.js b/packages/map/src/utils/index.ts similarity index 65% rename from packages/map/src/utils/Draw/utils.js rename to packages/map/src/utils/index.ts index fabe2a8..c91ffad 100644 --- a/packages/map/src/utils/Draw/utils.js +++ b/packages/map/src/utils/index.ts @@ -1,15 +1,58 @@ import * as turf from '@turf/turf'; import union from '@turf/union'; -// 获取2个点的距离 -export const getDistance = (point1, point2) => { +// 判断参数类型 +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, pointA, pointB) { +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两点的像素坐标距离 @@ -17,19 +60,19 @@ export function getPixelDistance(map, pointA, pointB) { } //判断是否是0值 给一个偏移量 -export const isZero = (floatValue) => { +export const isZero = (floatValue: number) => { return floatValue > -0.00001 && floatValue < 0.00001; }; //判断2个向量是否平行 -export const isParallel = (vectorA, vectorB) => { +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, path) => { +export const isPointOnLine = (vectorA: { x: number; y: number }, path: any[]) => { const [vectorB, vectorC] = path; //与端点重合 @@ -84,8 +127,8 @@ export const isPointOnLine = (vectorA, path) => { //获取线段重合部分,如果重合返回合并后的线段 如果不重合 返回传入的线段 //see https://blog.csdn.net/qq_39108767/article/details/81673921 //see https://www.cnblogs.com/tuyang1129/p/9390376.html -export const getLineCoincide = (path1, path2) => { - let paths = []; +export const getLineCoincide = (path1: [any, any], path2: [any, any]) => { + let paths: string | any[] = []; const [vectorA, vectorB] = path1; const [vectorC, vectorD] = path2; @@ -125,10 +168,10 @@ export const getLineCoincide = (path1, path2) => { }; // 计算与纬线的角度,正方向向上 -const calcAng = (point, p) => (Math.atan2(point[1] - p[1], point[0] - p[0]) * 180) / Math.PI + 180; +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, r = 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++) { @@ -160,3 +203,35 @@ export const lineToPoly = (geojson, r = 50) => { } return _union; }; + +/** + * @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) +} diff --git a/packages/meta/src/float-button/BackTop.tsx b/packages/meta/src/float-button/BackTop.tsx new file mode 100644 index 0000000..14cdba4 --- /dev/null +++ b/packages/meta/src/float-button/BackTop.tsx @@ -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((props, ref) => { + const { + prefixCls: customizePrefixCls, + className, + type = 'default', + shape = 'circle', + visibilityHeight = 400, + icon = , + target, + onClick, + duration = 450, + ...restProps + } = props; + + const [visible, setVisible] = useState(visibilityHeight === 0); + + const internalRef = React.useRef(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 | { 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 = (e) => { + scrollTo(0, { getContainer: target || getDefaultTarget, duration }); + onClick?.(e); + }; + + const { getPrefixCls } = useContext(ConfigContext); + + const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls); + const rootPrefixCls = getPrefixCls(); + + const groupShape = useContext(FloatButtonGroupContext); + + const mergedShape = groupShape || shape; + + const contentProps: FloatButtonProps = { + prefixCls, + icon, + type, + shape: mergedShape, + ...restProps, + }; + + return ( + + {({ className: motionClassName }, setRef) => ( + + )} + + ); +}); + +if (process.env.NODE_ENV !== 'production') { + BackTop.displayName = 'BackTop'; +} + +export default BackTop; diff --git a/packages/meta/src/float-button/FloatButton.tsx b/packages/meta/src/float-button/FloatButton.tsx new file mode 100644 index 0000000..d7fd3a7 --- /dev/null +++ b/packages/meta/src/float-button/FloatButton.tsx @@ -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((props, ref) => { + const { + prefixCls: customizePrefixCls, + className, + rootClassName, + type = 'default', + shape = 'circle', + icon, + description, + tooltip, + badge = {}, + ...restProps + } = props; + const { getPrefixCls, direction } = useContext(ConfigContext); + const groupShape = useContext(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( + () => omit(badge, ['title', 'children', 'status', 'text'] as any[]), + [badge], + ); + + const contentProps = useMemo( + () => ({ prefixCls, description, icon, type }), + [prefixCls, description, icon, type], + ); + + let buttonNode = ( +
+ +
+ ); + + if ('badge' in props) { + buttonNode = {buttonNode}; + } + + if ('tooltip' in props) { + buttonNode = ( + + {buttonNode} + + ); + } + + 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 ? ( + + {buttonNode} + + ) : ( + + ), + ); +}); + +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; diff --git a/packages/meta/src/float-button/FloatButtonContent.tsx b/packages/meta/src/float-button/FloatButtonContent.tsx new file mode 100644 index 0000000..3e388c8 --- /dev/null +++ b/packages/meta/src/float-button/FloatButtonContent.tsx @@ -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 = (props) => { + const { icon, description, prefixCls, className } = props; + const defaultElement = ( +
+ +
+ ); + return ( +
+ {icon || description ? ( + <> + {icon &&
{icon}
} + {description &&
{description}
} + + ) : ( + defaultElement + )} +
+ ); +}; + +export default memo(FloatButtonContent); diff --git a/packages/meta/src/float-button/FloatButtonGroup.tsx b/packages/meta/src/float-button/FloatButtonGroup.tsx new file mode 100644 index 0000000..6abfe4d --- /dev/null +++ b/packages/meta/src/float-button/FloatButtonGroup.tsx @@ -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 = (props) => { + const { + prefixCls: customizePrefixCls, + className, + style, + shape = 'circle', + type = 'default', + icon = , + closeIcon, + description, + trigger, + children, + onOpenChange, + open: customOpen, + ...floatButtonProps + } = props; + + const { direction, getPrefixCls, floatButtonGroup } = + useContext(ConfigContext); + + const mergedCloseIcon = closeIcon ?? floatButtonGroup?.closeIcon ?? ; + + 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(null); + + const floatButtonRef = React.useRef(null); + + const hoverAction = React.useMemo>(() => { + 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( + +
+ {trigger && ['click', 'hover'].includes(trigger) ? ( + <> + + {({ className: motionClassName }) => ( +
{children}
+ )} +
+ + + ) : ( + children + )} +
+
, + ); +}; + +export default memo(FloatButtonGroup); diff --git a/packages/meta/src/float-button/PurePanel.tsx b/packages/meta/src/float-button/PurePanel.tsx new file mode 100644 index 0000000..f7780be --- /dev/null +++ b/packages/meta/src/float-button/PurePanel.tsx @@ -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 { + backTop?: boolean; +} + +export interface PurePanelProps + extends PureFloatButtonProps, + Omit { + /** Convert to FloatGroup when configured */ + items?: PureFloatButtonProps[]; +} + +const PureFloatButton: React.FC = ({ backTop, ...props }) => + backTop ? : ; + +/** @private Internal Component. Do not use in your production. */ +const PurePanel: React.FC = ({ className, items, ...props }) => { + const { prefixCls: customizePrefixCls } = props; + + const { getPrefixCls } = React.useContext(ConfigContext); + const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls); + const pureCls = `${prefixCls}-pure`; + + if (items) { + return ( + + {items.map((item, index) => ( + + ))} + + ); + } + + return ; +}; + +export default PurePanel; diff --git a/packages/meta/src/float-button/context.ts b/packages/meta/src/float-button/context.ts new file mode 100644 index 0000000..798cf9f --- /dev/null +++ b/packages/meta/src/float-button/context.ts @@ -0,0 +1,9 @@ +import React from 'react'; + +import type { FloatButtonShape } from './interface'; + +const FloatButtonGroupContext = React.createContext(undefined); + +export const { Provider: FloatButtonGroupProvider } = FloatButtonGroupContext; + +export default FloatButtonGroupContext; diff --git a/packages/meta/src/float-button/demo/back-top.md b/packages/meta/src/float-button/demo/back-top.md new file mode 100644 index 0000000..15eb759 --- /dev/null +++ b/packages/meta/src/float-button/demo/back-top.md @@ -0,0 +1,7 @@ +## zh-CN + +返回页面顶部的操作按钮。 + +## en-US + +`BackTop` makes it easy to go back to the top of the page. diff --git a/packages/meta/src/float-button/demo/back-top.tsx b/packages/meta/src/float-button/demo/back-top.tsx new file mode 100644 index 0000000..29b5958 --- /dev/null +++ b/packages/meta/src/float-button/demo/back-top.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => ( +
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+
Scroll to bottom
+ +
+); + +export default App; diff --git a/packages/meta/src/float-button/demo/badge-debug.md b/packages/meta/src/float-button/demo/badge-debug.md new file mode 100644 index 0000000..19b791b --- /dev/null +++ b/packages/meta/src/float-button/demo/badge-debug.md @@ -0,0 +1,7 @@ +## zh-CN + +调试使用。 + +## en-US + +debug use. diff --git a/packages/meta/src/float-button/demo/badge-debug.tsx b/packages/meta/src/float-button/demo/badge-debug.tsx new file mode 100644 index 0000000..81e1143 --- /dev/null +++ b/packages/meta/src/float-button/demo/badge-debug.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import { ConfigProvider, FloatButton, Slider } from 'antd'; +import type { ConfigProviderProps, GetProp } from 'antd'; + +type AliasToken = GetProp['token']; + +const App: React.FC = () => { + const [radius, setRadius] = useState(0); + + const token: Partial = { + borderRadius: radius, + }; + + return ( + <> + + + + + + ); +}; + +export default App; diff --git a/packages/meta/src/float-button/demo/badge.md b/packages/meta/src/float-button/demo/badge.md new file mode 100644 index 0000000..12fc6e9 --- /dev/null +++ b/packages/meta/src/float-button/demo/badge.md @@ -0,0 +1,7 @@ +## zh-CN + +右上角附带圆形徽标数字的悬浮按钮。 + +## en-US + +FloatButton with Badge. diff --git a/packages/meta/src/float-button/demo/badge.tsx b/packages/meta/src/float-button/demo/badge.tsx new file mode 100644 index 0000000..879f95a --- /dev/null +++ b/packages/meta/src/float-button/demo/badge.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => ( + <> + + + custom badge color} + badge={{ count: 5, color: 'blue' }} + /> + + + + } /> + + + + +); + +export default App; diff --git a/packages/meta/src/float-button/demo/basic.md b/packages/meta/src/float-button/demo/basic.md new file mode 100644 index 0000000..c2120a5 --- /dev/null +++ b/packages/meta/src/float-button/demo/basic.md @@ -0,0 +1,7 @@ +## zh-CN + +最简单的用法。 + +## en-US + +The most basic usage. diff --git a/packages/meta/src/float-button/demo/basic.tsx b/packages/meta/src/float-button/demo/basic.tsx new file mode 100644 index 0000000..0b97fd6 --- /dev/null +++ b/packages/meta/src/float-button/demo/basic.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => console.log('onClick')} />; + +export default App; diff --git a/packages/meta/src/float-button/demo/controlled.md b/packages/meta/src/float-button/demo/controlled.md new file mode 100644 index 0000000..ac17a7d --- /dev/null +++ b/packages/meta/src/float-button/demo/controlled.md @@ -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. diff --git a/packages/meta/src/float-button/demo/controlled.tsx b/packages/meta/src/float-button/demo/controlled.tsx new file mode 100644 index 0000000..95489b5 --- /dev/null +++ b/packages/meta/src/float-button/demo/controlled.tsx @@ -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 ( + <> + } + > + + } /> + + + + ); +}; + +export default App; diff --git a/packages/meta/src/float-button/demo/description.md b/packages/meta/src/float-button/demo/description.md new file mode 100644 index 0000000..a20129e --- /dev/null +++ b/packages/meta/src/float-button/demo/description.md @@ -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. diff --git a/packages/meta/src/float-button/demo/description.tsx b/packages/meta/src/float-button/demo/description.tsx new file mode 100644 index 0000000..a0f8474 --- /dev/null +++ b/packages/meta/src/float-button/demo/description.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { FileTextOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => ( + <> + } + description="HELP INFO" + shape="square" + style={{ right: 24 }} + /> + + } + description="HELP" + shape="square" + style={{ right: 164 }} + /> + +); + +export default App; diff --git a/packages/meta/src/float-button/demo/group-menu.md b/packages/meta/src/float-button/demo/group-menu.md new file mode 100644 index 0000000..1f53c07 --- /dev/null +++ b/packages/meta/src/float-button/demo/group-menu.md @@ -0,0 +1,7 @@ +## zh-CN + +设置 `trigger` 属性即可开启菜单模式。提供 `hover` 和 `click` 两种触发方式。 + +## en-US + +Open menu mode with `trigger`, which could be `hover` or `click`. diff --git a/packages/meta/src/float-button/demo/group-menu.tsx b/packages/meta/src/float-button/demo/group-menu.tsx new file mode 100644 index 0000000..302600f --- /dev/null +++ b/packages/meta/src/float-button/demo/group-menu.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { CommentOutlined, CustomerServiceOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => ( + <> + } + > + + } /> + + } + > + + } /> + + +); + +export default App; diff --git a/packages/meta/src/float-button/demo/group.md b/packages/meta/src/float-button/demo/group.md new file mode 100644 index 0000000..a5094ac --- /dev/null +++ b/packages/meta/src/float-button/demo/group.md @@ -0,0 +1,7 @@ +## zh-CN + +按钮组合使用时,推荐使用 ``,并通过设置 `shape` 属性改变悬浮按钮组的形状。悬浮按钮组的 `shape` 会覆盖内部 FloatButton 的 `shape` 属性。 + +## en-US + +When multiple buttons are used together, `` 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. diff --git a/packages/meta/src/float-button/demo/group.tsx b/packages/meta/src/float-button/demo/group.tsx new file mode 100644 index 0000000..60c8cd5 --- /dev/null +++ b/packages/meta/src/float-button/demo/group.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => ( + <> + + } /> + + + + + } /> + + } /> + + + +); + +export default App; diff --git a/packages/meta/src/float-button/demo/render-panel.md b/packages/meta/src/float-button/demo/render-panel.md new file mode 100644 index 0000000..70bcbec --- /dev/null +++ b/packages/meta/src/float-button/demo/render-panel.md @@ -0,0 +1,7 @@ +## zh-CN + +调试用组件,请勿直接使用。 + +## en-US + +Debug usage. Do not use in your production. diff --git a/packages/meta/src/float-button/demo/render-panel.tsx b/packages/meta/src/float-button/demo/render-panel.tsx new file mode 100644 index 0000000..9c3c35e --- /dev/null +++ b/packages/meta/src/float-button/demo/render-panel.tsx @@ -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 = () => ( +
+ + } /> + } + description="HELP" + shape="square" + type="primary" + /> + }, + { icon: }, + { icon: }, + ]} + /> + } + trigger="click" + items={[ + { icon: }, + { icon: }, + { icon: }, + ]} + /> +
+); + +export default App; diff --git a/packages/meta/src/float-button/demo/shape.md b/packages/meta/src/float-button/demo/shape.md new file mode 100644 index 0000000..bd624dd --- /dev/null +++ b/packages/meta/src/float-button/demo/shape.md @@ -0,0 +1,7 @@ +## zh-CN + +通过 `shape` 设置不同的形状。 + +## en-US + +Change the shape of the FloatButton with `shape`. diff --git a/packages/meta/src/float-button/demo/shape.tsx b/packages/meta/src/float-button/demo/shape.tsx new file mode 100644 index 0000000..6fc6f72 --- /dev/null +++ b/packages/meta/src/float-button/demo/shape.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { CustomerServiceOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => ( + <> + } + /> + } + /> + +); + +export default App; diff --git a/packages/meta/src/float-button/demo/tooltip.md b/packages/meta/src/float-button/demo/tooltip.md new file mode 100644 index 0000000..e0f254c --- /dev/null +++ b/packages/meta/src/float-button/demo/tooltip.md @@ -0,0 +1,7 @@ +## zh-CN + +设置 tooltip 属性,即可开启气泡卡片。 + +## en-US + +Setting `tooltip` prop to show FloatButton with tooltip. diff --git a/packages/meta/src/float-button/demo/tooltip.tsx b/packages/meta/src/float-button/demo/tooltip.tsx new file mode 100644 index 0000000..73574da --- /dev/null +++ b/packages/meta/src/float-button/demo/tooltip.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => Documents} />; + +export default App; diff --git a/packages/meta/src/float-button/demo/type.md b/packages/meta/src/float-button/demo/type.md new file mode 100644 index 0000000..30c65cb --- /dev/null +++ b/packages/meta/src/float-button/demo/type.md @@ -0,0 +1,7 @@ +## zh-CN + +通过 `type` 改变悬浮按钮的类型。 + +## en-US + +Change the type of the FloatButton with `type`. diff --git a/packages/meta/src/float-button/demo/type.tsx b/packages/meta/src/float-button/demo/type.tsx new file mode 100644 index 0000000..58f7292 --- /dev/null +++ b/packages/meta/src/float-button/demo/type.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import { FloatButton } from 'antd'; + +const App: React.FC = () => ( + <> + } type="primary" style={{ right: 24 }} /> + } type="default" style={{ right: 94 }} /> + +); + +export default App; diff --git a/packages/meta/src/float-button/index.ts b/packages/meta/src/float-button/index.ts new file mode 100644 index 0000000..663dda1 --- /dev/null +++ b/packages/meta/src/float-button/index.ts @@ -0,0 +1,10 @@ +import BackTop from './BackTop'; +import FloatButton from './FloatButton'; +import FloatButtonGroup from './FloatButtonGroup'; +import PurePanel from './PurePanel'; + +FloatButton.BackTop = BackTop; +FloatButton.Group = FloatButtonGroup; +FloatButton._InternalPanelDoNotUseOrYouWillBeFired = PurePanel; + +export default FloatButton; diff --git a/packages/meta/src/float-button/index.zh-CN.md b/packages/meta/src/float-button/index.zh-CN.md new file mode 100644 index 0000000..fb4f05f --- /dev/null +++ b/packages/meta/src/float-button/index.zh-CN.md @@ -0,0 +1,71 @@ +--- +category: Components +group: 通用 +title: FloatButton +subtitle: 悬浮按钮 +toc: content +description: 悬浮于页面上方的按钮。 +demo: + cols: 2 +tag: 5.0.0 +--- + +## 何时使用 + +- 用于网站上的全局功能; +- 无论浏览到何处都可以看见的按钮。 + +## 代码演示 + + +基本 +类型 +形状 +描述 +含有气泡卡片的悬浮按钮 +浮动按钮组 +菜单模式 +受控模式 +回到顶部 +徽标数 +调试小圆点使用 +\_InternalPanelDoNotUseOrYouWillBeFired + +## API + +通用属性参考:[通用属性](/docs/react/common-props) + +> 自 `antd@5.0.0` 版本开始提供该组件。 + +### 共同的 API + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| icon | 自定义图标 | ReactNode | - | | +| description | 文字及其它内容 | ReactNode | - | | +| tooltip | 气泡卡片的内容 | ReactNode \| () => ReactNode | - | | +| type | 设置按钮类型 | `default` \| `primary` | `default` | | +| shape | 设置按钮形状 | `circle` \| `square` | `circle` | | +| onClick | 点击按钮时的回调 | (event) => void | - | | +| href | 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 | string | - | | +| target | 相当于 a 标签的 target 属性,href 存在时生效 | string | - | | +| badge | 带徽标数字的悬浮按钮(不支持 `status` 以及相关属性) | [BadgeProps](/components/badge-cn#api) | - | 5.4.0 | + +### FloatButton.Group + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| shape | 设置包含的 FloatButton 按钮形状 | `circle` \| `square` | `circle` | | +| trigger | 触发方式(有触发方式为菜单模式) | `click` \| `hover` | - | | +| open | 受控展开,需配合 trigger 一起使用 | boolean | - | | +| closeIcon | 自定义关闭按钮 | React.ReactNode | `` | | +| onOpenChange | 展开收起时的回调,需配合 trigger 一起使用 | (open: boolean) => void | - | | + +### FloatButton.BackTop + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| ---------------- | ---------------------------------- | ----------------- | ------------ | ---- | +| duration | 回到顶部所需时间(ms) | number | 450 | | +| target | 设置需要监听其滚动事件的元素 | () => HTMLElement | () => window | | +| visibilityHeight | 滚动高度达到此参数值才出现 BackTop | number | 400 | | +| onClick | 点击按钮的回调函数 | () => void | - | | diff --git a/packages/meta/src/float-button/interface.ts b/packages/meta/src/float-button/interface.ts new file mode 100644 index 0000000..72f0fba --- /dev/null +++ b/packages/meta/src/float-button/interface.ts @@ -0,0 +1,66 @@ +import type React from 'react'; + +import type { BadgeProps } from '../badge'; +import type { TooltipProps } from '../tooltip'; + +export type FloatButtonElement = HTMLAnchorElement & HTMLButtonElement; + +export interface FloatButtonRef { + nativeElement: FloatButtonElement | null; +} + +export type FloatButtonType = 'default' | 'primary'; + +export type FloatButtonShape = 'circle' | 'square'; + +export type FloatButtonGroupTrigger = 'click' | 'hover'; + +export type FloatButtonBadgeProps = Omit; + +export interface FloatButtonProps extends React.DOMAttributes { + prefixCls?: string; + className?: string; + rootClassName?: string; + style?: React.CSSProperties; + icon?: React.ReactNode; + description?: React.ReactNode; + type?: FloatButtonType; + shape?: FloatButtonShape; + tooltip?: TooltipProps['title']; + href?: string; + target?: React.HTMLAttributeAnchorTarget; + badge?: FloatButtonBadgeProps; + ['aria-label']?: React.HtmlHTMLAttributes['aria-label']; +} + +export interface FloatButtonContentProps extends React.DOMAttributes { + className?: string; + icon?: FloatButtonProps['icon']; + description?: FloatButtonProps['description']; + prefixCls: FloatButtonProps['prefixCls']; +} + +export interface FloatButtonGroupProps extends FloatButtonProps { + // 包含的 Float Button + children: React.ReactNode; + // 触发方式 (有触发方式为菜单模式) + trigger?: FloatButtonGroupTrigger; + // 受控展开 + open?: boolean; + // 关闭按钮自定义图标 + closeIcon?: React.ReactNode; + // 展开收起的回调 + onOpenChange?: (open: boolean) => void; +} + +export interface BackTopProps extends Omit { + visibilityHeight?: number; + onClick?: React.MouseEventHandler; + target?: () => HTMLElement | Window | Document; + prefixCls?: string; + children?: React.ReactNode; + className?: string; + rootClassName?: string; + style?: React.CSSProperties; + duration?: number; +} diff --git a/packages/meta/src/float-button/style/index.ts b/packages/meta/src/float-button/style/index.ts new file mode 100644 index 0000000..79090d2 --- /dev/null +++ b/packages/meta/src/float-button/style/index.ts @@ -0,0 +1,413 @@ +import type { CSSObject } from '@ant-design/cssinjs'; +import { Keyframes, unit } from '@ant-design/cssinjs'; + +import { resetComponent } from '../../style'; +import { initFadeMotion } from '../../style/motion/fade'; +import { initMotion } from '../../style/motion/motion'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; +import { genStyleHooks, mergeToken } from '../../theme/internal'; +import getOffset from '../util'; + +/** Component only token. Which will handle additional calculation of alias token */ +export interface ComponentToken { + /** + * Offset of the badge dot in a circular button + * @internal + */ + dotOffsetInCircle: number; + /** + * Offset of the badge dot in a square button + * @internal + */ + dotOffsetInSquare: number; +} + +type FloatButtonToken = FullToken<'FloatButton'> & { + floatButtonColor: string; + floatButtonBackgroundColor: string; + floatButtonHoverBackgroundColor: string; + floatButtonFontSize: number; + floatButtonSize: number; + floatButtonIconSize: number | string; + floatButtonBodySize: number | string; + floatButtonBodyPadding: number; + badgeOffset: number | string; + + // Position + floatButtonInsetBlockEnd: number; + floatButtonInsetInlineEnd: number; +}; + +const initFloatButtonGroupMotion = (token: FloatButtonToken) => { + const { componentCls, floatButtonSize, motionDurationSlow, motionEaseInOutCirc } = token; + const groupPrefixCls = `${componentCls}-group`; + const moveDownIn = new Keyframes('antFloatButtonMoveDownIn', { + '0%': { + transform: `translate3d(0, ${unit(floatButtonSize)}, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + '100%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + }); + + const moveDownOut = new Keyframes('antFloatButtonMoveDownOut', { + '0%': { + transform: 'translate3d(0, 0, 0)', + transformOrigin: '0 0', + opacity: 1, + }, + '100%': { + transform: `translate3d(0, ${unit(floatButtonSize)}, 0)`, + transformOrigin: '0 0', + opacity: 0, + }, + }); + + return [ + { + [`${groupPrefixCls}-wrap`]: { + ...initMotion(`${groupPrefixCls}-wrap`, moveDownIn, moveDownOut, motionDurationSlow, true), + }, + }, + { + [`${groupPrefixCls}-wrap`]: { + [` + &${groupPrefixCls}-wrap-enter, + &${groupPrefixCls}-wrap-appear + `]: { + opacity: 0, + animationTimingFunction: motionEaseInOutCirc, + }, + [`&${groupPrefixCls}-wrap-leave`]: { + animationTimingFunction: motionEaseInOutCirc, + }, + }, + }, + ]; +}; + +// ============================== Group ============================== +const floatButtonGroupStyle: GenerateStyle = (token) => { + const { + antCls, + componentCls, + floatButtonSize, + margin, + borderRadiusLG, + borderRadiusSM, + badgeOffset, + floatButtonBodyPadding, + calc, + } = token; + const groupPrefixCls = `${componentCls}-group`; + return { + [groupPrefixCls]: { + ...resetComponent(token), + zIndex: 99, + display: 'block', + border: 'none', + position: 'fixed', + width: floatButtonSize, + height: 'auto', + boxShadow: 'none', + minHeight: floatButtonSize, + insetInlineEnd: token.floatButtonInsetInlineEnd, + insetBlockEnd: token.floatButtonInsetBlockEnd, + borderRadius: borderRadiusLG, + + [`${groupPrefixCls}-wrap`]: { + zIndex: -1, + display: 'block', + position: 'relative', + marginBottom: margin, + }, + [`&${groupPrefixCls}-rtl`]: { + direction: 'rtl', + }, + [componentCls]: { + position: 'static', + }, + }, + [`${groupPrefixCls}-circle`]: { + [`${componentCls}-circle:not(:last-child)`]: { + marginBottom: token.margin, + [`${componentCls}-body`]: { + width: floatButtonSize, + height: floatButtonSize, + borderRadius: '50%', + }, + }, + }, + [`${groupPrefixCls}-square`]: { + [`${componentCls}-square`]: { + borderRadius: 0, + padding: 0, + '&:first-child': { + borderStartStartRadius: borderRadiusLG, + borderStartEndRadius: borderRadiusLG, + }, + '&:last-child': { + borderEndStartRadius: borderRadiusLG, + borderEndEndRadius: borderRadiusLG, + }, + '&:not(:last-child)': { + borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorSplit}`, + }, + [`${antCls}-badge`]: { + [`${antCls}-badge-count`]: { + top: calc(calc(floatButtonBodyPadding).add(badgeOffset)).mul(-1).equal(), + insetInlineEnd: calc(calc(floatButtonBodyPadding).add(badgeOffset)).mul(-1).equal(), + }, + }, + }, + [`${groupPrefixCls}-wrap`]: { + display: 'block', + borderRadius: borderRadiusLG, + boxShadow: token.boxShadowSecondary, + [`${componentCls}-square`]: { + boxShadow: 'none', + marginTop: 0, + borderRadius: 0, + padding: floatButtonBodyPadding, + '&:first-child': { + borderStartStartRadius: borderRadiusLG, + borderStartEndRadius: borderRadiusLG, + }, + '&:last-child': { + borderEndStartRadius: borderRadiusLG, + borderEndEndRadius: borderRadiusLG, + }, + '&:not(:last-child)': { + borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorSplit}`, + }, + [`${componentCls}-body`]: { + width: token.floatButtonBodySize, + height: token.floatButtonBodySize, + }, + }, + }, + }, + [`${groupPrefixCls}-circle-shadow`]: { + boxShadow: 'none', + }, + [`${groupPrefixCls}-square-shadow`]: { + boxShadow: token.boxShadowSecondary, + [`${componentCls}-square`]: { + boxShadow: 'none', + padding: floatButtonBodyPadding, + [`${componentCls}-body`]: { + width: token.floatButtonBodySize, + height: token.floatButtonBodySize, + borderRadius: borderRadiusSM, + }, + }, + }, + }; +}; + +// ============================== Shared ============================== +const sharedFloatButtonStyle: GenerateStyle = (token) => { + const { + antCls, + componentCls, + floatButtonBodyPadding, + floatButtonIconSize, + floatButtonSize, + borderRadiusLG, + badgeOffset, + dotOffsetInSquare, + dotOffsetInCircle, + calc, + } = token; + return { + [componentCls]: { + ...resetComponent(token), + border: 'none', + position: 'fixed', + cursor: 'pointer', + zIndex: 99, + // Do not remove the 'display: block' here. + // Deleting it will cause marginBottom to become ineffective. + // Ref: https://github.com/ant-design/ant-design/issues/44700 + display: 'block', + width: floatButtonSize, + height: floatButtonSize, + insetInlineEnd: token.floatButtonInsetInlineEnd, + insetBlockEnd: token.floatButtonInsetBlockEnd, + boxShadow: token.boxShadowSecondary, + // Pure Panel + '&-pure': { + position: 'relative', + inset: 'auto', + }, + '&:empty': { + display: 'none', + }, + [`${antCls}-badge`]: { + width: '100%', + height: '100%', + [`${antCls}-badge-count`]: { + transform: 'translate(0, 0)', + transformOrigin: 'center', + top: calc(badgeOffset).mul(-1).equal(), + insetInlineEnd: calc(badgeOffset).mul(-1).equal(), + }, + }, + [`${componentCls}-body`]: { + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + transition: `all ${token.motionDurationMid}`, + [`${componentCls}-content`]: { + overflow: 'hidden', + textAlign: 'center', + minHeight: floatButtonSize, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + padding: `${unit(calc(floatButtonBodyPadding).div(2).equal())} ${unit( + floatButtonBodyPadding, + )}`, + [`${componentCls}-icon`]: { + textAlign: 'center', + margin: 'auto', + width: floatButtonIconSize, + fontSize: floatButtonIconSize, + lineHeight: 1, + }, + }, + }, + }, + + [`${componentCls}-rtl`]: { + direction: 'rtl', + }, + [`${componentCls}-circle`]: { + height: floatButtonSize, + borderRadius: '50%', + [`${antCls}-badge`]: { + [`${antCls}-badge-dot`]: { + top: dotOffsetInCircle, + insetInlineEnd: dotOffsetInCircle, + }, + }, + [`${componentCls}-body`]: { + borderRadius: '50%', + }, + }, + [`${componentCls}-square`]: { + height: 'auto', + minHeight: floatButtonSize, + borderRadius: borderRadiusLG, + [`${antCls}-badge`]: { + [`${antCls}-badge-dot`]: { + top: dotOffsetInSquare, + insetInlineEnd: dotOffsetInSquare, + }, + }, + [`${componentCls}-body`]: { + height: 'auto', + borderRadius: borderRadiusLG, + }, + }, + [`${componentCls}-default`]: { + backgroundColor: token.floatButtonBackgroundColor, + transition: `background-color ${token.motionDurationMid}`, + [`${componentCls}-body`]: { + backgroundColor: token.floatButtonBackgroundColor, + transition: `background-color ${token.motionDurationMid}`, + '&:hover': { + backgroundColor: token.colorFillContent, + }, + [`${componentCls}-content`]: { + [`${componentCls}-icon`]: { + color: token.colorText, + }, + [`${componentCls}-description`]: { + display: 'flex', + alignItems: 'center', + lineHeight: unit(token.fontSizeLG), + color: token.colorText, + fontSize: token.fontSizeSM, + }, + }, + }, + }, + [`${componentCls}-primary`]: { + backgroundColor: token.colorPrimary, + [`${componentCls}-body`]: { + backgroundColor: token.colorPrimary, + transition: `background-color ${token.motionDurationMid}`, + '&:hover': { + backgroundColor: token.colorPrimaryHover, + }, + [`${componentCls}-content`]: { + [`${componentCls}-icon`]: { + color: token.colorTextLightSolid, + }, + [`${componentCls}-description`]: { + display: 'flex', + alignItems: 'center', + lineHeight: unit(token.fontSizeLG), + color: token.colorTextLightSolid, + fontSize: token.fontSizeSM, + }, + }, + }, + }, + }; +}; + +// ============================== Export ============================== +export const prepareComponentToken: GetDefaultToken<'FloatButton'> = (token) => ({ + dotOffsetInCircle: getOffset(token.controlHeightLG / 2), + dotOffsetInSquare: getOffset(token.borderRadiusLG), +}); + +export default genStyleHooks( + 'FloatButton', + (token) => { + const { + colorTextLightSolid, + colorBgElevated, + controlHeightLG, + marginXXL, + marginLG, + fontSize, + fontSizeIcon, + controlItemBgHover, + paddingXXS, + calc, + } = token; + + const floatButtonToken = mergeToken(token, { + floatButtonBackgroundColor: colorBgElevated, + floatButtonColor: colorTextLightSolid, + floatButtonHoverBackgroundColor: controlItemBgHover, + floatButtonFontSize: fontSize, + floatButtonIconSize: calc(fontSizeIcon).mul(1.5).equal(), + floatButtonSize: controlHeightLG, + floatButtonInsetBlockEnd: marginXXL, + floatButtonInsetInlineEnd: marginLG, + floatButtonBodySize: calc(controlHeightLG).sub(calc(paddingXXS).mul(2)).equal(), + // 这里的 paddingXXS 是简写,完整逻辑是 (controlHeightLG - (controlHeightLG - paddingXXS * 2)) / 2, + floatButtonBodyPadding: paddingXXS, + badgeOffset: calc(paddingXXS).mul(1.5).equal(), + }); + + return [ + floatButtonGroupStyle(floatButtonToken), + sharedFloatButtonStyle(floatButtonToken), + initFadeMotion(token), + initFloatButtonGroupMotion(floatButtonToken), + ]; + }, + prepareComponentToken, +); diff --git a/packages/meta/src/float-button/util.ts b/packages/meta/src/float-button/util.ts new file mode 100644 index 0000000..1cee7c3 --- /dev/null +++ b/packages/meta/src/float-button/util.ts @@ -0,0 +1,10 @@ +const getOffset = (radius: number): number => { + if (radius === 0) { + return 0; + } + // 如果要考虑通用性,这里应该用三角函数 Math.sin(45) + // 但是这个场景比较特殊,始终是等腰直角三角形,所以直接用 Math.sqrt() 开方即可 + return radius - Math.sqrt(radius ** 2 / 2); +}; + +export default getOffset; diff --git a/packages/meta/src/index.tsx b/packages/meta/src/index.tsx index 1eb8bdc..765bea5 100644 --- a/packages/meta/src/index.tsx +++ b/packages/meta/src/index.tsx @@ -16,10 +16,16 @@ export { default as VideoPlayer } from './VideoPlayer' export type { VideoViewProps, VideoViewRef } from './VideoPlayer' export { default as Tabs } from './tabs' export type { TabPaneProps, TabsProps } from './tabs'; -export { default as Button } from './button' export { default as message } from './message' +export { default as Button } from './button' export type { ArgsProps } from './message' export type { ButtonProps, ButtonGroupProps } from './button'; +export { default as FloatButton } from './float-button'; +export type { + FloatButtonGroupProps, + FloatButtonProps, + FloatButtonRef, +} from './float-button/interface'; export { default as Space } from './space' export { default as Slider } from './slider' export type { SliderBaseProps, SliderMarks, SliderRangeProps, SliderSingleProps, SliderTooltipProps } from './slider';