diff --git a/src/config.json b/src/config.json index 63d3cd753aea9dc9bc5080a051ca323822a80594..9ac31abbaa785881e0b51d863d3071e37186576f 100644 --- a/src/config.json +++ b/src/config.json @@ -232,15 +232,15 @@ "show": true, "author": "swag~jun" }, - { - "version": "1.0.0", - "name": "CircleProgress", - "type": "component", - "cName": "进度条", - "desc": "展示操作或任务的当前进度。", - "sort": 7, - "show": true, - "author": "swag~jun" + { + "version": "1.0.0", + "name": "CircleProgress", + "type": "component", + "cName": "进度条", + "desc": "展示操作或任务的当前进度。", + "sort": 7, + "show": true, + "author": "swag~jun" } ] }, @@ -276,6 +276,16 @@ "sort": 3, "show": true, "author": "swag~jun" + }, + { + "version": "1.0.0", + "name": "Elevator", + "type": "component", + "cName": "电梯楼层", + "desc": "用于列表快速定位以及索引的显示", + "sort": 4, + "show": true, + "author": "songsong" } ] }, @@ -336,7 +346,18 @@ }, { "name": "特色组件", - "packages": [] + "packages": [ + { + "version": "1.0.0", + "name": "Signature", + "type": "component", + "cName": "签字", + "desc": "基于Canvas的签名组件", + "sort": 1, + "show": true, + "author": "songsong" + } + ] } ] } \ No newline at end of file diff --git a/src/packages/avatar/avatar.tsx b/src/packages/avatar/avatar.tsx index 333a93b588cafd3bcb145c55b4716565a356850b..266818350d629c2d8a7ab1acd26a1c1219b1f0ed 100644 --- a/src/packages/avatar/avatar.tsx +++ b/src/packages/avatar/avatar.tsx @@ -10,22 +10,24 @@ export interface AvatarProps { bgColor: string prefixCls: string src: string + className: string + style: React.CSSProperties } export type AvatarSize = 'large' | 'normal' | 'small' export type AvatarShape = 'round' | 'square' -const defaultProps: AvatarProps = { +const defaultProps = { size: 'normal', icon: '', shape: 'round', bgColor: '#eee', prefixCls: 'nut-avatar', src: '', -} +} as AvatarProps export const Avatar: FunctionComponent< Partial & React.HTMLAttributes > = (props) => { - const { children, prefixCls, size, shape, bgColor, src, icon, className, ...rest } = { + const { children, prefixCls, size, shape, bgColor, src, icon, className, style, ...rest } = { ...defaultProps, ...props, } @@ -41,6 +43,7 @@ export const Avatar: FunctionComponent< height: sizeValue.indexOf(size) > -1 ? '' : `${size}px`, backgroundImage: src ? `url(${src})` : '', backgroundColor: `${bgColor}`, + ...style, } const iconStyles = !!icon && !src ? icon : '' const handleClick: MouseEventHandler = (e) => { diff --git a/src/packages/avatar/doc.md b/src/packages/avatar/doc.md index 618346d2fd0b93839c4969c86b3ee7931ebe78ec..a5790cda6a8ee516e8cf248590c5ea88f2a9bff9 100644 --- a/src/packages/avatar/doc.md +++ b/src/packages/avatar/doc.md @@ -6,7 +6,7 @@ ### 安装 ``` javascript -import { Avatar } from '@nutui/nutui'; +import { Avatar } from '@nutui/nutui-react'; ``` ## 代码示例 diff --git a/src/packages/cell/cell.scss b/src/packages/cell/cell.scss index b19adfba4816b56603645d2f950715cdccd7fef0..b6b70adf2d764525daf45b9cea93b4bc5c0f463a 100644 --- a/src/packages/cell/cell.scss +++ b/src/packages/cell/cell.scss @@ -1,6 +1,7 @@ .nut-cell { position: relative; display: flex; + align-items: center; width: 100%; line-height: 20px; padding: 13px 16px; @@ -57,11 +58,12 @@ flex: 1; &--icon { flex-direction: row; - .icon { - margin-right: 10px; - } + overflow: hidden; } } + &__icon { + margin-right: 10px; + } &__subtitle { font-size: $cell-title-desc-font; } diff --git a/src/packages/cell/cell.tsx b/src/packages/cell/cell.tsx index 0ec0ce1c8b81bdc62bf436db082554aaa84ff476..c6bdf8bf5f77026ee6932bac334d9edc09f83eb0 100644 --- a/src/packages/cell/cell.tsx +++ b/src/packages/cell/cell.tsx @@ -14,7 +14,7 @@ export interface CellProps { replace: boolean url: string icon: string - classPrefix: string + className: string extra: ReactNode click: (event: React.MouseEvent) => void } @@ -28,7 +28,7 @@ const defaultProps = { replace: false, url: '', icon: '', - classPrefix: 'nutui-cell', + className: '', extra: '', click: (event: React.MouseEvent) => {}, } as CellProps @@ -42,7 +42,7 @@ export const Cell: FunctionComponent & React.HTMLAttributes & React.HTMLAttributes handleClick(event)} {...rest} > @@ -80,12 +84,8 @@ export const Cell: FunctionComponent & React.HTMLAttributes {title || subTitle || icon ? ( <> - {icon ? ( -
- -
- ) : null}
+ {icon ? : null} {subTitle ? ( <>
{title}
@@ -98,18 +98,14 @@ export const Cell: FunctionComponent & React.HTMLAttributes ) : null} {desc ? ( -
+
{desc}
) : null} )} {extra ? extra : null} - {!extra && (isLink || to) ? ( -
- -
- ) : null} + {!extra && (isLink || to) ? : null}
) } diff --git a/src/packages/circleprogress/circleprogress.tsx b/src/packages/circleprogress/circleprogress.tsx index 7d8c37b5de8155c1a687cf61746e6d12185106e8..51a15fc6ba8f4863f0c7f4a0b51c7bcc5e668133 100644 --- a/src/packages/circleprogress/circleprogress.tsx +++ b/src/packages/circleprogress/circleprogress.tsx @@ -1,5 +1,6 @@ import React, { FunctionComponent } from 'react' import bem from '@/utils/bem' +import classNames from 'classnames' import './circleprogress.scss' export interface CircleProgressProps { @@ -7,6 +8,7 @@ export interface CircleProgressProps { progress: string | number isAuto: boolean progressOption: object + className: string } const defaultProps = { strokeInnerWidth: 10, @@ -17,11 +19,21 @@ const defaultProps = { export const CircleProgress: FunctionComponent< Partial & React.HTMLAttributes > = (props) => { - const { children, strokeInnerWidth, progress, isAuto, progressOption } = { + const { + children, + strokeInnerWidth, + progress, + isAuto, + progressOption, + className, + style, + ...restProps + } = { ...defaultProps, ...props, } const b = bem('circleprogress') + const classes = classNames(className, b('')) const option = () => { // 所有进度条的可配置项 let baseOption = { @@ -41,13 +53,18 @@ export const CircleProgress: FunctionComponent< baseOption.startPosition = 'rotate(-90,' + baseOption.cx + ',' + baseOption.cy + ')' return baseOption } + const styles: React.CSSProperties = { + height: `${option().size}px`, + width: `${option().size}px`, + ...style, + } const arcLength = () => { let circleLength = Math.floor(2 * Math.PI * option().radius) let progressLength = ((progress as number) / 100) * circleLength return `${progressLength},${circleLength}` } return ( -
+
& React.HTMLAttributes> = ( props ) => { - const { attract, direction, boundary, style, children } = { ...defaultProps, ...props } + const { attract, direction, boundary, children, className, ...reset } = { + ...defaultProps, + ...props, + } + const b = bem('drag') const elWidth = useRef(0) const elHeight = useRef(0) const screenWidth = useRef(0) @@ -164,8 +170,8 @@ export const Drag: FunctionComponent & React.HTMLAttributes touchStart(event)} > diff --git a/src/packages/elevator/demo.tsx b/src/packages/elevator/demo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0881b660a544d39612d7d1c79667318ce9cc3685 --- /dev/null +++ b/src/packages/elevator/demo.tsx @@ -0,0 +1,187 @@ +import React from 'react' +import { Elevator } from './elevator' + +const ElevatorDemo = () => { + const dataList = [ + { + title: 'A', + list: [ + { + name: '安徽', + id: 1, + }, + ], + }, + { + title: 'B', + list: [ + { + name: '北京', + id: 2, + }, + ], + }, + { + title: 'G', + list: [ + { + name: '广西', + id: 3, + }, + { + name: '广东', + id: 4, + }, + ], + }, + { + title: 'H', + list: [ + { + name: '湖南', + id: 5, + }, + { + name: '湖北', + id: 6, + }, + , + { + name: '琥珀', + id: 7, + }, + ], + }, + ] + const dataList2 = [ + { + num: '一', + list: [ + { + name: '北京', + id: 1, + }, + { + name: '上海', + id: 2, + }, + { + name: '深圳', + id: 3, + }, + { + name: '广州', + id: 4, + }, + { + name: '杭州', + id: 5, + }, + ], + }, + { + num: '二', + list: [ + { + name: '成都', + id: 6, + }, + { + name: '西安', + id: 7, + }, + { + name: '天津', + id: 8, + }, + { + name: '武汉', + id: 9, + }, + { + name: '长沙', + id: 10, + }, + { + name: '重庆', + id: 11, + }, + { + name: '苏州', + id: 12, + }, + { + name: '南京', + id: 13, + }, + ], + }, + { + num: '三', + list: [ + { + name: '西宁', + id: 14, + }, + { + name: '兰州', + id: 15, + }, + { + name: '石家庄', + id: 16, + }, + { + name: '秦皇岛', + id: 17, + }, + { + name: '大连', + id: 18, + }, + { + name: '哈尔滨', + id: 19, + }, + { + name: '长春', + id: 20, + }, + { + name: '太原', + id: 21, + }, + ], + }, + ] + const clickItem = (key: string, item: any) => { + console.log(key, JSON.stringify(item)) + } + + const clickIndex = (key: string) => { + console.log(key) + } + return ( + <> +
+

基础用法

+ clickItem(key, item)} + clickIndex={(key: string) => clickIndex(key)} + > +

自定义索引key

+ clickItem(key, item)} + clickIndex={(key: string) => clickIndex(key)} + > +
+ + ) +} + +export default ElevatorDemo diff --git a/src/packages/elevator/doc.md b/src/packages/elevator/doc.md new file mode 100644 index 0000000000000000000000000000000000000000..5e41f160553df81aa967ceeba64fac8eea56f74d --- /dev/null +++ b/src/packages/elevator/doc.md @@ -0,0 +1,221 @@ +# Elevator 组件 + +### 介绍 + +用于列表快速定位以及索引的显示 + +### 安装 + +## 代码演示 + +### 基础用法 + +```tsx +const dataList = [ + { + title: 'A', + list: [ + { + name: '安徽', + id: 1, + }, + ], + }, + { + title: 'B', + list: [ + { + name: '北京', + id: 2, + }, + ], + }, + { + title: 'G', + list: [ + { + name: '广西', + id: 3, + }, + { + name: '广东', + id: 4, + }, + ], + }, + { + title: 'H', + list: [ + { + name: '湖南', + id: 5, + }, + { + name: '湖北', + id: 6, + }, + , + { + name: '琥珀', + id: 7, + }, + ], + }, +] +const clickItem = (key: string, item: any) => { + console.log(key, JSON.stringify(item)) +} + +const clickIndex = (key: string) => { + console.log(key) +} +``` + +```tsx + clickItem(key, item)} + clickIndex={(key: string) => clickIndex(key)} +> +``` + +### 自定义索引 + +## API + +```tsx +const dataList = [ + { + num: '一', + list: [ + { + name: '北京', + id: 1, + }, + { + name: '上海', + id: 2, + }, + { + name: '深圳', + id: 3, + }, + { + name: '广州', + id: 4, + }, + { + name: '杭州', + id: 5, + }, + ], + }, + { + num: '二', + list: [ + { + name: '成都', + id: 6, + }, + { + name: '西安', + id: 7, + }, + { + name: '天津', + id: 8, + }, + { + name: '武汉', + id: 9, + }, + { + name: '长沙', + id: 10, + }, + { + name: '重庆', + id: 11, + }, + { + name: '苏州', + id: 12, + }, + { + name: '南京', + id: 13, + }, + ], + }, + { + num: '三', + list: [ + { + name: '西宁', + id: 14, + }, + { + name: '兰州', + id: 15, + }, + { + name: '石家庄', + id: 16, + }, + { + name: '秦皇岛', + id: 17, + }, + { + name: '大连', + id: 18, + }, + { + name: '哈尔滨', + id: 19, + }, + { + name: '长春', + id: 20, + }, + { + name: '太原', + id: 21, + }, + ], + }, +] +const clickItem = (key: string, item: any) => { + console.log(key, JSON.stringify(item)) +} + +const clickIndex = (key: string) => { + console.log(key) +} +``` + +```tsx + clickItem(key, item)} + clickIndex={(key: string) => clickIndex(key)} +> +``` + +### Props + +| 字段 | 说明 | 类型 | 默认值 | +| --------- | -------------- | ----------------------------------------------------------- | --------------------- | +| height | 电梯区域的高度 | Number、String | `200px` | +| acceptKey | 索引 key 值 | String | `title` | +| indexList | 索引列表 | Array(item 需包含 id、name 属性, name 支持传入 html 结构) | `[{id: 0, name: ''}]` | + +### Event + +| 名称 | 说明 | 回调参数 | +| ---------- | -------- | -------------------------------------- | +| clickItem | 点击内容 | key: string, item: { id: 0, name: '' } | +| clickIndex | 点击索引 | key: string | diff --git a/src/packages/elevator/elevator.scss b/src/packages/elevator/elevator.scss new file mode 100644 index 0000000000000000000000000000000000000000..da6cd7339731de4b28be2736ff9a6cc50b63fe46 --- /dev/null +++ b/src/packages/elevator/elevator.scss @@ -0,0 +1,71 @@ +.nut-elevator { + width: 100%; + display: block; + position: relative; + &__list { + display: block; + overflow: auto; + &__item { + display: block; + font-size: 12px; + color: #333; + &__code { + display: inline-flex; + position: relative; + height: 35px; + line-height: 35px; + font-size: 14px; + color: #1a1a1a; + padding: 0 20px; + font-weight: 500; + &::after { + content: ' '; + width: 100%; + height: 1px; + position: absolute; + left: 0; + bottom: 7px; + background-color: #eeeff2; + } + } + &__name { + display: flex; + align-items: center; + padding: 0 20px; + height: 30px; + line-height: 30px; + } + } + } + &__code--current { + position: absolute; + right: 60px; + top: 50%; + transform: translateY(-50%); + width: 45px; + height: 45px; + line-height: 45px; + border-radius: 50%; + background: $white; + box-shadow: 0 3px 3px 1px rgba(240, 240, 240, 1); + text-align: center; + } + &__bars { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + padding: 15px 0; + background-color: #eeeff2; + border-radius: 6px; + text-align: center; + z-index: 10; + &__inner { + &__item { + display: block; + padding: 3px; + font-size: 10px; + } + } + } +} diff --git a/src/packages/elevator/elevator.tsx b/src/packages/elevator/elevator.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b513821e2ecf5afe043b80f30fe03ca5c60f53a2 --- /dev/null +++ b/src/packages/elevator/elevator.tsx @@ -0,0 +1,200 @@ +import React, { FunctionComponent, useRef, useEffect, useState } from 'react' +import './elevator.scss' +import bem from '@/utils/bem' + +export interface ElevatorProps { + height: number | string + acceptKey: string + indexList: any + className: string + style: React.CSSProperties + clickItem: (key: string, item: ElevatorData) => void + clickIndex: (key: string) => void +} +const defaultProps = { + height: '200px', + acceptKey: 'title', + indexList: [], + className: '', +} as ElevatorProps +interface ElevatorData { + name: string + id: number | string + [key: string]: string | number +} +export const Elevator: FunctionComponent< + Partial & React.HTMLAttributes +> = (props) => { + const { height, acceptKey, indexList, className, clickItem, clickIndex, ...rest } = { + ...defaultProps, + ...props, + } + const b = bem('elevator') + const spaceHeight = 23 + const listview = useRef(null) + const initData = { + anchorIndex: 0, + listHeight: [] as number[], + listGroup: [] as Element[], + } + const touchState = useRef({ + y1: 0, + y2: 0, + }) + const [currentIndex, setCurrentIndex] = useState(0) + const [scrollStart, setScrollStart] = useState(false) + const state = useRef(initData) + //重置滚动参数 + const resetScrollState = () => { + state.current.anchorIndex = 0 + setCurrentIndex(0) + setScrollStart(false) + touchState.current = { + y1: 0, + y2: 0, + } + } + + const getData = (el: HTMLElement, name: string): string | void => { + const prefix = 'data-' + return el.getAttribute(prefix + name) as string + } + + const calculateHeight = () => { + let height = 0 + + state.current.listHeight.push(height) + for (let i = 0; i < state.current.listGroup.length; i++) { + let item = state.current.listGroup[i] + height += item.clientHeight + state.current.listHeight.push(height) + } + } + + const scrollTo = (index: number) => { + if (!index && index !== 0) { + return + } + + if (!state.current.listHeight.length) { + calculateHeight() + } + if (index < 0) index = 0 + + if (index > state.current.listHeight.length - 2) index = state.current.listHeight.length - 2 + + setCurrentIndex(index) + if (listview.current) { + listview.current.scrollTo(0, state.current.listHeight[index]) + } + } + + const touchMove = (e: React.TouchEvent) => { + let firstTouch = e.touches[0] + touchState.current.y2 = firstTouch.pageY + let delta = ((touchState.current.y2 - touchState.current.y1) / spaceHeight) | 0 + const cacheIndex = state.current.anchorIndex + delta + + setCurrentIndex(cacheIndex) + scrollTo(cacheIndex) + } + + const touchEnd = () => { + resetScrollState() + } + + const touchStart = (e: React.TouchEvent) => { + setScrollStart(true) + let index = Number(getData(e.target as HTMLElement, 'index')) + let firstTouch = e.touches[0] + touchState.current.y1 = firstTouch.pageY + state.current.anchorIndex = +index + setCurrentIndex((currentIndex) => currentIndex + index) + scrollTo(index) + + const target = e.currentTarget as HTMLElement + + target.removeEventListener('touchmove', () => touchMove(e), false) + target.removeEventListener('touchend', touchEnd, false) + target.addEventListener('touchmove', () => touchMove(e), false) + target.addEventListener('touchend', touchEnd, false) + } + + const handleClickItem = (key: string, item: ElevatorData) => { + clickItem && clickItem(key, item) + } + + const handleClickIndex = (key: string) => { + clickIndex && clickIndex(key) + } + + const setListGroup = () => { + if (listview.current) { + const els = listview.current.querySelectorAll('.nut-elevator__list__item') + + els.forEach((el: Element) => { + if (el != null && !state.current.listGroup.includes(el)) { + state.current.listGroup.push(el) + } + }) + } + } + useEffect(() => { + if (listview.current) { + setListGroup() + } + }, [listview.current]) + + return ( +
+
+ {indexList.map((item: any) => { + return ( +
+
{item[acceptKey]}
+ <> + {item.list.map((subitem: any) => { + return ( +
handleClickItem(item[acceptKey], subitem)} + > + {subitem.name} +
+ ) + })} + +
+ ) + })} +
+ {indexList.length && scrollStart ? ( +
{indexList[currentIndex][acceptKey]}
+ ) : null} +
touchStart(event)}> +
+ {indexList.map((item: any, index: number) => { + return ( +
handleClickIndex(item[acceptKey])} + > + {item[acceptKey]} +
+ ) + })} +
+
+
+ ) +} + +Elevator.defaultProps = defaultProps +Elevator.displayName = 'NutElevator' diff --git a/src/packages/elevator/index.ts b/src/packages/elevator/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0cb817ae4d5e5d5774c5a730fc8781403c14e503 --- /dev/null +++ b/src/packages/elevator/index.ts @@ -0,0 +1,2 @@ +import { Elevator } from './elevator' +export default Elevator diff --git a/src/packages/infiniteloading/doc.md b/src/packages/infiniteloading/doc.md index 3efeb99ec3f54cd68c689973d48891428b4bdec6..d18c2f69d7a7fb3ba1f9535167935103eac004f2 100644 --- a/src/packages/infiniteloading/doc.md +++ b/src/packages/infiniteloading/doc.md @@ -7,7 +7,7 @@ ### 安装 ```javascript - import { InfiniteLoading } from '@nutui/nutui'; + import { InfiniteLoading } from '@nutui/nutui-react'; ``` ## 代码演示 diff --git a/src/packages/infiniteloading/infiniteloading.tsx b/src/packages/infiniteloading/infiniteloading.tsx index bc90869137f3d588da32a2d2d218e62c0b006bb2..85896536f38bd817dab3a0f26b2e2d463e5acc24 100644 --- a/src/packages/infiniteloading/infiniteloading.tsx +++ b/src/packages/infiniteloading/infiniteloading.tsx @@ -1,4 +1,6 @@ -import React, { useState, useEffect, useRef, FunctionComponent, useReducer } from 'react' +import React, { useState, useEffect, useRef, FunctionComponent } from 'react' +import bem from '@/utils/bem' +import classNames from 'classnames' import Icon from '@/packages/icon' import './infiniteloading.scss' @@ -14,6 +16,8 @@ export interface InfiniteloadingProps { loadIcon: string loadTxt: string loadMoreTxt: string + className: string + style: React.CSSProperties refresh: (param: () => void) => void loadMore: (param: () => void) => void scrollChange: (param: number) => void @@ -52,9 +56,11 @@ export const Infiniteloading: FunctionComponent< loadIcon, loadTxt, loadMoreTxt, + className, refresh, loadMore, scrollChange, + ...restProps } = { ...defaultProps, ...props, @@ -66,10 +72,12 @@ export const Infiniteloading: FunctionComponent< const isTouching = useRef(false) const beforeScrollTop = useRef(0) const refreshMaxH = useRef(0) - const x = useRef(0) const y = useRef(0) const distance = useRef(0) + const b = bem('infiniteloading') + const classes = classNames(className, b()) + useEffect(() => { const parentElement = getParentElement(scroller.current as HTMLDivElement) as Node & ParentNode scrollEl.current = useWindow ? window : parentElement @@ -213,11 +221,12 @@ export const Infiniteloading: FunctionComponent< return (
touchStart(event)} onTouchMove={(event) => touchMove(event)} onTouchEnd={() => touchEnd()} + {...restProps} >
diff --git a/src/packages/inputnumber/doc.md b/src/packages/inputnumber/doc.md index 5ac49fe4f95727b295b065efd55d9275b2136351..d05c0a089716b873b8cf184e27ade338ff72ebd3 100644 --- a/src/packages/inputnumber/doc.md +++ b/src/packages/inputnumber/doc.md @@ -7,7 +7,7 @@ ### 安装 ``` javascript -import { InputNumber } from '@nutui/nutui'; +import { InputNumber } from '@nutui/nutui-react'; ``` ## 代码演示 diff --git a/src/packages/inputnumber/inputnumber.tsx b/src/packages/inputnumber/inputnumber.tsx index 684b85329057ce2cd3d53f03a57e9e3580968bed..c60a2d0b77477c9629a5284460406df914bf131b 100644 --- a/src/packages/inputnumber/inputnumber.tsx +++ b/src/packages/inputnumber/inputnumber.tsx @@ -15,6 +15,8 @@ export interface InputNumberProps { step: string | number decimalPlaces: string | number isAsync: boolean + className: string + style: React.CSSProperties add: (e: MouseEvent) => void reduce: (e: MouseEvent) => void overlimit: (e: MouseEvent) => void @@ -52,12 +54,15 @@ export const InputNumber: FunctionComponent< decimalPlaces, step, isAsync, + className, + style, add, reduce, change, overlimit, blur, focus, + ...restProps } = { ...defaultProps, ...props, @@ -68,7 +73,17 @@ export const InputNumber: FunctionComponent< }, [modelValue]) const b = bem('inputnumber') - + const classes = classNames( + { + [`${b('')}--disabled`]: disabled, + }, + className, + b('') + ) + const styles = { + height: pxCheck(buttonSize), + ...style, + } const addAllow = (value = Number(inputValue)) => { return value < Number(max) && !disabled } @@ -77,10 +92,6 @@ export const InputNumber: FunctionComponent< return value > Number(min) && !disabled } - const classes = classNames(b(''), { - [`${b('')}--disabled`]: disabled, - }) - const iconMinusClasses = classNames('nut-inputnumber__icon', { 'nut-inputnumber__icon--disabled': !reduceAllow(), }) @@ -97,7 +108,13 @@ export const InputNumber: FunctionComponent< const output_value: number | string = fixedDecimalPlaces(value) change && change(output_value, e) if (!isAsync) { - setInputValue(output_value) + if (Number(output_value) < Number(min)) { + setInputValue(Number(min)) + } else if (Number(output_value) > Number(max)) { + setInputValue(Number(max)) + } else { + setInputValue(output_value) + } } } @@ -153,7 +170,7 @@ export const InputNumber: FunctionComponent< blur && blur(e) } return ( -
+
> = (props) => { - const { price, needSymbol, symbol, decimalDigits, thousands } = { ...defaultProps, ...props } + const { price, needSymbol, symbol, decimalDigits, thousands, className, ...rest } = { + ...defaultProps, + ...props, + } const b = bem('price') const showSymbol = () => { return { __html: (needSymbol ? symbol : '') || '' } @@ -45,18 +51,19 @@ export const Price: FunctionComponent> = (props) => { if (Number(decimalNum) == 0) { decimalNum = 0 } + if (checkPoint(decimalNum)) { decimalNum = Number(decimalNum).toFixed(decimalDigits) decimalNum = typeof decimalNum.split('.') === 'string' ? 0 : decimalNum.split('.')[1] } else { - decimalNum = decimalNum.toString() + decimalNum = 0 } const result = '0.' + decimalNum const resultFixed = Number(result).toFixed(decimalDigits) return String(resultFixed).substring(2, resultFixed.length) } return ( -
+
{needSymbol ? (
) : null} diff --git a/src/packages/signature/demo.tsx b/src/packages/signature/demo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9848e15208d51314360afa841f53e99ef76ba9f4 --- /dev/null +++ b/src/packages/signature/demo.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { Signature } from './signature' + +const SignatureDemo = () => { + const confirm = (canvas: HTMLCanvasElement, data: string) => { + let img = document.createElement('img') + img.src = data + const demo = document.querySelector('.demo1') as HTMLElement + demo.appendChild(img) + } + const clear = () => { + let img = document.querySelector('.demo1 img') + if (img) { + img.remove() + } + } + + const confirm1 = (canvas: HTMLCanvasElement, data: string) => { + let img = document.createElement('img') + img.src = data + const demo = document.querySelector('.demo2') as HTMLElement + demo.appendChild(img) + } + + const clear1 = () => { + let img = document.querySelector('.demo2 img') + if (img) { + img.remove() + } + } + + const demoStyles: React.CSSProperties = { margin: '1em 0' } + return ( + <> +
+

基础用法

+ +

+ Tips: 点击确认按钮,下方显示签名图片 +

+

修改颜色和签字粗细

+ +

+ Tips: 点击确认按钮,下方显示签名图片 +

+
+ + ) +} + +export default SignatureDemo diff --git a/src/packages/signature/doc.md b/src/packages/signature/doc.md new file mode 100644 index 0000000000000000000000000000000000000000..815ced88645b6dd0af63e168f209928fca05762e --- /dev/null +++ b/src/packages/signature/doc.md @@ -0,0 +1,75 @@ +# Signature 组件 + +### 介绍 + +基于 Canvas 的签名组件 + +### 安装 + +## 代码演示 + +### 基础用法 + +```tsx +const confirm = (canvas: HTMLCanvasElement, data: string) => { + let img = document.createElement('img') + img.src = data + const demo = document.querySelector('.demo1') as HTMLElement + demo.appendChild(img) +} +const clear = () => { + let img = document.querySelector('.demo1 img') + if (img) { + img.remove() + } +} +Signature confirm={confirm} clear={clear}> +

+ Tips: 点击确认按钮,下方显示签名图片 +

+``` + +### 修改颜色和签字粗细 + +```tsx +const confirm = (canvas: HTMLCanvasElement, data: string) => { + let img = document.createElement('img') + img.src = data + const demo = document.querySelector('.demo1') as HTMLElement + demo.appendChild(img) +} +const clear = () => { + let img = document.querySelector('.demo1 img') + if (img) { + img.remove() + } +} + +

+ Tips: 点击确认按钮,下方显示签名图片 +

+``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| -------------- | ------------------------------ | ------ | --------------------------------------------------- | +| custom-class | 自定义 class | String | - | +| line-width | 线条的宽度 | Number | 3 | +| stroke-style | 绘图笔触颜色 | String | '#000' | +| type | 图片格式 | String | 'png' | +| un-support-tpl | 不支持 Canvas 情况下的展示文案 | String | '对不起,当前浏览器不支持 Canvas,无法使用本控件!' | + +## Event + +| 字段 | 说明 | 回调参数 | +| ------- | ---------------------------- | -------------------------------- | +| confirm | 点击确认按钮触发事件回调函数 | canvas 和签名图片展示的 data URI | +| clear | 点击重签按钮触发事件回调函数 | 无 | diff --git a/src/packages/signature/index.ts b/src/packages/signature/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b5b0fde717dfadbb5a8feef32eddd1477fd9fdc --- /dev/null +++ b/src/packages/signature/index.ts @@ -0,0 +1,2 @@ +import { Signature } from './signature' +export default Signature diff --git a/src/packages/signature/signature.scss b/src/packages/signature/signature.scss new file mode 100644 index 0000000000000000000000000000000000000000..3721e6dfd2fa90ca5651aea332f5be7a534de264 --- /dev/null +++ b/src/packages/signature/signature.scss @@ -0,0 +1,16 @@ +.nut-signature { + &__inner { + height: 10rem; + margin-bottom: 1rem; + border: 1px solid $signature-border-color; + display: flex; + justify-content: center; + align-items: center; + } + &__unsopport { + font-size: $font-size-base; + } + &__btn { + margin-right: 15px !important; + } +} diff --git a/src/packages/signature/signature.tsx b/src/packages/signature/signature.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93e2508dd28d2b51914249cad52c605cd2ac9753 --- /dev/null +++ b/src/packages/signature/signature.tsx @@ -0,0 +1,146 @@ +import React, { FunctionComponent, useRef, useState, useEffect } from 'react' +import './signature.scss' +import { Button } from '../button/button' +import bem from '@/utils/bem' + +export interface SignatureProps { + type: String + lineWidth: Number + strokeStyle: String + unSupportTpl: String + className: string + confirm?: (canvas: HTMLCanvasElement, dataurl: string) => void + clear?: () => void +} +const defaultProps = { + type: 'png', + lineWidth: 2, + strokeStyle: '#000', + unSupportTpl: '对不起,当前浏览器不支持Canvas,无法使用本控件!', + className: '', +} as SignatureProps +export const Signature: FunctionComponent< + Partial & React.HTMLAttributes +> = (props) => { + const { type, lineWidth, strokeStyle, unSupportTpl, className, ...rest } = { + ...defaultProps, + ...props, + } + const b = bem('signature') + const canvasRef = useRef(null) + const wrapRef = useRef(null) + const [canvasHeight, setCanvasHeight] = useState(0) + const [canvasWidth, setCanvasWidth] = useState(0) + const ctx = useRef(null) + const isCanvasSupported = () => { + let elem = document.createElement('canvas') + return !!(elem.getContext && elem.getContext('2d')) + } + const isSupportTouch = 'ontouchstart' in window + const events = isSupportTouch + ? ['touchstart', 'touchmove', 'touchend', 'touchleave'] + : ['mousedown', 'mousemove', 'mouseup', 'mouseleave'] + + useEffect(() => { + if (isCanvasSupported() && canvasRef.current && wrapRef.current) { + ctx.current = canvasRef.current.getContext('2d') + setCanvasWidth(wrapRef.current.offsetWidth) + setCanvasHeight(wrapRef.current.offsetHeight) + addEvent() + } + }, []) + + const startEventHandler = (event: any) => { + event.preventDefault() + if (ctx.current && canvasRef.current) { + ctx.current.beginPath() + ctx.current.lineWidth = lineWidth as number + ctx.current.strokeStyle = strokeStyle as string + canvasRef.current.addEventListener(events[1], moveEventHandler, false) + canvasRef.current.addEventListener(events[2], endEventHandler, false) + canvasRef.current.addEventListener(events[3], leaveEventHandler, false) + } + } + + const addEvent = () => { + if (canvasRef.current) { + canvasRef.current.addEventListener(events[0], startEventHandler, false) + } + } + + const moveEventHandler = (event: any) => { + event.preventDefault() + + let evt = isSupportTouch ? event.touches[0] : event + if (canvasRef.current && ctx.current) { + let coverPos = canvasRef.current.getBoundingClientRect() + let mouseX = evt.clientX - coverPos.left + let mouseY = evt.clientY - coverPos.top + + ctx.current.lineTo(mouseX, mouseY) + ctx.current.stroke() + } + } + + const endEventHandler = (event: any) => { + event.preventDefault() + if (canvasRef.current) { + canvasRef.current.removeEventListener(events[1], moveEventHandler, false) + canvasRef.current.removeEventListener(events[2], endEventHandler, false) + } + } + const leaveEventHandler = (event: any) => { + event.preventDefault() + if (canvasRef.current) { + canvasRef.current.removeEventListener(events[1], moveEventHandler, false) + canvasRef.current.removeEventListener(events[2], endEventHandler, false) + } + } + const clear = () => { + if (canvasRef.current && ctx.current) { + canvasRef.current.addEventListener(events[2], endEventHandler, false) + ctx.current.clearRect(0, 0, canvasWidth, canvasHeight) + ctx.current.closePath() + } + props.clear && props.clear() + } + + const confirm = () => { + onSave(canvasRef.current as HTMLCanvasElement) + } + + const onSave = (canvas: HTMLCanvasElement) => { + let dataurl + switch (type) { + case 'png': + dataurl = canvas.toDataURL('image/png') + break + case 'jpg': + dataurl = canvas.toDataURL('image/jpeg', 0.8) + break + } + clear() + props.confirm && props.confirm(canvas, dataurl as string) + } + return ( +
+
+ {isCanvasSupported() ? ( + + ) : ( +

{unSupportTpl}

+ )} +
+ + + +
+ ) +} + +Signature.defaultProps = defaultProps +Signature.displayName = 'NutSignature' diff --git a/src/packages/step/step.tsx b/src/packages/step/step.tsx index d2eaf30ab5b37687204164cbe4f77775f1b9a42b..69b495b5004796aada877051a1c466f1c95f508b 100644 --- a/src/packages/step/step.tsx +++ b/src/packages/step/step.tsx @@ -11,6 +11,8 @@ export interface StepProps { activeIndex: number icon: string size: string + className: string + style: React.CSSProperties renderContent: () => React.ReactNode } const defaultProps = { @@ -23,7 +25,17 @@ const defaultProps = { export const Step: FunctionComponent & React.HTMLAttributes> = ( props ) => { - const { children, title, content, activeIndex, icon, size, renderContent } = { + const { + children, + title, + content, + activeIndex, + icon, + size, + className, + renderContent, + ...restProps + } = { ...defaultProps, ...props, } @@ -41,10 +53,11 @@ export const Step: FunctionComponent & React.HTMLAttributes +
diff --git a/src/packages/steps/doc.md b/src/packages/steps/doc.md index 0478c10765efae3208b2e31cc0a68078dd714a85..767ac054ef670f6a3b49f33d2391903d328c9c8b 100644 --- a/src/packages/steps/doc.md +++ b/src/packages/steps/doc.md @@ -7,7 +7,7 @@ ### 安装 ```javascript -import { Steps } from '@nutui/nutui'; +import { Steps } from '@nutui/nutui-react'; ``` ## 代码演示 diff --git a/src/packages/steps/steps.tsx b/src/packages/steps/steps.tsx index c548a8d7eece58433b6349a17139266432de0386..3a3f2c5dfe4c376c6cc495fcf7970f3c2c67f50e 100644 --- a/src/packages/steps/steps.tsx +++ b/src/packages/steps/steps.tsx @@ -8,6 +8,8 @@ export interface StepsProps { current: number direction: string progressDot: boolean + className: string + style: React.CSSProperties } const defaultProps = { current: 0, @@ -18,7 +20,7 @@ const defaultProps = { export const Steps: FunctionComponent & React.HTMLAttributes> = (props) => { const propSteps = { ...defaultProps, ...props } - const { children, direction } = propSteps + const { children, direction, className, ...restProps } = propSteps const parentSteps = { propSteps, @@ -30,6 +32,7 @@ export const Steps: FunctionComponent & React.HTMLAttributes [`${b('')}-${direction}`]: true, [`${b('')}-dot`]: !!props.progressDot, }, + className, b('') ) return ( @@ -38,6 +41,7 @@ export const Steps: FunctionComponent & React.HTMLAttributes 'div', { className: classes, + ...restProps, }, children )} diff --git a/src/packages/uploader/doc.md b/src/packages/uploader/doc.md index 59938e7109c5f998b34d4bb21990a1aeefbd5dc6..126beadfd87c8692f6744c9bff26868feb914966 100644 --- a/src/packages/uploader/doc.md +++ b/src/packages/uploader/doc.md @@ -7,7 +7,7 @@ ### 安装 ``` javascript -import { Uploader } from '@nutui/nutui'; +import { Uploader } from '@nutui/nutui-react'; ``` ## 代码示例 diff --git a/src/styles/variables.scss b/src/styles/variables.scss index e3d42c778ba18fd0d390cf38bf6140376016ab44..28a6723472769a881b0de2ba2f988372e3944045 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -258,6 +258,9 @@ $checkbox-label-disable-color: #999; $radio-label-color: #1d1e1e; $radio-label-disable-color: #999; +// signature +$signature-border-color: #dadada; + //fixednav $fixednav-bg-color: $white; $fixednav-font-color: $black;