feat(map): 工具箱完成
This commit is contained in:
parent
e3e3c05ae0
commit
2e753a7259
@ -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>
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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<MapRefProps, MapProps>((props, ref) => {
|
||||
width = '100%',
|
||||
...others
|
||||
} = props || {};
|
||||
const mapRef = useRef(null)
|
||||
const mapRef = useRef<MapRef>(null)
|
||||
const drawControlRef = useRef<DrawControlRefProps>(null)
|
||||
|
||||
// 默认绘制配置
|
||||
const [drawConfig, setConfig] = useState<DrawControlProps>({
|
||||
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
|
||||
<Map
|
||||
ref={mapRef}
|
||||
initialViewState={{ ...mapCenter, zoom: 10 }}
|
||||
onLoad={(e) => {
|
||||
onLoad && onLoad(e);
|
||||
}}
|
||||
style={{ width: width, height: height, ...style }}
|
||||
{...merge(defaultMapConfig, others)}
|
||||
>
|
||||
{children}
|
||||
</Map>
|
||||
<div className={classnames(`${componentName}`)}>
|
||||
<Tools
|
||||
buttonList={[
|
||||
{
|
||||
label: '圆形框选',
|
||||
key: 'circle',
|
||||
icon: 'icon-yuan',
|
||||
onClick: () => 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()
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
ref={mapRef}
|
||||
className={classnames(`${componentName}-container`)}
|
||||
initialViewState={{ ...mapCenter, zoom: 10 }}
|
||||
onLoad={(e) => {
|
||||
onLoad && onLoad(e);
|
||||
}}
|
||||
style={{ width: width, height: height, ...style }}
|
||||
{...merge(defaultMapConfig, others)}
|
||||
>
|
||||
{/* <CustomOverlay
|
||||
>
|
||||
<Button>定制图层</Button>
|
||||
</CustomOverlay> */}
|
||||
<DrawControl
|
||||
ref={drawControlRef}
|
||||
onCreate={handleDrawCreate}
|
||||
onUpdate={handleDrawUpdate}
|
||||
onDelete={handleDrawDelete}
|
||||
{...drawConfig}
|
||||
/>
|
||||
{children}
|
||||
</Map>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
59
packages/map/src/components/customOverlay/CustomOverlay.tsx
Normal file
59
packages/map/src/components/customOverlay/CustomOverlay.tsx
Normal file
@ -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<OverlayControl>(() => {
|
||||
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);
|
6
packages/map/src/components/customOverlay/index.tsx
Normal file
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
|
89
packages/map/src/components/drawControl/DrawControl.tsx
Normal file
89
packages/map/src/components/drawControl/DrawControl.tsx
Normal file
@ -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<typeof MapboxDraw>[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<DrawControlRefProps, DrawControlProps>((props, ref) => {
|
||||
const drawRef = useRef<MapboxDrawOptions>(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
|
||||
}
|
||||
)
|
||||
// 修改模式
|
||||
// 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
|
8
packages/map/src/components/drawControl/index.tsx
Normal file
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
|
69
packages/map/src/components/tools/Tools.tsx
Normal file
69
packages/map/src/components/tools/Tools.tsx
Normal file
@ -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<ToolsRefProps, ToolsProps>((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 (
|
||||
<div className={componentName}>
|
||||
<ul className={classNames(`${componentName}-navs`, { [`${componentName}-navs_active`]: state })}>
|
||||
{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={toggle}
|
||||
icon={<IconFont icon={state ? 'icon-guanbi' : 'icon-kuangxuangongju'} />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Tools
|
64
packages/map/src/components/tools/index.less
Normal file
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
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
|
0
packages/map/src/hooks/useDraw.ts
Normal file
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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,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)
|
||||
}
|
107
packages/meta/src/float-button/BackTop.tsx
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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`.
|
22
packages/meta/src/float-button/demo/shape.tsx
Normal file
22
packages/meta/src/float-button/demo/shape.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomerServiceOutlined } from '@ant-design/icons';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<FloatButton
|
||||
shape="circle"
|
||||
type="primary"
|
||||
style={{ right: 94 }}
|
||||
icon={<CustomerServiceOutlined />}
|
||||
/>
|
||||
<FloatButton
|
||||
shape="square"
|
||||
type="primary"
|
||||
style={{ right: 24 }}
|
||||
icon={<CustomerServiceOutlined />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/tooltip.md
Normal file
7
packages/meta/src/float-button/demo/tooltip.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
设置 tooltip 属性,即可开启气泡卡片。
|
||||
|
||||
## en-US
|
||||
|
||||
Setting `tooltip` prop to show FloatButton with tooltip.
|
6
packages/meta/src/float-button/demo/tooltip.tsx
Normal file
6
packages/meta/src/float-button/demo/tooltip.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => <FloatButton tooltip={<div>Documents</div>} />;
|
||||
|
||||
export default App;
|
7
packages/meta/src/float-button/demo/type.md
Normal file
7
packages/meta/src/float-button/demo/type.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
通过 `type` 改变悬浮按钮的类型。
|
||||
|
||||
## en-US
|
||||
|
||||
Change the type of the FloatButton with `type`.
|
12
packages/meta/src/float-button/demo/type.tsx
Normal file
12
packages/meta/src/float-button/demo/type.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { FloatButton } from 'antd';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<FloatButton icon={<QuestionCircleOutlined />} type="primary" style={{ right: 24 }} />
|
||||
<FloatButton icon={<QuestionCircleOutlined />} type="default" style={{ right: 94 }} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default App;
|
10
packages/meta/src/float-button/index.ts
Normal file
10
packages/meta/src/float-button/index.ts
Normal file
@ -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;
|
71
packages/meta/src/float-button/index.zh-CN.md
Normal file
71
packages/meta/src/float-button/index.zh-CN.md
Normal file
@ -0,0 +1,71 @@
|
||||
---
|
||||
category: Components
|
||||
group: 通用
|
||||
title: FloatButton
|
||||
subtitle: 悬浮按钮
|
||||
toc: content
|
||||
description: 悬浮于页面上方的按钮。
|
||||
demo:
|
||||
cols: 2
|
||||
tag: 5.0.0
|
||||
---
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 用于网站上的全局功能;
|
||||
- 无论浏览到何处都可以看见的按钮。
|
||||
|
||||
## 代码演示
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<code src="./demo/basic.tsx" iframe="360">基本</code>
|
||||
<code src="./demo/type.tsx" iframe="360">类型</code>
|
||||
<code src="./demo/shape.tsx" iframe="360">形状</code>
|
||||
<code src="./demo/description.tsx" iframe="360">描述</code>
|
||||
<code src="./demo/tooltip.tsx" iframe="360">含有气泡卡片的悬浮按钮</code>
|
||||
<code src="./demo/group.tsx" iframe="360">浮动按钮组</code>
|
||||
<code src="./demo/group-menu.tsx" iframe="360">菜单模式</code>
|
||||
<code src="./demo/controlled.tsx" iframe="360">受控模式</code>
|
||||
<code src="./demo/back-top.tsx" iframe="360">回到顶部</code>
|
||||
<code src="./demo/badge.tsx" iframe="360">徽标数</code>
|
||||
<code src="./demo/badge-debug.tsx" iframe="360" debug>调试小圆点使用</code>
|
||||
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
|
||||
|
||||
## 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 | `<CloseOutlined />` | |
|
||||
| onOpenChange | 展开收起时的回调,需配合 trigger 一起使用 | (open: boolean) => void | - | |
|
||||
|
||||
### FloatButton.BackTop
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| ---------------- | ---------------------------------- | ----------------- | ------------ | ---- |
|
||||
| duration | 回到顶部所需时间(ms) | number | 450 | |
|
||||
| target | 设置需要监听其滚动事件的元素 | () => HTMLElement | () => window | |
|
||||
| visibilityHeight | 滚动高度达到此参数值才出现 BackTop | number | 400 | |
|
||||
| onClick | 点击按钮的回调函数 | () => void | - | |
|
66
packages/meta/src/float-button/interface.ts
Normal file
66
packages/meta/src/float-button/interface.ts
Normal file
@ -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<BadgeProps, 'status' | 'text' | 'title' | 'children'>;
|
||||
|
||||
export interface FloatButtonProps extends React.DOMAttributes<FloatButtonElement> {
|
||||
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<HTMLElement>['aria-label'];
|
||||
}
|
||||
|
||||
export interface FloatButtonContentProps extends React.DOMAttributes<HTMLDivElement> {
|
||||
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<FloatButtonProps, 'target'> {
|
||||
visibilityHeight?: number;
|
||||
onClick?: React.MouseEventHandler<FloatButtonElement>;
|
||||
target?: () => HTMLElement | Window | Document;
|
||||
prefixCls?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
style?: React.CSSProperties;
|
||||
duration?: number;
|
||||
}
|
413
packages/meta/src/float-button/style/index.ts
Normal file
413
packages/meta/src/float-button/style/index.ts
Normal file
@ -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<FloatButtonToken, CSSObject> = (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<FloatButtonToken, CSSObject> = (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<FloatButtonToken>(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,
|
||||
);
|
10
packages/meta/src/float-button/util.ts
Normal file
10
packages/meta/src/float-button/util.ts
Normal file
@ -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;
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user