未验证 提交 5e69e2bc 编写于 作者: _ __Oasis__ 提交者: GitHub

Merge branch 'nutui-react-dev' into nutui-react

......@@ -80,7 +80,7 @@ function init() {
type: 'input',
name: 'sort',
message:
'请选择组件分类(输入编号):1布局组件,2操作反馈,3基础组件,4导航组件,5数据录入,6业务组件',
'请选择组件分类(输入编号):1基础组件,2布局组件,3操作反馈,4导航组件,5数据录入,6特色组件',
validate(value) {
const pass = /^[1-6]$/.test(value)
if (pass) {
......
......@@ -81,8 +81,18 @@
},
"nav": [
{
"name": "布局组件",
"name": "基础组件",
"packages": [
{
"version": "1.0.0",
"name": "Avatar",
"type": "component",
"cName": "头像",
"desc": "用来代表用户或事物,支持图片、图标或字符展示。",
"sort": 2,
"show": true,
"author": "junjun"
},
{
"version": "1.0.0",
"name": "Button",
......@@ -95,19 +105,63 @@
},
{
"version": "1.0.0",
"name": "Collapse",
"name": "Cell",
"type": "component",
"cName": "折叠面板",
"desc": "折叠面板,可进行内容折叠展开操作,可以多面板配合使用。",
"cName": "列表组件",
"desc": "列表项,可组成列表",
"sort": 2,
"show": true,
"author": "zhenyulei"
"author": "songsong"
},
{
"version": "1.0.0",
"name": "Icon",
"type": "component",
"cName": "图标组件",
"desc": "图标",
"sort": 1,
"show": true,
"author": "oasis-cloud"
},
{
"version": "1.0.0",
"name": "Price",
"type": "component",
"cName": "价格",
"desc": "用来对商品价格数值的小数点前后部分应用不同样式,还支持人民币符号、千位分隔符、设置小数点位数等功能。",
"sort": 4,
"show": true,
"author": "songsong"
},
{
"version": "1.0.0",
"name": "Overlay",
"type": "component",
"cName": "遮罩层",
"desc": "创建一个遮罩层,通常用于阻止用户进行其他操作",
"sort": 5,
"show": true,
"author": "junjun"
}
]
},
{
"name": "布局组件",
"packages": []
},
{
"name": "操作反馈",
"packages": [
{
"version": "1.0.0",
"name": "Collapse",
"type": "component",
"cName": "折叠面板",
"desc": "折叠面板,可进行内容折叠展开操作,可以多面板配合使用。",
"sort": 2,
"show": true,
"author": "zhenyulei"
},
{
"version": "1.0.0",
"name": "Toast",
......@@ -135,13 +189,13 @@
"packages": [
{
"version": "1.0.0",
"name": "Icon",
"name": "Steps",
"type": "component",
"cName": "图标组件",
"desc": "图标",
"sort": 1,
"cName": "步骤条",
"desc": "拆分展示某项流程的步骤,引导用户按流程完成任务或向用户展示当前状态。",
"sort": 7,
"show": true,
"author": "oasis-cloud"
"author": "swag~jun"
}
]
},
......@@ -152,6 +206,16 @@
{
"name": "数据录入",
"packages": [
{
"version": "1.0.0",
"name": "Uploader",
"type": "component",
"cName": "上传",
"desc": "用于将本地的图片或文件上传至服务器。",
"sort": 1,
"show": true,
"author": "swag~jun"
},
{
"version": "1.0.0",
"name": "Input",
......@@ -171,11 +235,21 @@
"sort": 2,
"show": true,
"author": "VickyYe"
},
{
"version": "1.0.0",
"name": "CheckBox",
"type": "component",
"cName": "复选按钮",
"desc": "多选按钮用于选择。",
"sort": 4,
"show": true,
"author": "oasis"
}
]
},
{
"name": "业务组件",
"name": "特色组件",
"packages": []
}
]
......
.nut-avatar {
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center center;
display: inline-block;
position: relative;
margin-right: 24px;
flex: 0 0 auto; // 防止被压缩
.icon {
background-size: 100% 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.nut-icon__img {
width: 100%;
height: 100%;
}
.text {
display: inline-block;
width: 100%;
height: 100%;
text-align: center;
overflow: hidden;
}
}
.avatar-large {
width: $avatar-large-width;
height: $avatar-large-height;
line-height: $avatar-large-height;
}
.avatar-small {
width: $avatar-small-width;
height: $avatar-small-height;
line-height: $avatar-small-height;
}
.avatar-normal {
width: $avatar-normal-width;
height: $avatar-normal-height;
line-height: $avatar-normal-height;
}
.avatar-round {
border-radius: 50%;
}
.avatar-square {
border-radius: $avatar-square;
}
import React, { FunctionComponent, MouseEventHandler } from 'react'
import Icon from '@/packages/icon'
import classNames from 'classnames'
import './avatar.scss'
export interface AvatarProps {
size: string
icon: string
shape: string
bgColor: string
prefixCls: string
src: string
}
const defaultProps: AvatarProps = {
size: 'normal',
icon: '',
shape: 'round',
bgColor: '#eee',
prefixCls: 'nut-avatar',
src: '',
}
export const Avatar: FunctionComponent<
Partial<AvatarProps> & React.HTMLAttributes<HTMLDivElement>
> = (props) => {
const { children, prefixCls, size, shape, bgColor, src, icon, className, ...rest } = {
...defaultProps,
...props,
}
const sizeValue = ['large', 'normal', 'small']
const classes = classNames({
[`${prefixCls}`]: true,
['avatar-' + size]: true,
['avatar-' + shape]: true,
})
const cls = classNames(classes, className)
const styles: React.CSSProperties = {
width: sizeValue.indexOf(size) > -1 ? '' : `${size}px`,
height: sizeValue.indexOf(size) > -1 ? '' : `${size}px`,
backgroundImage: src ? `url(${src})` : '',
backgroundColor: `${bgColor}`,
}
const iconStyles = !!icon && !src ? icon : ''
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
if (props.onClick) {
props.onClick(e)
}
}
return (
<div className={cls} {...rest} style={styles} onClick={handleClick}>
<Icon className="icon" name={iconStyles} />
{children && <span className="text">{children}</span>}
</div>
)
}
Avatar.defaultProps = defaultProps
Avatar.displayName = 'NutAvatar'
.demo-avatar {
color: #fff;
}
import React from 'react'
import { Avatar } from './avatar'
import Cell from '@/packages/cell'
import './demo.scss'
const AvatarDemo = () => {
const AvatarStyle = {
alignItems: 'flex-end',
}
const handleClick = () => {
console.log('触发点击头像')
}
return (
<>
<div className="demo">
<h2>默认用法 (内置&quot;small&quot;,&quot;normal&quot;,&quot;large&quot;三种尺寸规格)</h2>
<Cell style={AvatarStyle}>
<Avatar
size="large"
src="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
></Avatar>
<Avatar
size="normal"
src="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
></Avatar>
<Avatar
size="small"
src="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
></Avatar>
</Cell>
<h2>修改形状</h2>
<Cell>
<Avatar icon="my" shape="square"></Avatar>
<Avatar icon="my" shape="round"></Avatar>
</Cell>
<h2>修改背景色</h2>
<Cell>
<Avatar className="demo-avatar" bgColor="#FA2C19" icon="my"></Avatar>
</Cell>
<h2>修改背景图片</h2>
<Cell>
<Avatar icon="https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png"></Avatar>
</Cell>
<h2>可以修改头像的内容</h2>
<Cell>
<Avatar icon="">N</Avatar>
</Cell>
<h2>点击头像触发事件</h2>
<Cell>
<Avatar icon="my" onClick={handleClick}></Avatar>
</Cell>
</div>
</>
)
}
export default AvatarDemo
# Avatar 头像
### 介绍
用来代表用户或事物,支持图片、图标或字符展示。
### 安装
``` javascript
import { Avatar } from '@nutui/nutui';
```
## 代码示例
### 基本用法
内置 smal / normal / large 三种尺寸规格
``` tsx
<Avatar size="large" src="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
></Avatar>
<Avatar size="normal" src="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
></Avatar>
<Avatar size="small" src="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
></Avatar>
```
### 修改形状类型
``` tsx
<Avatar shape="square"></Avatar>
<Avatar shape="round"></Avatar>
```
### 修改背景色
``` tsx
<Avatar bg-color="#f0250f"></Avatar>
```
### 修改背景icon
``` tsx
<Avatar icon="https://img30.360buyimg.com/uba/jfs/t1/84318/29/2102/10483/5d0704c1Eb767fa74/fc456b03fdd6cbab.png"></Avatar>
```
### 设置头像的文本内容
``` tsx
<Avatar icon>N</Avatar>
```
### Prop
| 字段 | 说明 | 类型 | 默认值 |
|----------|--------------------------------------------------------------------------|--------|--------|
| bgColor | 设置头像背景色 | String | #eee |
| size | 设置头像的大小,提供三种:large/normal/small,支持直接输入数字 | String | normal |
| shape | 设置头像的形状,默认是圆形,可以设置为square方形 | String | round |
| src | 设置头像的背景图片 | String | '' |
| icon | 设置头像的icon图标, 优先级低于src,类似Icon组件的name属性,支持名称和链接 | String | '' |
### Events
| 字段 | 说明 | 类型 | 回调参数 |
|----------|----------------------|----------|----------|
| onClick | 点击图片触发事件 | Function | event |
\ No newline at end of file
import { Avatar } from './avatar'
export default Avatar
.nut-button {
}
\ No newline at end of file
position: relative;
display: inline-block;
flex-shrink: 0;
height: $button-default-height;
box-sizing: border-box;
margin: 0;
padding: 0;
line-height: $button-default-line-height;
font-size: $button-default-font-size;
text-align: center;
cursor: pointer;
transition: opacity 0.2s;
-webkit-appearance: none;
user-select: none;
touch-action: manipulation;
.text {
margin-left: 5px;
}
&::before {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background-color: $black;
border: inherit;
border-color: $black;
border-radius: inherit;
transform: translate(-50%, -50%);
opacity: 0;
content: ' ';
}
&:active::before {
opacity: 0.1;
}
&__warp {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
&--loading,
&--disabled {
&::before {
display: none;
}
}
&--default {
color: $button-default-color;
background: $button-default-bg-color;
border: $button-border-width solid $button-default-border-color;
}
&--primary {
color: $button-primary-color;
background: $button-primary-background-color;
border: $button-border-width solid transparent;
}
&--info {
color: $button-info-color;
background: $button-info-background-color;
border: $button-border-width solid transparent;
}
&--success {
color: $button-success-color;
background: $button-success-background-color;
border: $button-border-width solid transparent;
}
&--danger {
color: $button-danger-color;
background: $button-danger-background-color;
border: $button-border-width solid transparent;
}
&--warning {
color: $button-warning-color;
background: $button-warning-background-color;
border: $button-border-width solid transparent;
}
&--plain {
background: $button-plain-background-color;
&.nut-button--primary {
color: $button-primary-border-color;
border-color: $button-primary-border-color;
}
&.nut-button--info {
color: $button-info-border-color;
border-color: $button-info-border-color;
}
&.nut-button--success {
color: $button-success-border-color;
border-color: $button-success-border-color;
}
&.nut-button--danger {
color: $button-danger-border-color;
border-color: $button-danger-border-color;
}
&.nut-button--warning {
color: $button-warning-border-color;
border-color: $button-warning-border-color;
}
}
&--large {
width: 100%;
height: $button-large-height;
line-height: $button-large-line-height;
}
&--normal {
padding: 0 18px;
font-size: $button-default-font-size;
}
&--small {
height: $button-small-height;
line-height: $button-small-line-height;
padding: 0 $padding-xs;
font-size: $font-size-1;
}
&--block {
display: block;
width: 100%;
}
&--disabled {
cursor: not-allowed;
opacity: $button-disabled-opacity;
}
&--loading {
cursor: default;
opacity: 0.9;
}
&--round {
border-radius: $button-border-radius;
}
&--square {
border-radius: 0;
}
}
import React, { FunctionComponent } from 'react'
import React, { CSSProperties, FunctionComponent, useEffect, useState } from 'react'
import './button.scss'
import Icon from '@/packages/icon'
export interface ButtonProps {
className: string
color: string
shape: string
plain: boolean
loading: boolean
disabled: boolean
style: object
type: string
size: string
block: boolean
icon: string
children: any
onClick: (e: MouseEvent) => void
}
export interface ButtonProps {}
const defaultProps = {} as ButtonProps
export type ButtonType = 'default' | 'primary' | 'info' | 'success' | 'warning' | 'danger'
export type ButtonSize = 'large' | 'normal' | 'small'
export type ButtonShape = 'square' | 'round'
const defaultProps = {
className: '',
color: '',
shape: 'round',
plain: false,
loading: false,
disabled: false,
type: 'default',
size: 'normal',
block: false,
icon: '',
style: {},
children: undefined,
onClick: (e: MouseEvent) => {},
} as ButtonProps
export const Button: FunctionComponent<Partial<ButtonProps>> = (props) => {
const { children } = { ...defaultProps, ...props }
return <div className="nut-button">Button</div>
const {
color,
shape,
plain,
loading,
disabled,
type,
size,
block,
icon,
children,
onClick,
className,
style,
...rest
} = {
...defaultProps,
...props,
}
const [btnName, setBtnName] = useState('')
const [btnStyle, setBtnStyle] = useState({})
useEffect(() => {
setBtnName(classes())
setBtnStyle(getStyle())
}, [
className,
color,
shape,
plain,
loading,
disabled,
style,
type,
size,
block,
icon,
children,
onClick,
])
const classes = () => {
const prefixCls = 'nut-button'
return `${prefixCls} ${type ? `${prefixCls}--${type}` : ''}
${size ? `${prefixCls}--${size}` : ''}
${shape ? `${prefixCls}--${shape}` : ''}
${plain ? `${prefixCls}--plain` : ''}
${block ? `${prefixCls}--block` : ''}
${disabled ? `${prefixCls}--disabled` : ''}
${loading ? `${prefixCls}--loading` : ''}`
}
const handleClick = (e: any) => {
if (!loading && !disabled && onClick) {
onClick(e)
}
}
const getStyle = () => {
const style: CSSProperties = {}
if (color) {
if (plain) {
style.color = color
style.background = '#fff'
if (!color?.includes('gradient')) {
style.borderColor = color
}
} else {
style.color = '#fff'
style.background = color
}
}
return style
}
return (
<div
className={`${btnName} ${className}`}
style={{ ...btnStyle, ...style }}
{...rest}
onClick={(e) => handleClick(e)}
>
<div className="nut-button__warp" style={getStyle()}>
{loading && <Icon name="loading"></Icon>}
{!loading && icon ? <Icon name={icon}></Icon> : ''}
{children}
</div>
</div>
)
}
Button.defaultProps = defaultProps
......
import React from 'react'
import React, { useState } from 'react'
import { Button } from './button'
const ButtonDemo = () => {
const [loading, setLoading] = useState(false)
return (
<>
<div className="demo">
<h2>基础用法</h2>
<Button></Button>
<h2>按钮类型</h2>
<Button className="aa" style={{ margin: 8 }} type="primary" shape="round">
主要按钮
</Button>
<Button type="info" style={{ margin: 8 }} shape="round">
信息按钮
</Button>
<Button shape="round" style={{ margin: 8 }}>
默认按钮
</Button>
<Button type="danger" style={{ margin: 8 }} shape="round">
危险按钮
</Button>
<Button type="warning" style={{ margin: 8 }}>
警告按钮
</Button>
<Button type="success" style={{ margin: 8 }}>
成功按钮
</Button>
<h2>朴素按钮</h2>
<Button plain={true} style={{ margin: 8 }} type="primary">
朴素按钮
</Button>
<Button plain={true} style={{ margin: 8 }} type="info">
朴素按钮
</Button>
<h2>禁用状态</h2>
<Button disabled style={{ margin: 8 }} type="primary">
禁用状态
</Button>
<Button plain={true} disabled style={{ margin: 8 }} type="info">
禁用状态
</Button>
<Button plain={true} disabled style={{ margin: 8 }} type="primary">
禁用状态
</Button>
<h2>加载状态</h2>
<Button loading type="info" style={{ margin: 8 }}></Button>
<Button loading type="warning" style={{ margin: 8 }}>
加载中
</Button>
<Button
loading={loading}
type="success"
onClick={() => {
setTimeout(() => {
setLoading(false)
}, 1500),
setLoading(!loading)
}}
style={{ margin: 8 }}
>
Click me!
</Button>
<h2>按钮尺寸</h2>
<Button shape="square" plain type="primary" icon="star-fill" style={{ margin: 8 }}></Button>
<Button shape="square" type="primary" icon="star" style={{ margin: 8 }}>
收藏
</Button>
<h2>按钮尺寸</h2>
<Button size="large" type="primary">
大号按钮
</Button>
<Button type="primary" style={{ margin: 8 }}>
普通按钮
</Button>
<Button size="small" style={{ margin: 8 }} type="primary">
小型按钮
</Button>
<h2>块级元素</h2>
<Button block type="primary">
块级元素
</Button>
<h2>自定义颜色</h2>
<Button color="#7232dd" style={{ margin: 8 }}>
单色按钮
</Button>
<Button color="#7232dd" plain={true} style={{ margin: 8 }}>
单色按钮
</Button>
<Button color="linear-gradient(to right, #ff6034, #ee0a24)" style={{ margin: 8 }}>
渐变按钮
</Button>
</div>
</>
)
......
.nut-calendar {
position: relative;
display: flex;
flex: 1;
height: 518px;
padding-top: 132px;
padding-bottom: 78px;
color: $calendar-base-color;
font-size: $calendar-base-font;
background-color: $white;
overflow: hidden;
&.nut-calendar-tile {
height: 100%;
padding-top: 46px;
padding-bottom: 0;
.nut-calendar-header {
.calendar-title {
font-size: $calendar-base-font;
}
}
}
&.nut-calendar-nofooter {
padding-bottom: 0;
}
// 头部导航
.nut-calendar-header {
position: absolute;
top: -1px;
left: 0;
right: 0;
display: flex;
flex-direction: column;
text-align: center;
padding-top: 1px;
background-color: $white;
z-index: 1;
.calendar-title {
padding-top: 22px;
font-size: $calendar-title-font;
line-height: 25px;
border-radius: 12px 12px 0 0;
}
.calendar-curr-month {
padding: 10px 0 7px;
line-height: 22px;
}
.calendar-weeks {
display: flex;
align-items: center;
justify-content: space-around;
height: 46px;
box-shadow: 0px 4px 10px 0px rgba($color: #000000, $alpha: 0.06);
.calendar-week-item {
&:first-of-type,
&:last-of-type {
color: $calendar-primary-color;
}
}
}
}
// 月份
.nut-calendar-content {
flex: 1;
.calendar-months-panel {
position: relative;
width: 100%;
height: auto;
display: block;
.calendar-month {
display: flex;
flex-direction: column;
text-align: center;
}
div:nth-of-type(2) {
.calendar-month-title {
padding-top: 0;
}
}
.calendar-loading-tip {
height: 50px;
line-height: 50px;
text-align: center;
position: absolute;
top: -50px;
left: 0;
right: 0;
font-size: $calendar-text-font;
color: $text-color;
}
.calendar-month-title {
height: 23px;
line-height: 23px;
margin: 8px 0;
}
.calendar-month-con {
overflow: hidden;
.calendar-month-item {
.calendar-month-day:nth-child(7n + 0),
.calendar-month-day:nth-child(7n + 1) {
color: $calendar-primary-color;
}
}
.calendar-month-day {
float: left;
width: 14.28%;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
position: relative;
.calendar-curr-tips,
.calendar-day-tip {
position: absolute;
top: 10px;
width: 100%;
font-size: 11px;
line-height: 12px;
color: $calendar-primary-color;
}
&-active {
background-color: $calendar-primary-color;
color: $white !important;
.calendar-curr-tips {
visibility: hidden;
}
.calendar-day-tip {
color: $white;
}
}
&-disabled {
color: $calendar-disable-color !important;
}
&-choose {
background-color: $calendar-choose-color;
}
.calendar-day {
padding: 4px 0;
font-size: $calendar-day-font;
}
}
}
}
}
// 底部导航
.nut-calendar-footer {
position: absolute;
left: 0;
right: 0;
bottom: -1px;
display: flex;
height: 78px;
width: 100%;
background-color: $white;
.calendar-confirm-btn {
height: 44px;
width: 100%;
margin: 14px 18px;
border-radius: 22px;
background: $button-primary-background-color;
color: $white;
text-align: center;
line-height: 44px;
}
}
}
import React, { FunctionComponent, ReactHTML, useState } from 'react'
import bem from '@/utils/bem'
import Utils from '../../utils/date'
import requestAniFrame from '../../utils/raf'
import './calendaritem.scss'
import { render } from 'react-dom'
type InputDate = string | string[]
interface CalendarItemProps {
type: string
isAutoBackFill: boolean
poppable: boolean
title: string
defaultValue: string | null
startDate: string
endDate: string
choose: (e: MouseEvent) => void
update: (e: MouseEvent) => void
close: (e: MouseEvent) => void
}
interface CalendarState {
weeks: string[]
yearMonthTitle: string
currDate: InputDate
unLoadPrev: boolean
touchParams: any
transformY: number
translateY: number
scrollDistance: number
defaultData: InputDate
chooseData: any
monthsData: any[]
dayPrefix: string
startData: InputDate
endData: InputDate
isRange: boolean
timer: number
}
interface Day {
day: string | number
type: string
}
interface MonthInfo {
curData: string[] | string
title: string
monthData: Day[]
}
function pxCheck(value: string | number): string {
return Number.isNaN(Number(value)) ? String(value) : `${value}px`
}
const defaultProps: CalendarItemProps = {
type: 'one',
isAutoBackFill: false,
poppable: true,
title: '日历选择',
defaultValue: null,
startDate: Utils.getDay(0),
endDate: Utils.getDay(365),
choose: () => {},
update: () => {},
close: () => {},
}
class CalendarItem extends React.Component<Partial<CalendarItemProps>> {
state: CalendarState
static displayName: string
months: React.RefObject<unknown>
monthsPanel: React.RefObject<unknown>
weeksPanel: React.RefObject<unknown>
constructor(props: CalendarItemProps) {
super(props)
this.months = React.createRef()
this.monthsPanel = React.createRef()
this.weeksPanel = React.createRef()
this.state = {
weeks: ['', '', '', '', '', '', ''],
yearMonthTitle: '',
currDate: '',
unLoadPrev: false,
touchParams: {
startY: 0,
endY: 0,
startTime: 0,
endTime: 0,
lastY: 0,
lastTime: 0,
},
transformY: 0,
translateY: 0,
scrollDistance: 0,
defaultData: [],
chooseData: [],
monthsData: [],
dayPrefix: 'calendar-month-day',
startData: '',
endData: '',
isRange: props.type === 'range',
timer: 0,
}
}
b = bem('calendar')
// choose = (e: MouseEvent) => {
// if (this.props?.choose) {
// this.props?.choose(e)
// }
// }
// // 日期转化成数组
// splitDate = (date: string) => {
// return date.split('-')
// }
// isStart = (currDate: string) => {
// return Utils.isEqual(this.state?.currDate[0], currDate)
// }
// isEnd = (currDate: string) => {
// return Utils.isEqual(this.state?.currDate[1], currDate)
// }
// //计算滚动方向和距离
// setMove = (move: number, type?: string, time?: number) => {
// let updateMove = move + this.state.transformY
// const h = this.months?.offsetHeight || 0
// const offsetHeight = this?.monthsPanel?.offsetHeight || 0
// if (type === 'end') {
// // 限定滚动距离
// if (updateMove > 0) {
// updateMove = 0
// }
// if (updateMove < 0 && updateMove < -offsetHeight + h) {
// updateMove = -offsetHeight + h
// }
// if (offsetHeight <= h && this.state.monthsData?.length == 1) {
// updateMove = 0
// }
// // setTransform(updateMove, type, time)
// } else {
// if (updateMove > 0 && updateMove > 100) {
// updateMove = 100
// }
// if (updateMove < -offsetHeight + h - 100 && this.state.monthsData?.length > 1) {
// updateMove = -offsetHeight + h - 100
// }
// if (updateMove < 0 && updateMove < -100 && this.state.monthsData?.length == 1) {
// updateMove = -100
// }
// // setTransform(updateMove)
// }
// }
// // 设置月份滚动距离和速度
// setTransform = (translateY = 0, type?: string, time = 1000) => {
// if (this.monthsPanel?.value) {
// if (type === 'end') {
// // this.monthsPanel?.value.style.webkitTransition = `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)`
// clearTimeout(this.state.timer)
// const timer = setTimeout(() => {
// // loadScroll()
// }, time)
// this.setState({ timer })
// } else {
// // this.monthsPanel?.value.style.webkitTransition = ''
// // loadScroll()
// }
// // this.monthsPanel?.value.style.webkitTransform = `translateY(${translateY}px)`
// this.setState({
// scrollDistance: translateY,
// })
// }
// }
// //监听月份
// // 监听月份滚动,改变月份标题
// loadScroll = () => {
// if (!this.props.poppable) {
// return false
// }
// requestAniFrame(() => {
// if (this.weeksPanel?.value && this.monthsPanel?.value) {
// const top = this.weeksPanel?.value.getBoundingClientRect().bottom
// const monthsDoms = this.monthsPanel?.value.querySelectorAll('.calendar-month')
// for (let i = 0; i < monthsDoms.length; i++) {
// if (
// monthsDoms[i].getBoundingClientRect().top <= top &&
// monthsDoms[i].getBoundingClientRect().bottom >= top
// ) {
// this.setState({ yearMonthTitle: this.state.monthsData[i].title })
// } else if (this.state.scrollDistance === 0) {
// this.setState({ yearMonthTitle: monthsData[0].title })
// }
// }
// }
// })
// }
// // 监听touch开始
// touchStart = (event: any) => {
// const changedTouches = event.changedTouches[0]
// let touchParams = Object.assign({}, this.state.touchParams, {
// startY: changedTouches.pageY,
// startTime: event.timeStamp || Date.now(),
// })
// // console.log('touchStart:', event, data, this.state.scrollDistance)
// this.setState({
// touchParams,
// transformY: this.state.scrollDistance,
// })
// }
// // 监听touchmove
// touchMove = (event: TouchEvent) => {
// event.preventDefault()
// const changedTouches = event.changedTouches[0]
// let touchParams = Object.assign({}, this.state.touchParams, {
// lastY: changedTouches.pageY,
// lastTime: event.timeStamp || Date.now(),
// })
// this.setState({ touchParams })
// const move = this.state.touchParams.lastY - this.state.touchParams.startY
// if (Math.abs(move) < 5) {
// return false
// }
// this.setMove(move)
// }
// // 获取当前数据
// getCurrDate = (day: Day, month: MonthInfo, isRange?: boolean) => {
// return isRange
// ? month.curData[3] + '-' + month.curData[4] + '-' + Utils.getNumTwoBit(+day.day)
// : month.curData[0] + '-' + month.curData[1] + '-' + Utils.getNumTwoBit(+day.day)
// }
// // 区间选择&&当前月&&选中态
// isActive = (day: Day, month: MonthInfo) => {
// return (
// this.state.isRange &&
// day.type == 'curr' &&
// this.getClass(day, month) == 'calendar-month-day-active'
// )
// }
// // 获取样式
// getClass = (day: Day, month: MonthInfo, isRange?: boolean) => {
// const currDate = getCurrDate(day, month, isRange)
// if (day.type == 'curr') {
// if (
// (!this.state.isRange && Utils.isEqual(this.state.currDate as string, currDate)) ||
// (this.state.isRange && (this.isStart(currDate) || this.isEnd(currDate)))
// ) {
// return `${state.dayPrefix}-active`
// } else if (
// (this.props.startDate && Utils.compareDate(currDate, this.props.startDate)) ||
// (this.props.endDate && Utils.compareDate(this.props.endDate, currDate))
// ) {
// return `${this.state.dayPrefix}-disabled`
// } else if (
// this.state.isRange &&
// Array.isArray(this.state.currDate) &&
// Object.values(this.state.currDate).length == 2 &&
// Utils.compareDate(this.state.currDate[0], currDate) &&
// Utils.compareDate(currDate, this.state.currDate[1])
// ) {
// return `${this.state.dayPrefix}-choose`
// } else {
// return null
// }
// } else {
// return `${this.state.dayPrefix}-disabled`
// }
// }
// // confirm = () => {
// // if ((this.state.isRange && this.state.chooseData.length == 2) || !state.isRange) {
// // this.choose(this.state.chooseData)
// // if (this.props.poppable) {
// // this.props.update()
// // }
// // }
// // }
// isStartTip = (day: Day, month: MonthInfo) => {
// if (this.isActive(day, month)) {
// return this.isStart(this.getCurrDate(day, month))
// } else {
// return false
// }
// }
// // 是否有结束提示
// isEndTip = (day: Day, month: MonthInfo) => {
// return this.isActive(day, month)
// }
// // 是否有是当前日期
// isCurrDay = (month: any, day: string) => {
// const date = `${month.curData[0]}-${month.curData[1]}-${day}`
// return Utils.isEqual(date, Utils.date2Str(new Date()))
// }
render() {
const { type, isAutoBackFill, poppable, title, defaultValue, startDate, endDate } = {
...defaultProps,
...this.props,
}
const {
weeks,
yearMonthTitle,
currDate,
unLoadPrev,
touchParams,
transformY,
translateY,
scrollDistance,
defaultData,
chooseData,
monthsData,
dayPrefix,
startData,
endData,
isRange,
timer,
} = this.state
return (
<div
className={`nut-calendar ${!poppable ? 'nut-calendar-tile' : ''} ${
isAutoBackFill ? 'nut-calendar-nofooter' : ''
}`}
>
<div className={`nut-calendar-header ${!poppable && 'nut-calendar-header-tile'}`}>
{poppable && (
<>
<div className="calendar-title">{title}</div>
<div className="calendar-curr-month">{yearMonthTitle}</div>
</>
)}
<div
className="calendar-weeks"
// ref={this.weeksPanel}
>
{weeks.map((item, index) => {
return (
<div className="calendar-week-item" key={index}>
{item}
</div>
)
})}
</div>
{/* <!-- content--> */}
<div
className="nut-calendar-content"
// ref={this.months}
// onTouchStart={(event) => this.touchStart(event)}
// onTouchMove={(event) => this.touchMove(event)}
// onTouchEnd={}
>
<div
className="calendar-months-panel"
// ref={this.monthsPanel}
>
<div className="calendar-loading-tip">
{!unLoadPrev ? '加载上一个月' : '没有更早月份'}
</div>
{monthsData.map((month, index) => {
return (
<div className="calendar-month" key={index}>
<div className="calendar-month-title">{month?.title}</div>
<div className="calendar-month-con">
<div
className={`calendar-month-item ${
type === 'range' ? 'month-item-range' : ''
}`}
>
{month?.monthData?.length > 0 &&
month.monthData.map(
(
day: { type: string; day: React.ReactNode },
i: string | number | null | undefined
) => {
return (
<div
// className={`calendar-month-day ${this.getClass(day, month)}`}
// onClick={() => this.chooseDay(day, month)}
key={i}
>
<div className={'calendar-day'}>
{day.type === 'curr' ? day.day : ''}
</div>
{/* {this.isCurrDay(month, day.day) && (
<div className={'calendar-curr-tips'}>今天</div>
)}
{this.isStartTip(day, month) ? (
<div className="calendar-day-tip">开始</div>
) : (
this.isEndTip(day, month) && (
<div className="calendar-day-tip">结束</div>
)
)} */}
</div>
)
}
)}
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
{/* foot */}
{poppable && !isAutoBackFill && (
<div className="nut-calendar-footer">
<div
className="calendar-confirm-btn"
// onClick={() => this.confirm()}
>
确定
</div>
</div>
)}
</div>
)
}
}
CalendarItem.displayName = 'NutIcon'
export { CalendarItem }
import React from 'react'
import { CalendarItem } from './calendaritem'
const Calendar = () => (
<>
<div className="demo">
<h2>基础用法</h2>
<CalendarItem />
</div>
</>
)
export default Calendar
import { CalendarItem } from './calendaritem'
export default CalendarItem
.nut-cell {
position: relative;
display: flex;
width: 100%;
line-height: 20px;
padding: 13px 16px;
background: $white;
border-radius: $cell-border-radius;
box-shadow: 0px 1px 7px 0px rgba(237, 238, 241, 1);
font-size: $cell-title-font;
color: $cell-color;
margin: 10px 0;
box-sizing: border-box;
&:last-child {
&::after {
border: 0;
}
}
&::after {
position: absolute;
box-sizing: border-box;
content: ' ';
pointer-events: none;
right: 16px;
bottom: 0;
left: 16px;
border-bottom: 2px solid #f5f6f7;
transform: scaleY(0.5);
}
&:active::before {
opacity: 0.1;
}
&--clickable {
cursor: pointer;
&::before {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background-color: $black;
border: inherit;
border-color: $black;
border-radius: inherit;
transform: translate(-50%, -50%);
opacity: 0;
content: ' ';
}
}
&__title {
display: flex;
flex-direction: column;
flex: 1;
&--icon {
flex-direction: row;
.icon {
margin-right: 10px;
}
}
}
&__subtitle {
font-size: $cell-title-desc-font;
}
&__desc {
display: inline-block;
text-align: right;
font-size: $cell-desc-font;
color: $cell-desc-color;
}
&__link {
color: #979797;
}
}
import React, { FunctionComponent, CSSProperties, ReactNode } from 'react'
import { useHistory } from 'react-router-dom'
import './cell.scss'
import bem from '@/utils/bem'
import { Icon } from '../icon/icon'
export interface CellProps {
title: string
subTitle: string
desc: string
descTextAlign: string
isLink: boolean
to: string
replace: boolean
url: string
icon: string
classPrefix: string
extra: ReactNode
click: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
}
const defaultProps = {
title: '',
subTitle: '',
desc: '',
descTextAlign: 'right',
isLink: false,
to: '',
replace: false,
url: '',
icon: '',
classPrefix: 'nutui-cell',
extra: '',
click: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {},
} as CellProps
export const Cell: FunctionComponent<Partial<CellProps> & React.HTMLAttributes<HTMLDivElement>> = (
props
) => {
const {
children,
click,
isLink,
to,
url,
replace,
classPrefix,
descTextAlign,
desc,
icon,
title,
subTitle,
extra,
...rest
} = {
...defaultProps,
...props,
}
const b = bem('cell')
let history = useHistory()
const handleClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
click(event)
if (to && history) {
history[replace ? 'replace' : 'push'](to)
} else if (url) {
replace ? location.replace(url) : (location.href = url)
}
}
const styles = {
textAlign: descTextAlign,
}
return (
<div
className={`${b({ clickable: isLink || to ? true : false }, [classPrefix])} `}
onClick={(event) => handleClick(event)}
{...rest}
>
{children ? (
children
) : (
<>
{title || subTitle || icon ? (
<>
{icon ? (
<div className={`${b('icon')}`}>
<Icon name={icon} />
</div>
) : null}
<div className={`${b('title', { icon: icon ? true : false })}`}>
{subTitle ? (
<>
<div className={b('maintitle')}>{title}</div>
<div className={b('subtitle')}>{subTitle}</div>
</>
) : (
<>{title}</>
)}
</div>
</>
) : null}
{desc ? (
<div className={b('desc')} style={styles as CSSProperties}>
{desc}
</div>
) : null}
</>
)}
{extra ? extra : null}
{!extra && (isLink || to) ? (
<div className={b('link')}>
<Icon name="right"></Icon>
</div>
) : null}
</div>
)
}
Cell.defaultProps = defaultProps
Cell.displayName = 'NutCell'
import React, { MouseEvent } from 'react'
import { Cell } from './cell'
import { CellGroup } from '../cellgroup/cellgroup'
const CellDemo = () => {
const testClick = (event: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
console.log('点击事件')
}
return (
<>
<div className="demo">
<h2>基础用法</h2>
<Cell title="我是标题" desc="描述文字"></Cell>
<Cell title="我是标题" subTitle="副标题描述" desc="描述文字"></Cell>
<Cell
title="点击测试"
click={(event: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>) =>
testClick(event)
}
></Cell>
<h2>直接使用插槽(slot)</h2>
<Cell title="我是标题" desc="描述文字">
<div>自定义内容</div>
</Cell>
<CellGroup title="链接 | 分组用法">
<Cell title="链接" isLink={true}></Cell>
<Cell title="URL 跳转" desc="https://jd.com" isLink={true} url="https://jd.com"></Cell>
<Cell title="路由跳转 ’/‘ " to="/"></Cell>
</CellGroup>
<CellGroup title="自定义右侧箭头区域">
<Cell title="Switch" extra={<div>这里是switch组件</div>}></Cell>
</CellGroup>
<h2>展示图标</h2>
<Cell title="姓名" icon="my" desc="张三" isLink={true}></Cell>
<h2>只展示 desc ,可通过 desc-text-align 调整内容位置</h2>
<Cell descTextAlign="left" desc="张三"></Cell>
</div>
</>
)
}
export default CellDemo
# Cell 组件
### 介绍
列表项,可组成列表。
### 安装
import { Cell,CellGroup } from '@nutui/nutui';
## 代码演示
### 基本用法
```tsx
const testClick = (event: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
console.log('点击事件')
}
<Cell title="我是标题" desc="描述文字"></Cell>
<Cell title="我是标题" subTitle="副标题描述" desc="描述文字"></Cell>
<Cell title="点击测试" click="{(event:" React.MouseEvent<HTMLDivElement, globalThis.MouseEvent
>) => testClick(event) } ></Cell
>
```
### 直接使用插槽
```tsx
<Cell title="我是标题" desc="描述文字">
<div>自定义内容</div>
</Cell>
```
### 链接 | 分组用法
```tsx
<CellGroup title="链接 | 分组用法">
<Cell title="链接" isLink={true}></Cell>
<Cell title="URL 跳转" desc="https://jd.com" isLink={true} url="https://jd.com"></Cell>
<Cell title="路由跳转 ’/‘ " to="/"></Cell>
</CellGroup>
```
### 自定义右侧箭头区域
```tsx
<CellGroup title="自定义右侧箭头区域">
<Cell title="Switch" extra={<div>这里是switch组件</div>}></Cell>
</CellGroup>
```
### 单元格展示图标
```tsx
<Cell title="姓名" icon="my" desc="张三" isLink={true}></Cell>
```
### 只展示 desc ,可通过 desc-text-align 调整内容位置
```tsx
<Cell descTextAlign="left" desc="张三"></Cell>
```
## API
### Prop
| 字段 | 说明 | 类型 | 默认值 |
| --------------- | -------------------------------------------------------------------------------------------- | --------------- | ------ |
| title | 标题名称 | String | - |
| sub-title | 左侧副标题 | String | - |
| desc | 右侧描述 | String | - |
| desc-text-align | 右侧描述文本对齐方式 [text-align](https://www.w3school.com.cn/cssref/pr_text_text-align.asp) | String | right |
| is-link | 是否展示右侧箭头并开启点击反馈 | Boolean | false |
| icon | 左侧 [图标名称](#/icon) 或图片链接 | String | - |
| url | 点击后跳转的链接地址 | String | - |
| to | 点击后跳转的目标路由对象 | String | - |
| replace | 是否在跳转时替换当前页面历史 | Boolean | false |
| extra | 其他 | React.ReactNode | - |
### Event
| 名称 | 说明 | 回调参数 |
| ----- | -------- | -------------------------------------------------------------- |
| click | 点击事件 | event: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent> |
import { Cell } from './cell'
export default Cell
.nut-cell-group {
display: block;
&__title {
display: inherit;
padding: $cell-group-title-padding;
color: $cell-group-title-color;
font-size: $cell-group-title-font-size;
line-height: $cell-group-title-line-height;
margin-top: 30px;
margin-bottom: 10px;
}
&__wrap {
display: inherit;
border-radius: $cell-border-radius;
overflow: hidden;
background-color: $cell-group-background-color;
margin: 10px 0;
.nut-cell {
margin: 0;
box-shadow: none;
border-radius: 0;
}
}
}
import React, { FunctionComponent } from 'react'
import './cellgroup.scss'
import bem from '@/utils/bem'
export interface CellGroupProps {
title: string
classPrefix: string
}
const defaultProps = { title: '', classPrefix: 'nutui-cell-group' } as CellGroupProps
export const CellGroup: FunctionComponent<Partial<CellGroupProps>> = (props) => {
const { children, classPrefix, title } = {
...defaultProps,
...props,
}
const b = bem('cell-group')
return (
<div className={b(null, [classPrefix])}>
{title ? <div className={b('title')}>{title}</div> : null}
<div className={b('wrap')}>{children}</div>
</div>
)
}
CellGroup.defaultProps = defaultProps
CellGroup.displayName = 'NutCellGroup'
import { CellGroup } from './cellgroup'
export default CellGroup
.nut-checkbox {
display: flex;
align-items: center;
&--reverse {
flex-direction: row-reverse;
.nut-checkbox__label {
margin-right: 15px;
margin-left: 0;
}
}
&__label {
margin-left: 15px;
font-size: 16px;
color: $checkbox-label-color;
&--disabled {
color: $checkbox-label-disable-color;
}
}
}
import React, { FunctionComponent, useEffect, useState } from 'react'
import Icon from '../icon'
import './checkbox.scss'
import bem from '@/utils/bem'
export interface CheckBoxProps {
checked: boolean
disabled: boolean
textPosition: 'left' | 'right'
iconSize: string | number
iconName: string
iconActiveName: string
label: string
onChange: (state: boolean, label: string) => void
}
const defaultProps = {
checked: false,
disabled: false,
textPosition: 'right',
iconSize: 18,
iconName: 'check-normal',
iconActiveName: 'checked',
onChange: (state, label) => {},
} as CheckBoxProps
export const CheckBox: FunctionComponent<
Partial<CheckBoxProps> & Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>
> = (props) => {
const { children } = { ...defaultProps, ...props }
const b = bem('checkbox')
const {
iconName,
iconSize,
label,
className,
textPosition,
iconActiveName,
checked,
disabled,
onChange,
...rest
} = props
const [innerChecked, setInnerChecked] = useState(checked)
const [innerDisabled, setDisabled] = useState(disabled)
useEffect(() => {
setInnerChecked(checked)
setDisabled(disabled)
}, [disabled, checked])
const renderIcon = () => {
return (
<Icon name={innerChecked ? iconActiveName : iconName} size={iconSize} color={color()}></Icon>
)
}
const color = () => {
return !innerDisabled ? (!innerChecked ? '#d6d6d6' : '#fa2c19') : '#f5f5f5'
}
const renderLabel = () => {
return (
<span className={`${b('label', { disabled: innerDisabled })} `}>{label || children}</span>
)
}
const handleClick = () => {
if (disabled) return
onChange && onChange(!innerChecked, label || (children as string))
return setInnerChecked(!innerChecked)
}
return (
<div
className={`${b({ reverse: textPosition === 'left' })} ${className || ''}`}
{...rest}
onClick={handleClick}
>
{renderIcon()}
{renderLabel()}
</div>
)
}
CheckBox.defaultProps = defaultProps
CheckBox.displayName = 'NutCheckBox'
import React, { useEffect, useRef, useState } from 'react'
import Toast from '../toast'
import { CheckBox } from './checkbox'
import { CheckBoxGroup } from '@/packages/checkboxgroup/checkboxgroup'
import Button from '@/packages/button'
const CheckBoxDemo = () => {
const [checked, setChecked] = useState(true)
const [checkboxgroup1, setCheckboxgroup1] = useState(['1'])
const [checkboxgroup2, setCheckboxgroup2] = useState(['1'])
const checkboxgroup2Ref = useRef(null)
return (
<>
<div className="demo">
<h2
onClick={() => {
console.log('click')
setChecked(false)
}}
>
基本用法-左右
</h2>
<div className={'nut-cell'}>
<CheckBox textPosition={'left'} label={'复选框'} checked={checked}></CheckBox>
</div>
<div className={'nut-cell'}>
<CheckBox textPosition={'right'} label={'复选框'} checked={false}></CheckBox>
</div>
<h2>禁用状态</h2>
<div className={'nut-cell'}>
<CheckBox
textPosition={'right'}
label={'未选时禁用状态'}
checked={false}
disabled={true}
></CheckBox>
</div>
<div className={'nut-cell'}>
<CheckBox
textPosition={'right'}
label={'选中时禁用状态'}
checked={true}
disabled={true}
></CheckBox>
</div>
<h2>自定义尺寸</h2>
<div className={'nut-cell'}>
<CheckBox label={'自定义尺寸25'} iconSize={25}></CheckBox>
</div>
<div className={'nut-cell'}>
<CheckBox label={'自定义尺寸10'} iconSize={10}></CheckBox>
</div>
<h2>自定义图标</h2>
<div className={'nut-cell'}>
<CheckBox checked={false} iconName="checklist" iconActiveName={'checklist'}>
自定义图标
</CheckBox>
</div>
<h2>点击触发change事件</h2>
<div className={'nut-cell'}>
<CheckBox
checked={false}
onChange={(state, label) => {
Toast.text(`您${state ? '选中' : '取消'}${label}`)
}}
>
change复选框
</CheckBox>
</div>
<h2>checkboxGroup使用</h2>
<div className="show-demo group1">
<CheckBoxGroup
checkedValue={checkboxgroup1}
onChange={(value) => {
console.log(value)
setCheckboxgroup1(value)
}}
>
<CheckBox checked={false} label="1">
组合复选框
</CheckBox>
<CheckBox checked={false} label="2">
组合复选框
</CheckBox>
<CheckBox checked={false} label="3">
组合复选框
</CheckBox>
<CheckBox checked={false} label="4">
组合复选框
</CheckBox>
</CheckBoxGroup>
<span>选中:{checkboxgroup1.toString()}</span>
</div>
<h2>checkboxGroup禁用</h2>
<CheckBoxGroup checkedValue={checkboxgroup1} disabled>
<CheckBox checked={false} label="1">
组合复选框
</CheckBox>
<CheckBox checked={false} label="2">
组合复选框
</CheckBox>
<CheckBox checked={false} label="3">
组合复选框
</CheckBox>
<CheckBox checked={false} label="4">
组合复选框
</CheckBox>
</CheckBoxGroup>
<h2>checkboxGroup 全选/取消</h2>
<CheckBoxGroup
style={{}}
ref={checkboxgroup2Ref}
checkedValue={checkboxgroup2}
onChange={(value) => {
Toast.text(`${value.length === 2 ? '全选' : '取消全选'}`)
}}
>
<CheckBox checked={false} label="1">
组合复选框
</CheckBox>
<CheckBox checked={false} label="2">
组合复选框
</CheckBox>
</CheckBoxGroup>
<div>
<Button
type="primary"
onClick={() => {
;(checkboxgroup2Ref.current as any).toggleAll(true)
}}
>
全选
</Button>
<Button
type="info"
onClick={() => {
;(checkboxgroup2Ref.current as any).toggleAll(false)
}}
>
取消
</Button>
</div>
</div>
</>
)
}
export default CheckBoxDemo
# CheckBox组件
### 介绍
基于 xxxxxxx
### 安装
## 代码演示
### 基础用法1
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
|--------------|----------------------------------|--------|------------------|
| name | 图标名称或图片链接 | String | - |
| color | 图标颜色 | String | - |
| size | 图标大小,如 '20px' '2em' '2rem' | String | - |
| class-prefix | 类名前缀,用于使用自定义图标 | String | 'nutui-iconfont' |
| tag | HTML 标签 | String | 'i' |
### Events
| 事件名 | 说明 | 回调参数 |
|--------|----------------|--------------|
| click | 点击图标时触发 | event: Event |
import { CheckBox } from './checkbox'
export default CheckBox
import React, { FunctionComponent, useEffect, useImperativeHandle, useState } from 'react'
import '../checkbox/checkbox.scss'
import bem from '@/utils/bem'
export interface CheckBoxGroupProps {
disabled: boolean
checkedValue: string[]
onChange: (value: string[]) => void
}
const defaultProps = {
disabled: false,
checkedValue: [],
onChange: (value: string[]) => {},
} as CheckBoxGroupProps
export const CheckBoxGroup = React.forwardRef(
(
props: Partial<CheckBoxGroupProps> & Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>,
ref
) => {
const { children } = { ...defaultProps, ...props }
const b = bem('checkboxgroup')
const { className, disabled, onChange, checkedValue, ...rest } = props
const [innerDisabled, setInnerDisabled] = useState(disabled)
const [innerValue, setInnerValue] = useState(checkedValue)
useImperativeHandle(ref, () => ({
toggleAll(state: boolean) {
console.log(state)
if (state === false) {
setInnerValue([])
} else {
const childrenLabel: string[] = []
React.Children.map(children, (child) => {
const childProps = (child as any).props
childrenLabel.push(childProps.label || (child as any).children)
})
setInnerValue(childrenLabel)
}
},
}))
useEffect(() => {
setInnerDisabled(disabled)
setInnerValue(checkedValue)
}, [disabled, checkedValue])
function handleChildChange(state: boolean, label: string) {
if (innerValue) {
let clippedValue = []
if (state) {
clippedValue = [...innerValue, label]
} else {
innerValue?.splice(innerValue?.indexOf(label), 1)
clippedValue = [...innerValue]
}
setInnerValue(clippedValue)
onChange && onChange(clippedValue)
}
}
function validateChildChecked(child: any) {
if (!innerValue) return false
return innerValue?.indexOf(child.props.label || child.children) > -1
}
function cloneChildren() {
return React.Children.map(children, (child: any, index) => {
const childChecked = validateChildChecked(child)
if ((child as any).type.displayName !== 'NutCheckBox') {
return React.cloneElement(child)
}
return React.cloneElement(child, {
disabled: innerDisabled,
checked: childChecked,
onChange: handleChildChange,
})
})
}
return (
<div className={`${b()} ${className || ''}`} {...rest}>
{cloneChildren()}
</div>
)
}
)
CheckBoxGroup.defaultProps = defaultProps
CheckBoxGroup.displayName = 'NutCheckBoxGroup'
......@@ -27,7 +27,7 @@ function pxCheck(value: string | number): string {
export const Icon: FunctionComponent<Partial<IconProps> & React.HTMLAttributes<HTMLDivElement>> = (
props
) => {
const { name, size, classPrefix, color, tag, children, className, style, ...rest } = {
const { name, size, classPrefix, color, tag, children, className, style, click, ...rest } = {
...defaultProps,
...props,
}
......@@ -48,8 +48,8 @@ export const Icon: FunctionComponent<Partial<IconProps> & React.HTMLAttributes<H
type,
{
className: isImage
? `${className} ${b('img')}`
: `${className} nut-icon-${name} ${b(null, [classPrefix])}`,
? `${className || ''} ${b('img')}`
: `${className || ''} ${b(null, [classPrefix])} nut-icon-${name} `,
style: {
color,
fontSize: pxCheck(size),
......
.wrapper {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
.content {
display: flex;
width: 150px;
height: 150px;
background: #fff;
border-radius: 8px;
align-items: center;
justify-content: center;
color: red;
}
}
import React, { useState } from 'react'
import { Overlay } from './overlay'
import Cell from '@/packages/cell'
import Button from '@/packages/button'
import './demo.scss'
const OverlayDemo = () => {
const [visible, setVisible] = useState(false)
const [visible2, setVisible2] = useState(false)
const handleToggleShow = () => {
setVisible(true)
}
const handleToggleShow2 = () => {
setVisible2(true)
}
const onClose = () => {
setVisible(false)
}
const onClose2 = () => {
setVisible2(false)
}
return (
<>
<div className="demo">
<h2>基础用法</h2>
<Cell>
<Button type="primary" onClick={handleToggleShow}>
显示遮罩层
</Button>
<Overlay visible={visible} onClick={onClose}></Overlay>
</Cell>
<h2>嵌套内容</h2>
<Cell>
<Button type="success" onClick={handleToggleShow2}>
嵌套内容
</Button>
<Overlay visible={visible2} onClick={onClose2}>
<div className="wrapper">
<div className="content">这里是正文</div>
</div>
</Overlay>
</Cell>
</div>
</>
)
}
export default OverlayDemo
# Overlay 组件
### 介绍
创建一个遮罩层,通常用于阻止用户进行其他操作
### 安装
``` javascript
import { OverLay } from '@nutui/nutui';
```
## 代码演示
### 基础用法
```tsx
<Overlay visible={true} zindex={2000}></Overlay>
```
### 嵌套内容
```tsx
<nut-overlay visible={true} zIndex={2000}>
<div className="wrapper">
<div className="content">这里是正文</div>
</div>
</nut-overlay>
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| ---------------------- | ---------------- | -------------- | ------ |
| visible | 当前组件是否显示 | Boolean | `false` |
| zIndex | 遮罩层级 | Number | 2000 |
| duration | 动画时长,单位秒 | Number | 0.3 |
| overlayClass | 自定义遮罩类名 | String | - |
| overlayStyle | 自定义遮罩样式 | CSSProperties | - |
| closeOnClickOverlay | 是否点击遮罩关闭 | Boolean | `true` |
### Events
| 事件名 | 说明 | 回调参数 |
| ------ | ---------- | ------------ |
| onClick | 点击时触发 | event: Event |
import { Overlay } from './overlay'
export default Overlay
.nut-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: $overlay-bg-color;
}
.overlay-fade-enter-active {
animation: nut-fade-in;
}
.overlay-fade-leave-active {
animation: nut-fade-out;
}
.first-render {
display: none;
}
.hidden-render {
display: none;
}
@keyframes nut-fade-in {
0% {
opacity: 0;
}
1% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes nut-fade-out {
0% {
opacity: 1;
}
1% {
opacity: 1;
}
100% {
opacity: 0;
}
}
import React, { FunctionComponent, MouseEventHandler, useEffect, useRef, useState } from 'react'
import bem from '@/utils/bem'
import classNames from 'classnames'
import './overlay.scss'
export interface OverlayProps {
zIndex: number
duration: number
overlayClass: string
overlayStyle: React.CSSProperties
closeOnClickOverlay: boolean
visible: boolean
}
const defaultProps = {
zIndex: 2000,
duration: 0.3,
overlayClass: '',
closeOnClickOverlay: true,
visible: false,
} as OverlayProps
export const Overlay: FunctionComponent<
Partial<OverlayProps> & React.HTMLAttributes<HTMLDivElement>
> = (props) => {
const [show, setShow] = useState(false)
const renderRef = useRef(true)
const intervalRef = useRef(0)
const { children, zIndex, duration, overlayClass, closeOnClickOverlay, visible, ...rest } = {
...defaultProps,
...props,
}
useEffect(() => {
setShow(false)
}, [visible])
useEffect(() => {
return () => {
clearTimeout(intervalRef.current)
}
}, [])
const b = bem('overlay')
const classes = classNames(
{
[overlayClass]: true,
'overlay-fade-leave-active': !renderRef.current && !visible,
'overlay-fade-enter-active': visible,
'first-render': renderRef.current && !visible,
'hidden-render': show,
},
b('')
)
const styles = {
zIndex: zIndex,
animationDuration: `${props.duration}s`,
...props.overlayStyle,
}
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
renderRef.current = false
let id = setTimeout(() => {
setShow(true)
}, duration * 1000 * 0.8)
intervalRef.current = id
if (props.onClick) {
props.onClick(e)
}
}
return (
<React.Fragment>
<div className={classes} style={styles} {...rest} onClick={handleClick}>
{children}
</div>
</React.Fragment>
)
}
Overlay.defaultProps = defaultProps
Overlay.displayName = 'NutOverlay'
import React, { useState, useEffect } from 'react'
import { Price } from './price'
import { Cell } from '../cell/cell'
const PriceDemo = () => {
const [price, setPrice] = useState(Math.random() * 10000000)
useEffect(() => {
const timer = setInterval(() => {
setPrice(Math.random() * 10000000)
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
return (
<div className="demo">
<h2>基本用法</h2>
<Cell>
<Price price={0} need-symbol={false} thousands={true} />
</Cell>
<h2>有人民币符号,无千位分隔</h2>
<Cell>
<Price price={10010.01} need-symbol={true} thousands={false} />
</Cell>
<h2>带人民币符号,有千位分隔,保留小数点后三位</h2>
<Cell>
<Price price={15213.1221} decimal-digits={3} need-symbol={true} thousands={true} />
</Cell>
<h2>异步随机变更</h2>
<Cell>
<Price price={price} decimal-digits={3} need-symbol={true} thousands={true} />
</Cell>
</div>
)
}
export default PriceDemo
# Price 组件
### 介绍
基于 xxxxxxx
### 安装
## 代码演示
### 基本用法
```tsx
<Price price={0} need-symbol={false} thousands={true} />
```
### 有人民币符号,无千位分隔
```tsx
<Price price={10010.01} need-symbol={true} thousands={false} />
```
### 带人民币符号,有千位分隔,保留小数点后三位
```tsx
<Price price={15213.1221} decimal-digits={3} need-symbol={true} thousands={true} />
```
### 异步随机变更
```tsx
const [price, setPrice] = useState(Math.random() * 10000000)
useEffect(() => {
const timer = setInterval(() => {
setPrice(Math.random() * 10000000)
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
<Price price={price} decimal-digits={3} need-symbol={true} thousands={true} />
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| -------------- | ------------------------ | ------- | ------ |
| price | 价格数量 | Number | 0 |
| need-symbol | 是否需要加上 symbol 符号 | Boolean | true |
| symbol | 符号类型 | String | &yen; |
| decimal-digits | 小数位位数 | Number | 2 |
| thousands | 是否按照千分号形式显示 | Boolean | false |
import { Price } from './price'
export default Price
.nut-price {
font-size: $font-size-3;
display: inline;
color: $primary-color;
&__symbol {
display: inline-block;
font-size: $font-size-3;
margin-right: 4px;
}
&__big {
display: inline-block;
font-size: $price-big-size;
}
&__point {
display: inline-block;
font-size: $price-big-size;
}
&__small {
display: inline-block;
font-size: $font-size-4;
}
}
import React, { FunctionComponent } from 'react'
import './price.scss'
import bem from '@/utils/bem'
export interface PriceProps {
price: number | string
needSymbol: boolean
symbol: string
decimalDigits: number
thousands: boolean
}
const defaultProps = {
price: 0,
needSymbol: true,
symbol: '&yen;',
decimalDigits: 2,
thousands: false,
} as PriceProps
export const Price: FunctionComponent<Partial<PriceProps>> = (props) => {
const { price, needSymbol, symbol, decimalDigits, thousands } = { ...defaultProps, ...props }
const b = bem('price')
const showSymbol = () => {
return { __html: (props.needSymbol ? props.symbol : '') || '' }
}
const checkPoint = (price: string | number) => {
return String(price).indexOf('.') > 0
}
const formatThousands = (num: any) => {
if (Number(num) == 0) {
num = 0
}
if (checkPoint(num)) {
num = Number(num).toFixed(decimalDigits)
num = typeof num.split('.') === 'string' ? num.split('.') : num.split('.')[0]
} else {
num = num.toString()
}
if (props.thousands) {
return (num || 0).toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
} else {
return num
}
}
const formatDecimal = (decimalNum: any) => {
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()
}
const result = '0.' + decimalNum
const resultFixed = Number(result).toFixed(decimalDigits)
return String(resultFixed).substring(2, resultFixed.length)
}
return (
<div className="nut-price">
{needSymbol ? (
<div className={`${b('symbol')}`} dangerouslySetInnerHTML={showSymbol()}></div>
) : null}
<div className={`${b('big')}`}>{formatThousands(price)}</div>
<div className={`${b('point')}`}>.</div>
<div className={`${b('small')}`}>{formatDecimal(price)}</div>
</div>
)
}
Price.defaultProps = defaultProps
Price.displayName = 'NutPrice'
import { Step } from './step'
export default Step
.nut-step {
flex-grow: 0;
flex-shrink: 0;
flex: 1;
text-align: center;
font-size: 0;
&-head {
position: relative;
display: flex;
justify-content: center;
margin-bottom: 12px;
}
&-line {
position: absolute;
top: 11px;
left: 50%;
right: -50%;
display: inline-block;
height: 1px;
background: #909ca4;
}
&-icon {
position: relative;
display: flex;
width: 25px;
height: 25px;
line-height: 25px;
font-size: 13px;
align-items: center;
justify-content: center;
z-index: 1;
.nut-icon {
width: 100%;
height: 100%;
}
&.is-text {
border-radius: 50%;
border-width: 1px;
border-style: solid;
}
&.is-icon {
border-radius: 50%;
border-width: 1px;
border-style: solid;
background-color: transparent;
}
}
&-main {
display: inline-block;
padding-left: 10%;
padding-right: 10%;
text-align: center;
}
&-title {
display: block;
margin-bottom: 10px;
font-size: 14px;
color: #909ca4;
}
&-content {
display: block;
font-size: 14px;
color: #666;
}
&:last-child {
.nut-step-line {
display: none;
}
}
&.nut-step-finish {
.nut-step-head {
color: $primary-color;
border-color: $primary-color;
}
.nut-step-icon.is-text {
background-color: $white;
}
.nut-step-icon.is-icon {
background-color: $white;
}
.nut-step-line {
background: $primary-color;
}
.nut-step-title {
color: $primary-color;
}
}
&.nut-step-process {
.nut-step-head {
color: $white;
border-color: $primary-color;
}
.nut-step-icon.is-text {
background-color: $primary-color;
}
.nut-step-icon.is-icon {
background-color: $primary-color;
}
.nut-step-title {
color: $primary-color;
}
}
&.nut-step-wait {
.nut-step-head {
color: #909ca4;
border-color: #909ca4;
}
.nut-step-icon.is-text {
background-color: $white;
}
.nut-step-icon.is-icon {
background-color: $step-wait-bg-color;
.nut-icon {
color: $white;
}
}
.nut-step-content {
color: #909ca4;
}
}
}
.nut-steps-vertical {
.nut-step {
display: flex;
height: 33.34%;
}
.nut-step-line {
position: absolute;
display: inline-block;
width: 1px;
height: 100%;
background: #909ca4;
}
.nut-step-main {
display: inline-block;
padding-left: 6%;
text-align: left;
}
&.nut-steps-dot {
.nut-step-head {
margin-top: 7px;
margin-bottom: 0;
}
.nut-step-line {
top: 7px;
left: 50%;
right: -50%;
}
.nut-step-icon {
width: 8px;
height: 8px;
background: $primary-color;
border-radius: 50%;
box-sizing: content-box;
}
.nut-step-wait {
.nut-step-icon {
background-color: #959fb1;
}
.nut-step-content {
color: #909ca4;
}
}
.nut-step-finish {
.nut-step-icon {
background-color: $primary-color;
}
}
.nut-step-process {
.nut-step-icon {
position: relative;
background-color: $primary-color;
&:before {
content: '';
display: inline-block;
position: absolute;
left: 50%;
top: 50%;
margin-left: -7px;
margin-top: -7px;
width: 14px;
height: 14px;
background-color: $primary-color-end;
border-radius: 50%;
opacity: 0.23;
}
}
}
}
}
import React, { FunctionComponent, useContext } from 'react'
import { DataContext } from '@/packages/steps/UserContext'
import bem from '@/utils/bem'
import classNames from 'classnames'
import Icon from '@/packages/icon'
import './step.scss'
export interface StepProps {
title: string
content: string
activeIndex: number
icon: string
size: string
renderContent: () => React.ReactNode
}
const defaultProps = {
title: '',
content: '',
activeIndex: 0,
icon: '',
size: '12px',
} as StepProps
export const Step: FunctionComponent<Partial<StepProps> & React.HTMLAttributes<HTMLDivElement>> = (
props
) => {
const { children, title, content, activeIndex, icon, size, renderContent } = {
...defaultProps,
...props,
}
const parent: any = useContext(DataContext)
const dot = parent.propSteps.progressDot
const getCurrentStatus = () => {
const index = activeIndex
if (index < +parent.propSteps.current) return 'finish'
return index === +parent.propSteps.current ? 'process' : 'wait'
}
const b = bem('step')
const classes = classNames(
{
[`${b('')}-${getCurrentStatus()}`]: true,
},
b('')
)
return (
<div className={classes}>
<div className="nut-step-head">
<div className="nut-step-line"></div>
<div className={`nut-step-icon ${!dot ? (icon ? 'is-icon' : 'is-text') : ''}`}>
{icon ? (
<Icon className="nut-step-icon-inner" name={icon} size={size} />
) : dot ? (
<span></span>
) : (
<span className="nut-step-inner">{activeIndex}</span>
)}
</div>
</div>
<div className="nut-step-main">
<span className="nut-step-title">{title}</span>
{content && <span className="nut-step-content">{content}</span>}
{renderContent && <span className="nut-step-content">{renderContent()}</span>}
</div>
</div>
)
}
Step.defaultProps = defaultProps
Step.displayName = 'NutStep'
import { createContext } from 'react'
export const DataContext = createContext({})
.padding {
padding-left: 0 !important;
padding-right: 0 !important;
h2 {
padding-left: 27px !important;
}
}
.steps-wrapper {
width: 100%;
padding: 15px 0;
background-color: #fff;
.steps-button {
text-align: center;
}
}
import React, { useState } from 'react'
import { Steps } from './steps'
import Button from '@/packages/button'
import Step from '@/packages/step'
import './demo.scss'
const StepsDemo = () => {
const [stepState, setStepState] = useState<any>({
current1: 1,
current2: 1,
current3: 1,
current4: 1,
current5: 1,
})
const handleStep = (params: string) => {
if (stepState[params] >= 3) {
stepState[params] = 1
setStepState({ ...stepState })
} else {
stepState[params] += 1
setStepState({ ...stepState })
}
}
return (
<>
<div className="demo padding">
<h2>基本用法</h2>
<div className="steps-wrapper">
<Steps current={stepState.current1}>
<Step activeIndex={1} title="步骤一">
1
</Step>
<Step activeIndex={2} title="步骤二">
2
</Step>
<Step activeIndex={3} title="步骤三">
3
</Step>
</Steps>
<div className="steps-button">
<Button type="danger" onClick={() => handleStep('current1')}>
下一步
</Button>
</div>
</div>
<h2>标题和描述信息</h2>
<div className="steps-wrapper">
<Steps current={stepState.current2}>
<Step activeIndex={1} title="步骤一" content="步骤描述">
1
</Step>
<Step activeIndex={2} title="步骤二" content="步骤描述"></Step>
<Step activeIndex={3} title="步骤三" content="步骤描述"></Step>
</Steps>
<div className="steps-button" style={{ marginTop: '10px' }}>
<Button type="danger" onClick={() => handleStep('current2')}>
下一步
</Button>
</div>
</div>
<h2>自定义图标</h2>
<div className="steps-wrapper">
<Steps current={1}>
<Step activeIndex={1} title="已完成" icon="service">
1
</Step>
<Step activeIndex={2} title="进行中" icon="people">
2
</Step>
<Step activeIndex={3} title="未开始" icon="location2">
3
</Step>
</Steps>
</div>
<h2>竖向步骤条</h2>
<div className="steps-wrapper" style={{ height: '300px', padding: '15px 30px' }}>
<Steps direction="vertical" current={2}>
<Step activeIndex={1} title="已完成" content="您的订单已经打包完成,商品已发出">
1
</Step>
<Step activeIndex={2} title="进行中" content="您的订单正在配送途中">
2
</Step>
<Step
activeIndex={3}
title="未开始"
content="收货地址为:北京市经济技术开发区科创十一街18号院京东大厦"
>
3
</Step>
</Steps>
</div>
<h2>竖向步骤条</h2>
<div className="steps-wrapper" style={{ height: '300px', padding: '15px 40px' }}>
<Steps direction="vertical" progressDot current={2}>
<Step activeIndex={1} title="已完成" content="您的订单已经打包完成,商品已发出">
1
</Step>
<Step activeIndex={2} title="进行中" content="您的订单正在配送途中">
2
</Step>
<Step
activeIndex={3}
title="未开始"
renderContent={() => (
<>
<p>收货地址为:</p>
<p>北京市经济技术开发区科创十一街18号院京东大厦</p>
</>
)}
>
3
</Step>
</Steps>
</div>
</div>
</>
)
}
export default StepsDemo
# Steps 步骤条
### 介绍
拆分展示某项流程的步骤,引导用户按流程完成任务或向用户展示当前状态。
### 安装
```javascript
import { Steps } from '@nutui/nutui';
```
## 代码演示
### 基本用法
```tsx
<Steps current={1}>
<Step activeIndex={1} title="步骤一">1</Step>
<Step activeIndex={2} title="步骤二">2</Step>
<Step activeIndex={3} title="步骤三">3</Step>
</Steps>
```
### 标题和描述信息
```tsx
<Steps current={2}>
<Step activeIndex={1} title="步骤一" content="步骤描述">1</Step>
<Step activeIndex={2} title="步骤二" content="步骤描述">2</Step>
<Step activeIndex={3} title="步骤三" content="步骤描述">3</Step>
</Steps>
```
### 自定义图标
```tsx
<Steps current={1}>
<Step activeIndex={1} title="已完成" icon="service">1</Step>
<Step activeIndex={2} title="进行中" icon="people">2</Step>
<Step activeIndex={3} title="未开始" icon="location2">3</Step>
</Steps>
```
### 竖向步骤条
```tsx
<Steps direction="vertical" current={2}>
<Step activeIndex={1} title="已完成" content="您的订单已经打包完成,商品已发出">1</Step>
<Step activeIndex={2} title="进行中" content="您的订单正在配送途中">2</Step>
<Step activeIndex={3} title="未开始" content="收货地址为:北京市经济技术开发区科创十一街18号院京东大厦">3</Step>
</Steps>
```
### 点状步骤和垂直方向
```tsx
<Steps direction="vertical" progressDot current={2}>
<Step activeIndex={1} title="已完成" content="您的订单已经打包完成,商品已发出">1</Step>
<Step activeIndex={2} title="进行中" content="您的订单正在配送途中">2</Step>
<Step activeIndex={3} title="未开始" renderContent={() => (
<>
<p>收货地址为:</p>
<p>北京市经济技术开发区科创十一街18号院京东大厦</p>
</>
)}>3</Step>
</Steps>
```
## API
### Props
#### Steps
| 参数 | 说明 | 类型 | 默认值 |
| ---------------------- | ----------------------------------------------------------- | -------------- | ----------- |
| direction | 显示方向,`horizontal`,`vertical` | String | 'horizontal' |
| current | 当前所在的步骤 | Number | 0 |
| progressDot | 点状步骤条 | Boolean | false |
#### nut-step
| 参数 | 说明 | 类型 | 默认值 |
| ---------------- | ---------------------- | ------------ | ----------- |
| title | 流程步骤的标题 | String | '' |
| content | 流程步骤的描述性文字 | String | '' |
| icon | 图标 | String | '' |
| size | 图标尺寸大小 | String | '' |
| activeIndex | 流程步骤的索引 | Number | 0 |
| renderContent | 流程步骤的描述性文字的html结构 | React.ReactNode | - |
\ No newline at end of file
import { Steps } from './steps'
export default Steps
.nut-steps {
display: flex;
}
.nut-steps-vertical {
height: 100%;
flex-flow: column;
}
import React, { useState, FunctionComponent } from 'react'
import { DataContext } from './UserContext'
import bem from '@/utils/bem'
import classNames from 'classnames'
import './steps.scss'
export interface StepsProps {
current: number
direction: string
progressDot: boolean
}
const defaultProps = {
current: 0,
direction: 'horizontal',
progressDot: false,
} as StepsProps
export const Steps: FunctionComponent<Partial<StepsProps> & React.HTMLAttributes<HTMLDivElement>> =
(props) => {
const propSteps = { ...defaultProps, ...props }
const { children, direction } = propSteps
const parentSteps = {
propSteps,
}
const b = bem('steps')
const classes = classNames(
{
[`${b('')}-${direction}`]: true,
[`${b('')}-dot`]: !!props.progressDot,
},
b('')
)
return (
<DataContext.Provider value={parentSteps}>
{React.createElement(
'div',
{
className: classes,
},
children
)}
</DataContext.Provider>
)
}
Steps.defaultProps = defaultProps
Steps.displayName = 'NutSteps'
.demo.bg-w {
background: #fff;
}
import React from 'react'
import { Uploader, FileItem } from './uploader'
import Button from '@/packages/button'
const UploaderDemo = () => {
const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts'
const formData = {
custom: 'test',
}
const fileToDataURL = (file: Blob): Promise<any> => {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onloadend = (e) => resolve((e.target as FileReader).result)
reader.readAsDataURL(file)
})
}
const dataURLToImage = (dataURL: string): Promise<HTMLImageElement> => {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve(img)
img.src = dataURL
})
}
const canvastoFile = (
canvas: HTMLCanvasElement,
type: string,
quality: number
): Promise<Blob | null> => {
return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), type, quality))
}
const onOversize = (files: File[]) => {
console.log('oversize 触发 文件大小不能超过 50kb', files)
}
const onStart = () => {
console.log('start 触发')
}
const onDelete = (file: FileItem, fileList: FileItem[]) => {
console.log('delete 事件触发', file, fileList)
}
const beforeUpload = async (files: File[]) => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d') as CanvasRenderingContext2D
const base64 = await fileToDataURL(files[0])
const img = await dataURLToImage(base64)
canvas.width = img.width
canvas.height = img.height
context.clearRect(0, 0, img.width, img.height)
context.drawImage(img, 0, 0, img.width, img.height)
let blob = (await canvastoFile(canvas, 'image/jpeg', 0.5)) as Blob //quality:0.5可根据实际情况计算
const f = await new File([blob], files[0].name, { type: files[0].type })
return [f]
}
return (
<>
<div className="demo bg-w">
<h2>基础用法</h2>
<Uploader url={uploadUrl} start={onStart}></Uploader>
<h2>自定义上传样式</h2>
<Uploader url={uploadUrl}>
<Button type="primary" icon="uploader">
上传文件
</Button>
</Uploader>
<h2>直接调起摄像头(移动端生效)</h2>
<Uploader capture></Uploader>
<h2>上传状态</h2>
<Uploader url={uploadUrl} multiple removeImage={onDelete}></Uploader>
<h2>限制上传数量5个</h2>
<Uploader url={uploadUrl} multiple maximum="5"></Uploader>
<h2>限制上传大小(每个文件最大不超过 50kb)</h2>
<Uploader url={uploadUrl} multiple maximize={1024 * 50} oversize={onOversize}></Uploader>
<h2>限制上传大小(在beforeupload钩子中处理)</h2>
<Uploader
url={uploadUrl}
multiple
beforeUpload={beforeUpload}
maximize={1024 * 50}
oversize={onOversize}
></Uploader>
<h2>自定义数据 FormData 、 headers </h2>
<Uploader
url={uploadUrl}
data={formData}
headers={formData}
withCredentials={true}
></Uploader>
<h2>禁用状态</h2>
<Uploader disabled></Uploader>
</div>
</>
)
}
export default UploaderDemo
# Uploader 上传
### 介绍
用于将本地的图片或文件上传至服务器。
### 安装
``` javascript
import { Uploader } from '@nutui/nutui';
```
## 代码示例
### 基本用法
``` tsx
<Uploader url="http://服务器地址"></Uploader>
```
### 自定义上传样式
``` tsx
<Uploader url="http://服务器地址">
<Button type="primary" icon="uploader">上传文件</Button>
</Uploader>
```
### 直接调起摄像头(移动端生效)
``` tsx
<Uploader url="http://服务器地址" capture></Uploader>
```
### 限制上传数量5个
``` tsx
<Uploader url="http://服务器地址" multiple maximum="5"></Uploader>
```
### 限制上传大小(每个文件最大不超过 50kb,也可以在beforeupload中自行处理)
``` tsx
<Uploader url="http://服务器地址" multiple maximize={1024 * 50} beforeUpload={beforeUpload} oversize={onOversize}></Uploader>
```
``` javascript
const formData = {
custom: 'test'
};
const onOversize = (files: File[]) => {
console.log('oversize 触发 文件大小不能超过 50kb', files);
};
const beforeUpload = (files: File[]) => {
//自定义处理
return files;
}
```
### 自定义 FormData headers
``` tsx
<Uploader url="http://服务器地址" data={formData} headers={formData} withCredentials={true}></Uploader>
```
``` javascript
const formData = {
custom: 'test'
};
const onOversize = (files: File[]) => {
console.log('oversize 触发 文件大小不能超过 50kb', files);
};
const beforeUpload = (files: File[]) => {
//自定义处理
return files;
}
```
### 禁用状态
``` tsx
<Uploader disabled></Uploader>
```
### Prop
| 字段 | 说明 | 类型 | 默认值 |
|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|------------------|
| name | `input` 标签 `name` 的名称,发到后台的文件参数名 | String | "file" |
| url | 上传服务器的接口地址 | String | - |
| isPreview | 是否上传成功后展示预览图 | Boolean | true |
| isDeletable | 是否展示删除按钮 | Boolean | true |
| method | 上传请求的 http method | String | "post" |
| capture | 图片[选取模式](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#htmlattrdefcapture),直接调起摄像头 | String | false |
| maximize | 可以设定最大上传文件的大小(字节) | Number丨String | Number.MAX_VALUE |
| maximum | 文件上传数量限制 | Number丨String | 1 |
| clearInput | 是否需要清空`input`内容,设为`true`支持重复选择上传同一个文件 | Boolean | false |
| accept | 允许上传的文件类型,[详细说明](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file#%E9%99%90%E5%88%B6%E5%85%81%E8%AE%B8%E7%9A%84%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B) | String | * |
| headers | 设置上传的请求头部 | Object | {} |
| data | 附加上传的信息 formData | Object | {} |
| uploadIcon | 上传区域[图标名称](#/zh-CN/icon)或图片链接 | String | "photograph" |
| xhrState | 接口响应的成功状态(status)值 | Number | 200 |
| withCredentials | 支持发送 cookie 凭证信息 | Boolean | fasle |
| multiple | 是否支持文件多选 | Boolean | fasle |
| disabled | 是否禁用文件上传 | Boolean | fasle |
| timeout | 超时时间,单位为毫秒 | Number丨String | 1000 * 30 |
| beforeUpload | 上传前的函数需要返回一个`Promise`对象 | Function | null |
| beforeDelete | 除文件时的回调,返回值为 false 时不移除。支持返回一个 `Promise` 对象,`Promise` 对象 resolve(false) 或 reject 时不移除 | Function(file): boolean 丨Promise | - |
### FileItem
| 名称 | 说明 | 默认值 |
|----------|---------------------------------------------------------|---------------------------------|
| status | 文件状态值,可选'ready,uploading,success,error,removed' | "ready" |
| uid | 文件的唯一标识 | new Date().getTime().toString() |
| name | 文件名称 | "" |
| url | 文件路径 | "" |
| type | 文件类型 | "image/jpeg" |
| formData | 上传所需的data | new FormData() |
### Event
| 名称 | 说明 | 回调参数 |
|----------|------------------------|----------------------|
| start | 文件上传开始 | options |
| progress | 文件上传的进度 | event,options |
| oversize | 文件大小超过限制时触发 | files |
| success | 上传成功 | responseText,options |
| failure | 上传失败 | responseText,options |
| change | 上传文件改变时的状态 | fileList,event |
| removeImage | 文件删除之前的状态 | files,fileList |
import { Uploader } from './uploader'
export default Uploader
export class UploadOptions {
url = ''
formData?: FormData
method = 'post'
xhrState: string | number = 200
timeout: number = 30 * 1000
headers = {}
withCredentials = false
onStart?: Function
onProgress?: Function
onSuccess?: Function
onFailure?: Function
}
export class Upload {
options: UploadOptions
constructor(options: UploadOptions) {
this.options = options
}
upload() {
const options = this.options
const xhr = new XMLHttpRequest()
xhr.timeout = options.timeout
if (xhr.upload) {
xhr.upload.addEventListener(
'progress',
(e: ProgressEvent<XMLHttpRequestEventTarget>) => {
options.onProgress?.(e, options)
},
false
)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === options.xhrState) {
options.onSuccess?.(xhr.responseText, options)
} else {
options.onFailure?.(xhr.responseText, options)
}
}
}
xhr.withCredentials = options.withCredentials
xhr.open(options.method, options.url, true)
// headers
for (const [key, value] of Object.entries(options.headers)) {
xhr.setRequestHeader(key, value as string)
}
options.onStart?.(options)
xhr.send(options.formData)
} else {
console.warn('浏览器不支持 XMLHttpRequest')
}
}
}
.nut-uploader {
position: relative;
display: flex;
flex-wrap: wrap;
&__slot {
position: relative;
}
&__upload {
position: relative;
background: $uploader-background;
width: $uploader-width;
height: $uploader-height;
display: flex;
align-items: center;
justify-content: center;
}
&__input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
cursor: pointer;
opacity: 0;
&:disabled {
cursor: not-allowed;
}
}
&__preview {
width: $uploader-width;
height: $uploader-height;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
margin-bottom: 10px;
&-img {
position: relative;
width: 100%;
height: 100%;
.close {
position: absolute;
right: 0;
top: 0;
transform: translate(50%, -50%);
}
.tips {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 12px;
color: $white;
height: 30px;
line-height: 30px;
text-align: c;
background: rgba(0, 0, 0, 0.54);
}
&__c {
height: 100%;
width: 100%;
}
}
}
}
import React, { useState, FunctionComponent } from 'react'
import Icon from '@/packages/icon'
import { Upload, UploadOptions } from './upload'
import bem from '@/utils/bem'
import './uploader.scss'
export interface UploaderProps {
url: string
maximum: string | number
maximize: number
uploadIcon: string
name: string
accept: string
disabled: boolean
multiple: boolean
timeout: number
data: object
method: string
xhrState: number | string
headers: object
withCredentials: boolean
clearInput: boolean
isPreview: boolean
isDeletable: boolean
capture: boolean
start?: (option: UploadOptions) => void
removeImage?: (file: FileItem, fileList: FileItem[]) => void
success?: (param: { responseText: XMLHttpRequest['responseText']; option: UploadOptions }) => void
progress?: (param: { e: ProgressEvent<XMLHttpRequestEventTarget>; option: UploadOptions }) => void
failure?: (param: { responseText: XMLHttpRequest['responseText']; option: UploadOptions }) => void
update?: (fileList: any[]) => void
oversize?: (file: File[]) => void
change?: (param: { fileList: any[]; event: React.ChangeEvent<HTMLInputElement> }) => void
beforeUpload?: (file: File[]) => Promise<File[]>
beforeDelete?: (file: FileItem, files: FileItem[]) => boolean
}
export type FileItemStatus = 'ready' | 'uploading' | 'success' | 'error' | 'removed'
const defaultProps: UploaderProps = {
url: '',
maximum: 1,
uploadIcon: 'photograph',
name: 'file',
accept: '*',
disabled: false,
multiple: false,
maximize: Number.MAX_VALUE,
data: {},
headers: {},
method: 'post',
xhrState: 200,
timeout: 1000 * 30,
withCredentials: false,
clearInput: false,
isPreview: true,
isDeletable: true,
capture: false,
beforeDelete: (file: FileItem, files: FileItem[]) => {
return true
},
}
export class FileItem {
status: FileItemStatus = 'ready'
uid: string = new Date().getTime().toString()
name?: string
url?: string
type?: string
formData: FormData = new FormData()
}
export const Uploader: FunctionComponent<
Partial<UploaderProps> & React.HTMLAttributes<HTMLDivElement>
> = (props) => {
const {
children,
uploadIcon,
name,
accept,
disabled,
multiple,
url,
headers,
timeout,
method,
xhrState,
withCredentials,
data,
isPreview,
isDeletable,
maximum,
capture,
maximize,
start,
removeImage,
progress,
success,
update,
failure,
beforeDelete,
} = { ...defaultProps, ...props }
const [fileList, setFileList] = useState<any>([])
const b = bem('uploader')
const clearInput = (el: HTMLInputElement) => {
el.value = ''
}
const executeUpload = (fileItem: FileItem) => {
const uploadOption = new UploadOptions()
uploadOption.url = url
for (const [key, value] of Object.entries(data)) {
fileItem.formData.append(key, value)
}
uploadOption.formData = fileItem.formData
uploadOption.timeout = timeout * 1
uploadOption.method = method
uploadOption.xhrState = xhrState
uploadOption.headers = headers
uploadOption.withCredentials = withCredentials
uploadOption.onStart = (option: UploadOptions) => {
setFileList((fileList: FileItem[]) => {
fileList.map((item) => {
if (item.uid === fileItem.uid) {
item.status = 'ready'
}
})
return [...fileList]
})
start && start(option)
}
uploadOption.onProgress = (
e: ProgressEvent<XMLHttpRequestEventTarget>,
option: UploadOptions
) => {
setFileList((fileList: FileItem[]) => {
fileList.map((item) => {
if (item.uid === fileItem.uid) {
item.status = 'uploading'
}
})
return [...fileList]
})
progress && progress({ e, option })
}
uploadOption.onSuccess = (
responseText: XMLHttpRequest['responseText'],
option: UploadOptions
) => {
setFileList((fileList: FileItem[]) => {
update && update(fileList)
fileList.map((item) => {
if (item.uid === fileItem.uid) {
item.status = 'success'
}
})
return [...fileList]
})
success &&
success({
responseText,
option,
})
}
uploadOption.onFailure = (
responseText: XMLHttpRequest['responseText'],
option: UploadOptions
) => {
setFileList((fileList: FileItem[]) => {
fileList.map((item) => {
if (item.uid === fileItem.uid) {
item.status = 'error'
}
})
return [...fileList]
})
failure &&
failure({
responseText,
option,
})
}
new Upload(uploadOption).upload()
}
const readFile = (files: File[]) => {
files.forEach((file: File) => {
const formData = new FormData()
formData.append(name, file)
const fileItem = new FileItem()
fileItem.name = file.name
fileItem.status = 'uploading'
fileItem.type = file.type
fileItem.formData = formData
executeUpload(fileItem)
if (isPreview && file.type.includes('image')) {
const reader = new FileReader()
reader.onload = (event: ProgressEvent<FileReader>) => {
fileItem.url = (event.target as FileReader).result as string
fileList.push(fileItem)
setFileList([...fileList])
}
reader.readAsDataURL(file)
} else {
fileList.push(fileItem)
setFileList([...fileList])
}
})
}
const filterFiles = (files: File[]) => {
const maximum = (props.maximum as number) * 1
const oversizes = new Array<File>()
const filterFile = files.filter((file: File) => {
if (file.size > maximize) {
oversizes.push(file)
return false
} else {
return true
}
})
if (oversizes.length) {
props.oversize && props.oversize(files)
}
if (filterFile.length > maximum) {
filterFile.splice(maximum - 1, filterFile.length - maximum)
}
return filterFile
}
const onDelete = (file: FileItem, index: number) => {
if (beforeDelete && beforeDelete(file, fileList)) {
fileList.splice(index, 1)
removeImage && removeImage(file, fileList)
setFileList([...fileList])
} else {
console.log('用户阻止了删除!')
}
}
const fileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
return
}
const $el = event.target
let { files } = $el
if (props.beforeUpload) {
props.beforeUpload(new Array<File>().slice.call(files)).then((f: Array<File>) => {
const _files: File[] = filterFiles(new Array<File>().slice.call(f))
readFile(_files)
})
} else {
const _files = filterFiles(new Array<File>().slice.call(files))
readFile(_files)
}
props.change && props.change({ fileList, event })
if (props.clearInput) {
clearInput($el)
}
}
return (
<div className={`${b()}`}>
{children ? (
<div className="nut-uploader__slot">
{
<>
{children}
{maximum > fileList.length && (
<>
{capture ? (
<input
className="nut-uploader__input"
type="file"
capture="camera"
name={name}
accept={accept}
disabled={disabled}
multiple={multiple}
onChange={fileChange}
/>
) : (
<input
className="nut-uploader__input"
type="file"
name={name}
accept={accept}
disabled={disabled}
multiple={multiple}
onChange={fileChange}
/>
)}
</>
)}
</>
}
</div>
) : (
<>
{fileList.length !== 0 &&
fileList.map((item: any, index: number) => {
return (
<div className="nut-uploader__preview" key={item.uid}>
<div className="nut-uploader__preview-img">
{isDeletable && (
<Icon
color="rgba(0,0,0,0.6)"
className="close"
name="circle-close"
click={() => onDelete(item, index)}
/>
)}
{item.type.includes('image') && item.url && (
<img className="nut-uploader__preview-img__c" src={item.url} />
)}
{item.status !== 'success' && <div className="tips">{item.status}</div>}
</div>
</div>
)
})}
{maximum > fileList.length && (
<div className="nut-uploader__upload">
<Icon color="#808080" name={uploadIcon} />
{capture ? (
<input
className="nut-uploader__input"
type="file"
capture="camera"
name={name}
accept={accept}
disabled={disabled}
multiple={multiple}
onChange={fileChange}
/>
) : (
<input
className="nut-uploader__input"
type="file"
name={name}
accept={accept}
disabled={disabled}
multiple={multiple}
onChange={fileChange}
/>
)}
</div>
)}
</>
)}
</div>
)
}
Uploader.defaultProps = defaultProps
Uploader.displayName = 'NutUploader'
......@@ -33,7 +33,6 @@
&-components {
background: #f7f8fa;
border-radius: 30px 30px 0 0;
overflow: hidden;
padding: 30px 25px;
> ol {
margin-bottom: 17px;
......
......@@ -19,8 +19,7 @@ $black: #000;
// padding
$padding-xs: 12px;
$font-family: PingFang SC, Microsoft YaHei, Helvetica, Hiragino Sans GB, SimSun,
sans-serif !default;
$font-family: PingFang SC, Microsoft YaHei, Helvetica, Hiragino Sans GB, SimSun, sans-serif !default;
// ---- Animation ----
$animation-duration: 0.25s !default;
......@@ -100,7 +99,7 @@ $cell-title-font: $font-size-2;
$cell-title-desc-font: $font-size-1;
$cell-desc-font: $font-size-2;
$cell-desc-color: $disable-color;
$cell-border-radius: 7px;
$cell-border-radius: 6px;
// cell-group
......@@ -240,20 +239,13 @@ $infinite-bottom-color: #c8c8c8;
//range
$range-max: #333333;
$rang-bg-color: rgba($primary-color, 0.5);
$rang-bar-bg-color: linear-gradient(
135deg,
$primary-color 0%,
$primary-color-end 100%
);
$rang-bar-bg-color: linear-gradient(135deg, $primary-color 0%, $primary-color-end 100%);
//address
$address-region-tab-line: linear-gradient(
90deg,
$primary-color 0%,
$primary-color-end 100%
);
$address-region-tab-line: linear-gradient(90deg, $primary-color 0%, $primary-color-end 100%);
//steps
$step-wait-bg-color: #959fb1;
// dialog
$dialog-width: 296px;
......@@ -266,9 +258,11 @@ $checkbox-label-disable-color: #999;
$radio-label-color: #1d1e1e;
$radio-label-disable-color: #999;
view-block {
display: block;
}
//fixednav
$fixednav-bg-color: $white;
$fixednav-font-color: $black;
$fixednav-index: 201;
$fixednav-btn-bg: linear-gradient(135deg, rgba(250, 25, 44, 1) 0%, rgba(250, 63, 25, 1) 100%);
@import './mixins/index';
@import './animation/index';
......@@ -2,7 +2,7 @@ import { withNaming } from '@bem-react/classname'
const cn = withNaming({ n: 'nut-', e: '__', m: '--', v: '-' })
const b = cn('icon', 'Element')
// const b = cn('icon', 'Element')
//
// console.log(b()) // nut-icon__Element
// console.log(b('Element')) // nut-icon__Element
......
const Utils = {
/**
* 是否为闫年
* @return {Boolse} true|false
*/
isLeapYear: function (y: number): boolean {
return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
},
/**
* 返回星期数
* @return {String}
*/
getWhatDay: function (year: number, month: number, day: number): string {
const date = new Date(year + '/' + month + '/' + day)
const index = date.getDay()
const dayNames = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
return dayNames[index]
},
/**
* 返回星期数
* @return {Number}
*/
getMonthPreDay: function (year: number, month: number): number {
const date = new Date(year + '/' + month + '/01')
let day = date.getDay()
if (day == 0) {
day = 7
}
return day
},
/**
* 返回月份天数
* @return {Number}
*/
getMonthDays: function (year: string, month: string): number {
if (/^0/.test(month)) {
month = month.split('')[1]
}
return (
[
0,
31,
this.isLeapYear(Number(year)) ? 29 : 28,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
] as number[]
)[month as any]
},
/**
* 补齐数字位数
* @return {string}
*/
getNumTwoBit: function (n: number): string {
n = Number(n)
return (n > 9 ? '' : '0') + n
},
/**
* 日期对象转成字符串
* @return {string}
*/
date2Str: function (date: Date, split?: string): string {
split = split || '-'
const y = date.getFullYear()
const m = this.getNumTwoBit(date.getMonth() + 1)
const d = this.getNumTwoBit(date.getDate())
return [y, m, d].join(split)
},
/**
* 返回日期格式字符串
* @param {Number} 0返回今天的日期、1返回明天的日期,2返回后天得日期,依次类推
* @return {string} '2014-12-31'
*/
getDay: function (i: number): string {
i = i || 0
let date = new Date()
const diff = i * (1000 * 60 * 60 * 24)
date = new Date(date.getTime() + diff)
return this.date2Str(date)
},
/**
* 时间比较
* @return {Boolean}
*/
compareDate: function (date1: string, date2: string): boolean {
const startTime = new Date(date1.replace('-', '/').replace('-', '/'))
const endTime = new Date(date2.replace('-', '/').replace('-', '/'))
if (startTime >= endTime) {
return false
}
return true
},
/**
* 时间是否相等
* @return {Boolean}
*/
isEqual: function (date1: string, date2: string): boolean {
const startTime = new Date(date1).getTime()
const endTime = new Date(date2).getTime()
if (startTime == endTime) {
return true
}
return false
},
}
export default Utils
function requestAniFrame() {
if (typeof window !== 'undefined') {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60)
}
)
} else {
return function (callback: Function) {
setTimeout(callback, 1000 / 60)
}
}
}
export default requestAniFrame()
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册