diff --git a/README.md b/README.md index f6dda5924..c4090afd7 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ const App = () => { - [ ] Collapse - [ ] CountDown - [ ] Divider -- [ ] Empty +- [x] Empty - [ ] ImagePreview - [ ] Lazyload - [ ] List diff --git a/package.json b/package.json index b4e277f31..c03c92a7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vant-react", - "version": "0.3.0", + "version": "1.0.0", "description": "Lightweight Mobile UI Components built in React & Typescript, inspired by Vant: https://youzan.github.io/vant", "author": "mxdi9i7", "license": "MIT", diff --git a/src/components/Empty/Network.tsx b/src/components/Empty/Network.tsx new file mode 100644 index 000000000..aa9e225f4 --- /dev/null +++ b/src/components/Empty/Network.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +const renderStop = (color: string, offset: number, opacity?: number) => ( + +); + +export const Network = ( + + + + {renderStop('#FFF', 0, 0.5)} + {renderStop('#F2F3F5', 100)} + + + {renderStop('#F2F3F5', 0, 0.3)} + {renderStop('#F2F3F5', 100)} + + + {renderStop('#EBEDF0', 0)} + {renderStop('#DCDEE0', 100, 0)} + + + {renderStop('#EAEDF0', 0)} + {renderStop('#DCDEE0', 100)} + + + {renderStop('#EAEDF0', 0)} + {renderStop('#DCDEE0', 100)} + + + {renderStop('#EAEDF0', 0)} + {renderStop('#DCDEE0', 100)} + + + {renderStop('#EAEDF0', 0)} + {renderStop('#DCDEE0', 100)} + + + {renderStop('#EBEDF0', 0)} + {renderStop('#FFF', 100, 0)} + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/src/components/Empty/index.scss b/src/components/Empty/index.scss new file mode 100644 index 000000000..5c9f12ef4 --- /dev/null +++ b/src/components/Empty/index.scss @@ -0,0 +1,35 @@ +@import '../../styles/variables.scss'; +@import '../../styles/typography.scss'; + +$baseClass: 'vant-empty'; + +.#{$baseClass} { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-sizing: border-box; + padding: $empty-padding; + + &__image { + width: $empty-image-size; + height: $empty-image-size; + + img { + width: 100%; + height: 100%; + } + } + + &__description { + margin-top: $empty-description-margin-top; + padding: $empty-description-padding; + color: $empty-description-color; + font-size: $empty-description-font-size; + line-height: $empty-description-line-height; + } + + &__bottom { + margin-top: $empty-bottom-margin-top; + } +} diff --git a/src/components/Empty/index.stories.tsx b/src/components/Empty/index.stories.tsx new file mode 100644 index 000000000..f07e2c611 --- /dev/null +++ b/src/components/Empty/index.stories.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import Empty from '.'; +import Button from '../Button'; +import Image from '../Image'; + +export default { + title: 'Empty', + component: Empty +}; + +export const BasicUsage = () => ( +
+ +
+); + +export const Description = () => ( +
+ +
+); + +export const ImageType = () => { + const [showType, setShowType] = useState('error'); + + return ( +
+
+
+ {showType === 'error' && ( + + )} + {showType === 'network' && ( + + )} + {showType === 'search' && ( + + )} + {showType === 'custom' && ( + } /> + )} +
+ ); +}; + +export const CustomImage = () => ( +
+ +
+); + +export const CustomBottomContent = () => ( +
+ + Button + + } + /> +
+); diff --git a/src/components/Empty/index.tsx b/src/components/Empty/index.tsx new file mode 100644 index 000000000..f0ce3086d --- /dev/null +++ b/src/components/Empty/index.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import classnames from '../../utils/classNames'; +import { Props } from './types'; +import './index.scss'; +import { Network } from './Network'; + +const baseClass = 'vant-empty'; +const PRESET_IMAGES = ['error', 'search', 'default']; + +// TODO: custom imageSize +// TODO: bottom & image & description slots +export default function Empty({ + description, + image = 'default', + bottom +}: Props) { + const containerProps = { + className: classnames(`${baseClass}`, []), + style: {} + }; + + const imageProps = { + className: classnames(`${baseClass}__image`, []), + style: {} + }; + + const bottomProps = { + className: classnames(`${baseClass}__bottom`, []), + style: {} + }; + + const descriptionProps = { + className: classnames(`${baseClass}__description`, []), + style: {} + }; + + const renderImage = () => { + if (image === 'network') { + return Network; + } + if (typeof image === 'string') { + if (PRESET_IMAGES.includes(image)) { + image = `https://img.yzcdn.cn/vant/empty-image-${image}.png`; + } + return ; + } else { + return image; + } + }; + + return ( +
+
{renderImage()}
+ {description &&

{description}

} + {bottom &&
{bottom}
} +
+ ); +} diff --git a/src/components/Empty/types.ts b/src/components/Empty/types.ts new file mode 100644 index 000000000..34e605cdd --- /dev/null +++ b/src/components/Empty/types.ts @@ -0,0 +1,8 @@ +import { ReactElement } from 'react'; + +export interface Props { + imageSize?: number | string; + description?: string | ReactElement; + image?: string | ReactElement; + bottom?: ReactElement; +} diff --git a/src/index.tsx b/src/index.tsx index 62b093c1a..0da5f3270 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,6 +12,7 @@ import Slider from './components/Slider'; import Checkbox from './components/Checkbox'; import Radio from './components/Radio'; import Stepper from './components/Stepper'; +import Empty from './components/Empty'; export { default as Button } from './components/Button'; export { default as Icon } from './components/Icons'; @@ -27,6 +28,7 @@ export { default as Slider } from './components/Slider'; export { default as Checkbox } from './components/Checkbox'; export { default as Radio } from './components/Radio'; export { default as Stepper } from './components/Stepper'; +export { default as Empty } from './components/Empty'; const Vant = { Button, @@ -42,7 +44,8 @@ const Vant = { Slider, Checkbox, Radio, - Stepper + Stepper, + Empty }; export default Vant; diff --git a/src/styles/stories.scss b/src/styles/stories.scss index 31378b7a4..30d7316ea 100644 --- a/src/styles/stories.scss +++ b/src/styles/stories.scss @@ -62,6 +62,10 @@ body { height: 200px; } } + + &.empty { + flex-direction: column; + } } .slider-container { display: flex; diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 89bf0f928..e7e201263 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -1,3 +1,5 @@ +@import './spacing.scss'; + // loaders $loader-size: 20px; $loader-animation-duration: 2s; @@ -16,3 +18,20 @@ $icon-dot-size: 8px; // popups $popup-alpha: 0.5; $popup-background-color: #000; + +// Empty +$padding-base: 4px; +$padding-md: $padding-base * 4; +$padding-xl: $padding-base * 8; +$gray-6: #969799; +$font-size-md: 14px; +$line-height-md: 20px; + +$empty-padding: $padding-xl 0; +$empty-image-size: 160px; +$empty-description-margin-top: $padding-md; +$empty-description-padding: 0 60px; +$empty-description-color: $gray-6; +$empty-description-font-size: $font-size-md; +$empty-description-line-height: $line-height-md; +$empty-bottom-margin-top: 24px;