├── components ├── hooks │ ├── index.ts │ └── useTouch.ts ├── icon │ ├── font │ │ ├── sty-icon.ttf │ │ └── sty-icon.woff │ └── index.tsx ├── select │ ├── index.less │ └── index.tsx ├── ripple │ ├── index.less │ └── index.tsx ├── overlay │ └── index.less ├── image │ ├── index.less │ └── index.tsx ├── cell-popup │ ├── index.less │ └── index.tsx ├── pull-refresh │ └── index.less ├── empty │ ├── index.less │ └── index.tsx ├── date-picker │ ├── generate │ │ ├── locale.ts │ │ └── index.ts │ ├── index.tsx │ ├── interface.ts │ ├── hooks │ │ └── useCellClassName.ts │ ├── panels │ │ ├── Header.tsx │ │ ├── MonthPanel │ │ │ └── index.tsx │ │ ├── YearPanel │ │ │ └── index.tsx │ │ ├── PanelBody.tsx │ │ ├── DecadePanel │ │ │ └── index.tsx │ │ ├── DatePanel │ │ │ └── index.tsx │ │ └── index.tsx │ ├── DatePicker.tsx │ └── index.less ├── nav-bar │ ├── index.less │ └── index.tsx ├── toast │ ├── index.less │ └── index.tsx ├── action-sheet │ ├── index.less │ └── index.tsx ├── index.ts ├── swipe │ └── index.less ├── switch │ ├── index.less │ └── index.tsx ├── button │ ├── index.less │ └── index.tsx ├── cell │ ├── index.less │ └── index.tsx ├── checkbox │ ├── index.less │ └── Group.tsx ├── loading │ ├── index.tsx │ └── index.less ├── style │ └── default.less ├── popup │ ├── index.tsx │ └── index.less ├── _utils │ └── index.ts ├── radio │ └── index.tsx ├── picker │ ├── index.less │ ├── index.tsx │ ├── PickerColumn.tsx │ └── PickerPanel.tsx ├── timeline │ ├── index.less │ └── index.tsx ├── dialog │ └── index.less └── tabs │ └── index.less ├── docs ├── static │ ├── css │ │ ├── 7.5a27f241.chunk.css │ │ ├── 6.79910c2e.chunk.css │ │ ├── 5.c084ae22.chunk.css │ │ ├── 3.52fc9f00.chunk.css │ │ ├── 2.b0c2f1ba.chunk.css │ │ ├── 4.2b5f8f75.chunk.css │ │ ├── 0.afdf8bf5.chunk.css │ │ └── 1.b506178b.chunk.css │ ├── img │ │ └── cat.58265f1.jpeg │ ├── font │ │ ├── sty-icon.c68110a.ttf │ │ └── sty-icon.b3dd9cb.woff │ └── js │ │ ├── 13.4963ed6a.chunk.js │ │ ├── 17.cd451688.chunk.js │ │ └── 18.b7e3bcf8.chunk.js └── index.html ├── site ├── page │ ├── Toast │ │ ├── index.less │ │ └── index.tsx │ ├── Image │ │ ├── img │ │ │ └── cat.jpeg │ │ ├── index.less │ │ └── index.tsx │ ├── Tabs │ │ ├── index.less │ │ └── index.tsx │ ├── Swipe │ │ ├── index.less │ │ └── index.tsx │ ├── Button │ │ ├── index.less │ │ └── index.tsx │ ├── Loading │ │ ├── index.less │ │ └── index.tsx │ ├── PullRefresh │ │ ├── index.less │ │ └── index.tsx │ ├── Ripple │ │ └── index.tsx │ ├── NavBar │ │ └── index.tsx │ ├── Icon │ │ ├── index.less │ │ └── index.tsx │ ├── Cell │ │ └── index.tsx │ ├── Select │ │ └── index.tsx │ ├── Switch │ │ └── index.tsx │ ├── Home │ │ ├── index.less │ │ ├── index.tsx │ │ └── config.ts │ ├── DatePicker │ │ └── index.tsx │ ├── Timeline │ │ └── index.tsx │ ├── Popup │ │ └── index.tsx │ ├── asyncComponent.tsx │ ├── Radio │ │ └── index.tsx │ ├── Checkbox │ │ └── index.tsx │ ├── ActionSheet │ │ └── index.tsx │ ├── Dialog │ │ └── index.tsx │ └── Picker │ │ └── index.tsx ├── index.tsx ├── index.html ├── app.tsx ├── index.css ├── app.css └── routes.tsx ├── config ├── webpack.prod.js ├── webpack.dev.js └── webpack.common.js ├── .editorconfig ├── .gitignore ├── README.md ├── tsconfig.json ├── .eslintrc.js └── package.json /components/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useTouch } from './useTouch'; 2 | -------------------------------------------------------------------------------- /docs/static/css/7.5a27f241.chunk.css: -------------------------------------------------------------------------------- 1 | .toast-demo .sty-button { 2 | margin-right: 16px; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /site/page/Toast/index.less: -------------------------------------------------------------------------------- 1 | .toast-demo{ 2 | .sty-button { 3 | margin-right: 16px; 4 | } 5 | } -------------------------------------------------------------------------------- /site/page/Image/img/cat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-9527/sty-ui/HEAD/site/page/Image/img/cat.jpeg -------------------------------------------------------------------------------- /docs/static/img/cat.58265f1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-9527/sty-ui/HEAD/docs/static/img/cat.58265f1.jpeg -------------------------------------------------------------------------------- /components/icon/font/sty-icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-9527/sty-ui/HEAD/components/icon/font/sty-icon.ttf -------------------------------------------------------------------------------- /components/icon/font/sty-icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-9527/sty-ui/HEAD/components/icon/font/sty-icon.woff -------------------------------------------------------------------------------- /docs/static/font/sty-icon.c68110a.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-9527/sty-ui/HEAD/docs/static/font/sty-icon.c68110a.ttf -------------------------------------------------------------------------------- /docs/static/font/sty-icon.b3dd9cb.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z-9527/sty-ui/HEAD/docs/static/font/sty-icon.b3dd9cb.woff -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production' 6 | }); 7 | -------------------------------------------------------------------------------- /site/page/Tabs/index.less: -------------------------------------------------------------------------------- 1 | .tabs-demo { 2 | padding-bottom: 50px; 3 | 4 | .sty-tabs-tabPane { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/static/css/6.79910c2e.chunk.css: -------------------------------------------------------------------------------- 1 | .tabs-demo { 2 | padding-bottom: 50px; 3 | } 4 | .tabs-demo .sty-tabs-tabPane { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /docs/static/css/5.c084ae22.chunk.css: -------------------------------------------------------------------------------- 1 | .swipe-demo .swipe { 2 | background-color: #fff; 3 | } 4 | .swipe-demo .swipe .swipe-item { 5 | text-align: center; 6 | line-height: 150px; 7 | color: #fff; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /site/page/Swipe/index.less: -------------------------------------------------------------------------------- 1 | .swipe-demo{ 2 | .swipe{ 3 | background-color: #fff; 4 | 5 | .swipe-item{ 6 | text-align: center; 7 | line-height: 150px; 8 | color: #fff; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /site/page/Button/index.less: -------------------------------------------------------------------------------- 1 | .button-demo { 2 | background-color: #fff; 3 | 4 | .block { 5 | padding: 16px; 6 | margin: 0 auto; 7 | } 8 | 9 | .sty-button { 10 | margin-bottom: 10px; 11 | 12 | &-inline { 13 | margin-right: 10px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/static/css/3.52fc9f00.chunk.css: -------------------------------------------------------------------------------- 1 | .loading-demo { 2 | height: calc(100vh - 47px); 3 | background-color: #fff; 4 | } 5 | .loading-demo .sty-loading { 6 | display: inline-block; 7 | margin: 5px 0 5px 20px; 8 | } 9 | .loading-demo .sty-loading-vertical { 10 | display: inline-flex; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /site/page/Loading/index.less: -------------------------------------------------------------------------------- 1 | .loading-demo { 2 | height: calc(100vh - 47px); 3 | background-color: #fff; 4 | } 5 | 6 | .loading-demo .sty-loading { 7 | display: inline-block; 8 | margin: 5px 0 5px 20px; 9 | } 10 | 11 | .loading-demo .sty-loading-vertical { 12 | display: inline-flex; 13 | } 14 | -------------------------------------------------------------------------------- /docs/static/css/2.b0c2f1ba.chunk.css: -------------------------------------------------------------------------------- 1 | .button-demo { 2 | background-color: #fff; 3 | } 4 | .button-demo .block { 5 | padding: 16px; 6 | margin: 0 auto; 7 | } 8 | .button-demo .sty-button { 9 | margin-bottom: 10px; 10 | } 11 | .button-demo .sty-button-inline { 12 | margin-right: 10px; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /site/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | import { HashRouter } from 'react-router-dom'; 5 | import './index.css'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('app') 12 | ); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | # 对所有文件生效 6 | [*] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | # 对后缀名为 md 的文件生效 15 | [*.md] 16 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 第一个模板 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 第一个模板
-------------------------------------------------------------------------------- /site/page/PullRefresh/index.less: -------------------------------------------------------------------------------- 1 | .refresh-demo { 2 | height: calc(100vh - 47px); 3 | background-color: #fff; 4 | 5 | .block{ 6 | padding: 16px 30px; 7 | line-height: 1.8em; 8 | font-size: 14px; 9 | } 10 | 11 | .dog { 12 | width: 140px; 13 | height: 72px; 14 | margin-top: 8px; 15 | border-radius: 4px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/static/css/4.2b5f8f75.chunk.css: -------------------------------------------------------------------------------- 1 | .refresh-demo { 2 | height: calc(100vh - 47px); 3 | background-color: #fff; 4 | } 5 | .refresh-demo .block { 6 | padding: 16px 30px; 7 | line-height: 1.8em; 8 | font-size: 14px; 9 | } 10 | .refresh-demo .dog { 11 | width: 140px; 12 | height: 72px; 13 | margin-top: 8px; 14 | border-radius: 4px; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /components/select/index.less: -------------------------------------------------------------------------------- 1 | 2 | .sty-select{ 3 | height: 280px; 4 | overflow: auto; 5 | 6 | } 7 | 8 | .sty-select-loading{ 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | width: 100%; 13 | height: 100%; 14 | background-color: rgba(255, 255, 255, 0.9); 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | z-index: 4; 19 | color: #1989fa; 20 | } -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | devtool: 'cheap-module-eval-source-map', 7 | devServer: { 8 | contentBase: './index.html', 9 | hot: true, 10 | port: 3000, 11 | inline: true, 12 | stats: 'errors-only' // 控制台只显示错误信息 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /components/ripple/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | .sty-ripple { 4 | position: relative; 5 | width: 100%; 6 | height: 100%; 7 | overflow: hidden; 8 | 9 | &-shadow { 10 | position: absolute; 11 | background: rgba(0, 0, 0, 0.08); 12 | min-height: 1rem; 13 | min-width: 1rem; 14 | border-radius: 50%; 15 | margin: -0.5rem; 16 | transition: transform 1s ease-in-out 0s, opacity 0.4s linear 0s; 17 | pointer-events: none; 18 | top: 0; 19 | left: 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /components/overlay/index.less: -------------------------------------------------------------------------------- 1 | .sty-overlay{ 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | z-index: 999; 8 | display: none; 9 | 10 | &.open { 11 | .sty-overlay-mask { 12 | opacity: 1; 13 | } 14 | } 15 | 16 | &-mask{ 17 | position: fixed; 18 | top: 0; 19 | left: 0; 20 | width: 100%; 21 | height: 100%; 22 | background-color: rgba(0, 0, 0, 0.7); 23 | transition: opacity 300ms; 24 | z-index: 999; 25 | will-change: opacity; 26 | opacity: 0; 27 | } 28 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于ts+hooks实现的移动端组件库 2 | 3 | ### 前言 4 | 5 | 自从公司项目使用了ts和hooks开发后,自己也在项目中封装过业务组件。但是自己还是想参考一下其它优秀框架如何封装代码的,于是自己利用业务时间开发了这个项目。主要的目的还是学习其它框架是如何组织代码、封装组件、代码规范 6 | 7 | 8 | 9 | 项目地址:https://github.com/z-9527/sty-ui 10 | 预览地址:https://z-9527.github.io/sty-ui/#/ 11 | 12 | 13 | 14 | 整个项目都是自己从零开始开发的,包括ts、eslint和其它webpack配置。部分代码和样式参考了[antd](https://github.com/ant-design/ant-design)、[vant](https://github.com/youzan/vant)、react-components 15 | 16 | **代码仅供学习和参考,请勿在生产环境使用** 17 | 18 | 19 | 20 | 对于表单组件暂时还没有封装,准备等后面有时间了参考[field-form](https://github.com/react-component/field-form)。 21 | -------------------------------------------------------------------------------- /components/image/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @imgPrefixCls: sty-img; 4 | 5 | .@{imgPrefixCls} { 6 | position: relative; 7 | display: inline-block; 8 | width: 100px; 9 | height: 100px; 10 | overflow: hidden; 11 | 12 | &>img { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | &-placeholder { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | width: 100%; 25 | height: 100%; 26 | background-color: #f7f8fa; 27 | color: #969799; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /site/page/Ripple/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Ripple } from '@/components/index'; 3 | 4 | function RippleDemo() { 5 | return ( 6 |
7 |
基础用法
8 | 9 | 10 |
颜色波纹
11 | 12 | 13 |
中心波纹
14 | 15 |
16 | ); 17 | } 18 | export default RippleDemo; 19 | -------------------------------------------------------------------------------- /components/cell-popup/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../style/default.less'; 3 | 4 | .sty-row { 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 44px; 9 | 10 | &-left { 11 | padding: 0 16px; 12 | font-size: 14px; 13 | color: #969799; 14 | } 15 | 16 | &-right { 17 | padding: 0 16px; 18 | font-size: 14px; 19 | color: #1989fa; 20 | } 21 | 22 | &-center { 23 | max-width: 50%; 24 | font-weight: 500; 25 | font-size: 16px; 26 | line-height: 20px; 27 | text-align: center; 28 | color: @base-text-color; 29 | .sty-ellipsis() 30 | } 31 | } -------------------------------------------------------------------------------- /components/pull-refresh/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @refreshPrefixCls: sty-refresh; 4 | 5 | .@{refreshPrefixCls} { 6 | position: relative; 7 | height: 100%; 8 | overflow: auto; 9 | 10 | &-header { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | -webkit-font-smoothing: antialiased; 15 | user-select: none; 16 | position: absolute; 17 | left: 0; 18 | width: 100%; 19 | height: 50px; 20 | overflow: hidden; 21 | color: #969799; 22 | font-size: 14px; 23 | text-align: center; 24 | transform: translateY(-100%); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /site/page/NavBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavBar, Icon } from '@/components/index'; 3 | 4 | function NavBarDemo() { 5 | return ( 6 |
7 |
基础用法
8 | 9 | 10 |
使用事件
11 | } 15 | leftArrow 16 | onClickLeft={() => console.log('click left')} 17 | onClickRight={() => console.log('click right')} 18 | /> 19 |
20 | ); 21 | } 22 | export default NavBarDemo; 23 | -------------------------------------------------------------------------------- /components/empty/index.less: -------------------------------------------------------------------------------- 1 | .sty-empty { 2 | margin: 8px; 3 | font-size: 14px; 4 | line-height: 1.5715; 5 | text-align: center; 6 | } 7 | 8 | .sty-empty-image { 9 | margin-bottom: 8px; 10 | transform: scale(.9); 11 | } 12 | 13 | .sty-empty-img-default-ellipse { 14 | fill-opacity: .8; 15 | fill: #f5f5f5 16 | } 17 | 18 | .sty-empty-img-default-path-1 { 19 | fill: #aeb8c2 20 | } 21 | 22 | .sty-empty-img-default-path-2 { 23 | fill: url(#linearGradient-1) 24 | } 25 | 26 | .sty-empty-img-default-path-3 { 27 | fill: #f5f5f7 28 | } 29 | 30 | .sty-empty-img-default-path-4, 31 | .sty-empty-img-default-path-5 { 32 | fill: #dce0e6 33 | } 34 | 35 | .sty-empty-img-default-g { 36 | fill: #fff 37 | } 38 | -------------------------------------------------------------------------------- /docs/static/css/0.afdf8bf5.chunk.css: -------------------------------------------------------------------------------- 1 | .icon-demo .sty-tabs-tabPane { 2 | background-color: #f7f8fa; 3 | } 4 | .icon-demo .icons-box { 5 | margin: 20px; 6 | background-color: #fff; 7 | border-radius: 12px; 8 | } 9 | .icon-demo .icons-box .icon-box { 10 | display: inline-block; 11 | float: none; 12 | text-align: center; 13 | vertical-align: middle; 14 | width: 25%; 15 | } 16 | .icon-demo .icons-box .icon-box .sty-icon { 17 | margin: 16px 0 16px; 18 | color: #323233; 19 | font-size: 32px; 20 | } 21 | .icon-demo .icons-box .icon-box .icon-name { 22 | display: block; 23 | height: 36px; 24 | margin: -4px 0 4px; 25 | padding: 0 5px; 26 | color: #646566; 27 | font-size: 12px; 28 | line-height: 18px; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /site/page/Icon/index.less: -------------------------------------------------------------------------------- 1 | .icon-demo { 2 | 3 | .sty-tabs-tabPane{ 4 | background-color: #f7f8fa; 5 | } 6 | 7 | .icons-box { 8 | margin: 20px; 9 | background-color: #fff; 10 | border-radius: 12px; 11 | 12 | .icon-box { 13 | display: inline-block; 14 | float: none; 15 | text-align: center; 16 | vertical-align: middle; 17 | width: 25%; 18 | 19 | .sty-icon { 20 | margin: 16px 0 16px; 21 | color: #323233; 22 | font-size: 32px; 23 | } 24 | 25 | .icon-name { 26 | display: block; 27 | height: 36px; 28 | margin: -4px 0 4px; 29 | padding: 0 5px; 30 | color: #646566; 31 | font-size: 12px; 32 | line-height: 18px; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /site/page/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon, Tabs } from '@/components/index'; 3 | import data from './data'; 4 | import './index.less'; 5 | 6 | function IconDemo() { 7 | return ( 8 |
9 | 10 | {data.map(item => ( 11 | 12 |
13 | {item.icons.map(i => ( 14 |
15 | 16 | {i} 17 |
18 | ))} 19 |
20 |
21 | ))} 22 |
23 |
24 | ); 25 | } 26 | export default IconDemo; 27 | -------------------------------------------------------------------------------- /components/date-picker/generate/locale.ts: -------------------------------------------------------------------------------- 1 | const locale = { 2 | locale: 'zh_CN', 3 | today: '今天', 4 | now: '此刻', 5 | backToToday: '返回今天', 6 | ok: '确定', 7 | timeSelect: '选择时间', 8 | dateSelect: '选择日期', 9 | weekSelect: '选择周', 10 | clear: '清除', 11 | month: '月', 12 | year: '年', 13 | previousMonth: '上个月 (翻页上键)', 14 | nextMonth: '下个月 (翻页下键)', 15 | monthSelect: '选择月份', 16 | yearSelect: '选择年份', 17 | decadeSelect: '选择年代', 18 | yearFormat: 'YYYY年', 19 | monthFormat: 'M月', 20 | dayFormat: 'D日', 21 | dateFormat: 'YYYY年M月D日', 22 | dateTimeFormat: 'YYYY年M月D日 HH时mm分ss秒', 23 | previousYear: '上一年 (Control键加左方向键)', 24 | nextYear: '下一年 (Control键加右方向键)', 25 | previousDecade: '上一年代', 26 | nextDecade: '下一年代', 27 | previousCentury: '上一世纪', 28 | nextCentury: '下一世纪' 29 | }; 30 | 31 | export default locale; 32 | -------------------------------------------------------------------------------- /components/icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import * as CSS from 'csstype'; 4 | import './index.less'; 5 | 6 | export interface IconProps { 7 | size?: number | string; 8 | color?: CSS.Property.Color; 9 | type?: string; 10 | onClick?: () => unknown; 11 | className?: string; 12 | style?: CSSProperties; 13 | } 14 | 15 | function Icon(props: IconProps) { 16 | const { className, style, size, color, type, onClick } = props; 17 | return ( 18 | 23 | ); 24 | } 25 | 26 | Icon.defaultProps = { 27 | onClick: () => undefined 28 | }; 29 | 30 | export default Icon; 31 | -------------------------------------------------------------------------------- /components/date-picker/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DatePicker from './DatePicker'; 3 | import DatePanelIndex from './panels'; 4 | import { DatePickerProps, DatePanelProps } from './interface'; 5 | 6 | type MergedDatePickerType = typeof DatePicker & { 7 | DatePanel: typeof DatePanelIndex; 8 | RangePicker: typeof DatePicker; 9 | RangePanel: typeof DatePanelIndex; 10 | }; 11 | 12 | function RangePanel(props: DatePanelProps) { 13 | return ; 14 | } 15 | RangePanel.defaultProps = DatePanelIndex.defaultProps; 16 | 17 | const MergedDatePicker = DatePicker as MergedDatePickerType; 18 | MergedDatePicker.DatePanel = DatePanelIndex; 19 | MergedDatePicker.RangePicker = (props: DatePickerProps) => ( 20 | 21 | ); 22 | MergedDatePicker.RangePanel = RangePanel; 23 | 24 | export default MergedDatePicker; 25 | -------------------------------------------------------------------------------- /docs/static/css/1.b506178b.chunk.css: -------------------------------------------------------------------------------- 1 | .img-demo { 2 | padding-bottom: 30px; 3 | background-color: #fff; 4 | } 5 | .img-demo .sty-img { 6 | margin-right: 10px; 7 | } 8 | .img-demo .list { 9 | padding: 0 16px; 10 | display: flex; 11 | flex-wrap: wrap; 12 | } 13 | .img-demo .list .item { 14 | width: 100px; 15 | margin-right: 10px; 16 | margin-bottom: 10px; 17 | text-align: center; 18 | } 19 | .img-demo .list .item .text { 20 | color: #646566; 21 | font-size: 14px; 22 | margin-top: 5px; 23 | } 24 | .img-demo .list .have-border { 25 | border: 1px dashed #ddd; 26 | } 27 | .img-demo .lazy-list { 28 | margin: 0 16px; 29 | height: 230px; 30 | width: 100px; 31 | border: 1px dashed #ddd; 32 | overflow: auto; 33 | } 34 | .img-demo .lazy-list-h { 35 | margin: 16px; 36 | display: flex; 37 | height: 100px; 38 | border: 1px dashed #ddd; 39 | overflow-x: auto; 40 | } 41 | 42 | -------------------------------------------------------------------------------- /site/page/Cell/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Cell } from '@/components/index'; 3 | 4 | function CellDemo() { 5 | return ( 6 |
7 |
基础用法
8 | 右侧内容 9 | 10 | 右侧内容 11 | 12 | 13 |
展示箭头
14 | 15 | 16 | 右侧内容 17 | 18 | 19 | 右侧内容 20 | 21 | 22 |
水波纹反馈
23 | 24 | 25 | 居中内容 26 | 27 |
28 | ); 29 | } 30 | export default CellDemo; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "buildOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "docs", // 指定输出目录 7 | "module": "esnext", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015' 8 | "target": "es6", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' 9 | "jsx": "react", // 允许编译 javascript 文件 10 | "moduleResolution": "node", // 选择模块解析策略 11 | "allowSyntheticDefaultImports": true, 12 | "lib": ["es6", "dom"], 13 | "sourceMap": true, // 生成相应的 '.map' 文件 14 | "allowJs": true, // 扩展名可以是 .js/.jsx 15 | "checkJs": false, // 开启 js 检测 16 | "noUnusedLocals": true, // 有未使用的变量时,抛出错误 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["site/**/*", "@types/*", "components/*"], // 需要编译的文件目录 22 | "exclude": ["node_modules", "docs", "public", "mock"] // 排除编译的文件目录 23 | } 24 | -------------------------------------------------------------------------------- /site/page/Image/index.less: -------------------------------------------------------------------------------- 1 | .img-demo { 2 | padding-bottom: 30px; 3 | background-color: #fff; 4 | 5 | .sty-img { 6 | margin-right: 10px; 7 | } 8 | 9 | .list { 10 | padding: 0 16px; 11 | display: flex; 12 | flex-wrap: wrap; 13 | 14 | .item { 15 | width: 100px; 16 | margin-right: 10px; 17 | margin-bottom: 10px; 18 | text-align: center; 19 | 20 | .text { 21 | color: #646566; 22 | font-size: 14px; 23 | margin-top: 5px; 24 | } 25 | } 26 | 27 | .have-border { 28 | border: 1px dashed #ddd; 29 | } 30 | } 31 | 32 | .lazy-list { 33 | margin: 0 16px; 34 | height: 230px; 35 | width: 100px; 36 | border: 1px dashed #ddd; 37 | overflow: auto; 38 | } 39 | 40 | .lazy-list-h { 41 | margin: 16px; 42 | display: flex; 43 | height: 100px; 44 | border: 1px dashed #ddd; 45 | overflow-x: auto; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /site/page/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Loading } from '@/components/index'; 3 | import './index.less'; 4 | 5 | function LoadingDemo() { 6 | return ( 7 |
8 |
加载类型
9 | 10 | 11 | 12 |
自定义颜色
13 | 14 | 15 | 16 |
自定义大小
17 | 18 | 19 | 20 |
加载文案
21 | 加载中... 22 | 23 |
垂直排列
24 | 加载中... 25 |
26 | ); 27 | } 28 | export default LoadingDemo; 29 | -------------------------------------------------------------------------------- /components/nav-bar/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | .sty-navbar-box { 4 | position: relative; 5 | height: 47px; 6 | line-height: 47px; 7 | text-align: center; 8 | background-color: #fff; 9 | color: #1989fa; 10 | 11 | &.fixed { 12 | position: fixed; 13 | top: 0; 14 | width: 100%; 15 | z-index: 999; 16 | } 17 | 18 | & .navbar-left { 19 | position: absolute; 20 | top: 0; 21 | bottom: 0; 22 | left: 0; 23 | display: flex; 24 | align-items: center; 25 | padding: 0 16px; 26 | font-size: 16px; 27 | } 28 | 29 | & .navbar-title { 30 | max-width: 60%; 31 | margin: 0 auto; 32 | color: #323233; 33 | font-weight: 500; 34 | font-size: 16px; 35 | } 36 | 37 | & .navbar-right { 38 | position: absolute; 39 | top: 0; 40 | bottom: 0; 41 | right: 0; 42 | display: flex; 43 | align-items: center; 44 | padding: 0 16px; 45 | font-size: 16px; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /site/page/Select/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select } from '@/components/index'; 3 | 4 | const dataSource1 = new Array(10).fill({}).map((item, index) => ({ 5 | label: `选项${index + 1}`, 6 | value: index + 1 7 | })); 8 | 9 | const dataSource2 = new Array(10).fill({}).map((item, index) => ({ 10 | label: `选项${index + 1}`, 11 | value: index + 1, 12 | disabled: !!(index % 2) 13 | })); 14 | 15 | function SelectDemo() { 16 | return ( 17 |
18 |
基础用法
19 | 22 | 25 | 28 | 31 |
32 | ); 33 | } 34 | export default SelectDemo; 35 | -------------------------------------------------------------------------------- /site/page/Switch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Switch } from '@/components/index'; 3 | 4 | function SwitchDemo() { 5 | const [value, setValue] = useState(true); 6 | return ( 7 |
8 |
基础用法
9 |
10 | 11 |
12 | 13 | 默认选中 14 | 15 | 16 | 禁用状态 17 | 18 | 19 | 加载状态 20 | 21 | 22 | 自定义颜色 23 | 24 | 25 | 自定义大小 26 | 27 | 28 | 受控模式 29 | 30 | 31 |
32 | ); 33 | } 34 | export default SwitchDemo; 35 | -------------------------------------------------------------------------------- /components/toast/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @toastPrefixCls: sty-toast; 4 | 5 | .@{toastPrefixCls}-text { 6 | position: fixed; 7 | top: 50%; 8 | left: 50%; 9 | transform: translate3d(-50%, -50%, 0); 10 | min-width: 60px; 11 | border-radius: 3px; 12 | color: #fff; 13 | background-color: rgba(58, 58, 58, .9); 14 | line-height: 1.5; 15 | padding: 9px 15px; 16 | max-width: 50%; 17 | text-align: center; 18 | 19 | &.@{toastPrefixCls}-icon { 20 | border-radius: 5px; 21 | padding: 15px; 22 | 23 | .@{toastPrefixCls}-text-info { 24 | margin-top: 6px; 25 | } 26 | } 27 | 28 | .sty-loading { 29 | color: #fff; 30 | } 31 | } 32 | 33 | .fadeIn { 34 | animation: fadeIn 500ms forwards; 35 | } 36 | 37 | 38 | .fadeOut { 39 | animation: fadeOut 500ms; 40 | } 41 | 42 | 43 | @keyframes fadeIn { 44 | from { 45 | opacity: 0; 46 | } 47 | 48 | to { 49 | opacity: 1; 50 | } 51 | } 52 | 53 | @keyframes fadeOut { 54 | from { 55 | opacity: 1; 56 | } 57 | 58 | to { 59 | opacity: 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /site/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocation, useHistory, Switch } from 'react-router-dom'; 3 | import { TransitionGroup, CSSTransition } from 'react-transition-group'; 4 | import { renderRoutes } from 'react-router-config'; 5 | import routes from './routes'; 6 | import '@vant/touch-emulator'; // 此库的作用是将鼠标事件转换为手势事件 7 | import './app.css'; 8 | 9 | // 页面过渡动画参考这里https://juejin.im/post/5cb1e4275188251ace1feee9 10 | // 请用高本版的node启动项目 11 | 12 | const ANIMATION_MAP = { 13 | PUSH: 'forward', 14 | POP: 'back' 15 | }; 16 | 17 | function App() { 18 | const location = useLocation(); 19 | const history = useHistory(); 20 | return ( 21 |
22 | 24 | React.cloneElement(child, { 25 | classNames: ANIMATION_MAP[history.action] 26 | }) 27 | } 28 | > 29 | 30 | {renderRoutes(routes)} 31 | 32 | 33 |
34 | ); 35 | } 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /components/action-sheet/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @actionSheetPrefixCls: sty-actionSheet; 4 | 5 | .@{actionSheetPrefixCls} { 6 | max-height: 80%; 7 | overflow-y: auto; 8 | 9 | .sty-button { 10 | border: none; 11 | 12 | } 13 | 14 | &-header { 15 | position: relative; 16 | font-weight: 500; 17 | font-size: 16px; 18 | line-height: 44px; 19 | text-align: center; 20 | 21 | .icon { 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | padding: 0 16px; 26 | color: #c8c9cc; 27 | font-size: 22px; 28 | line-height: inherit; 29 | } 30 | } 31 | 32 | &-description { 33 | padding: 16px; 34 | color: #646566; 35 | font-size: 14px; 36 | line-height: 20px; 37 | text-align: center; 38 | } 39 | 40 | &-cancelText { 41 | border-top: 8px solid #f7f8fa; 42 | } 43 | 44 | &-action { 45 | color: #c8c9cc; 46 | 47 | &-name { 48 | font-size: 16px; 49 | color: #323233; 50 | } 51 | 52 | &-subname { 53 | margin-left: 4px; 54 | color: #646566; 55 | font-size: 12px; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /site/page/Home/index.less: -------------------------------------------------------------------------------- 1 | .home { 2 | height: 100%; 3 | background-color: #fff; 4 | padding: 50px 20px; 5 | box-sizing: border-box; 6 | overflow: auto; 7 | } 8 | 9 | .demo-home-nav__title { 10 | margin: 24px 0 8px 16px; 11 | color: rgba(69, 90, 100, 0.6); 12 | font-size: 14px; 13 | } 14 | 15 | .demo-home-nav__block { 16 | position: relative; 17 | display: flex; 18 | margin: 0 0 12px; 19 | padding-left: 20px; 20 | color: #323233; 21 | font-weight: 500; 22 | font-size: 14px; 23 | line-height: 40px; 24 | background: #f7f8fa; 25 | border-radius: 99px; 26 | transition: background 0.3s; 27 | text-decoration: none; 28 | } 29 | 30 | .demo-home-nav__block:hover { 31 | background: #eef0f4; 32 | } 33 | 34 | .demo-home-nav__block:active { 35 | background: #e4e8ee; 36 | } 37 | 38 | .demo-home-nav__icon { 39 | position: absolute; 40 | top: 50%; 41 | right: 16px; 42 | width: 16px; 43 | height: 16px; 44 | margin-top: -8px; 45 | } 46 | 47 | // 去除a标签点击的蓝色背景 48 | a { 49 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 50 | -webkit-user-select: none; 51 | -moz-user-focus: none; 52 | -moz-user-select: none; 53 | } 54 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true 5 | }, 6 | extends: [ 7 | 'standard', 8 | "plugin:react/recommended" 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | globals: { 12 | Atomics: 'readonly', 13 | SharedArrayBuffer: 'readonly' 14 | }, 15 | parserOptions: { 16 | ecmaFeatures: { 17 | jsx: true 18 | }, 19 | ecmaVersion: 2018, 20 | sourceType: 'module' 21 | }, 22 | plugins: [ 23 | 'react', 24 | 'prettier', 25 | '@typescript-eslint' 26 | ], 27 | rules: { 28 | semi: ["error", "always"], //语句必须用; 29 | "space-before-function-paren": 0, 30 | "no-unused-vars": 1, 31 | "react/prop-types": [0, { ignore: ['className', 'style', 'children'] }], //定义是否检测propTypes 32 | "no-return-assign": 0, 33 | "react/display-name": 0, 34 | "prettier/prettier": ["error", { 35 | singleQuote: true, 36 | jsxSingleQuote: true, 37 | endOfLine: 'auto', 38 | trailingComma: 'none', 39 | arrowParens: 'avoid' 40 | }], 41 | eqeqeq: 0, 42 | "no-use-before-define": "off", //'React' was used before it was defined 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from './button'; 2 | export { default as Icon } from './icon'; 3 | export { default as NavBar } from './nav-bar'; 4 | export { default as Ripple } from './ripple'; 5 | export { default as Loading } from './loading'; 6 | export { default as Switch } from './switch'; 7 | export { default as Timeline } from './timeline'; 8 | export { default as Cell } from './cell'; 9 | export { default as Checkbox } from './checkbox'; 10 | export { default as Radio } from './radio'; 11 | export { default as Toast } from './toast'; 12 | export { default as Popup } from './popup'; 13 | export { default as ActionSheet } from './action-sheet'; 14 | export { default as Empty } from './empty'; 15 | export { default as Dialog } from './dialog'; 16 | export { default as Image } from './image'; 17 | export { default as Tabs } from './tabs'; 18 | export { default as PullRefresh } from './pull-refresh'; 19 | export { default as Overlay } from './overlay'; 20 | export { default as Picker } from './picker'; 21 | export { default as Swipe } from './swipe'; 22 | export { default as CellPopup } from './cell-popup'; 23 | export { default as DatePicker } from './date-picker'; 24 | export { default as Select } from './select'; 25 | -------------------------------------------------------------------------------- /site/page/DatePicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DatePicker } from '@/components/index'; 3 | import dayjs from 'dayjs'; 4 | const { DatePanel, RangePicker, RangePanel } = DatePicker; 5 | 6 | function DatePickerDemo() { 7 | return ( 8 |
9 |
基础用法
10 | 选择日期 11 | 选择月份 12 | 选择年 13 | { 16 | return ( 17 | dayjs().subtract(1, 'day').format('YYYY-MM-DD') === 18 | date.format('YYYY-MM-DD') 19 | ); 20 | }} 21 | > 22 | 禁用前一天 23 | 24 |
范围选择器
25 | 选择日期 26 | 选择月份 27 | 选择年 28 | 29 |
面板用法
30 | 31 |
范围选择面板用法
32 | 33 |
34 | ); 35 | } 36 | export default DatePickerDemo; 37 | -------------------------------------------------------------------------------- /site/index.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /site/page/Swipe/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Swipe } from '@/components/index'; 3 | import './index.less'; 4 | 5 | const data = ['#39a9ed', '#66c6f2', 'orange', 'pink']; 6 | 7 | function SwipeDemo() { 8 | return ( 9 |
10 |
基础用法
11 | 12 | {data.map((item, index) => { 13 | return ( 14 |
21 | {index + 1} 22 |
23 | ); 24 | })} 25 |
26 |
纵向滚动
27 | 28 | {data.map((item, index) => { 29 | return ( 30 |
37 | {index + 1} 38 |
39 | ); 40 | })} 41 |
42 |
43 | ); 44 | } 45 | export default SwipeDemo; 46 | -------------------------------------------------------------------------------- /site/page/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Icon } from '@/components/index'; 4 | import nav from './config'; 5 | import './index.less'; 6 | 7 | function Home() { 8 | const homeRef = useRef(); 9 | 10 | useEffect(() => { 11 | const scrollTop = localStorage.getItem('scrollTop'); 12 | homeRef.current.scrollTop = Number(scrollTop) || 0; 13 | return () => { 14 | const top = homeRef.current?.scrollTop; 15 | localStorage.setItem('scrollTop', JSON.stringify(top)); 16 | }; 17 | }, []); 18 | 19 | return ( 20 |
21 | {nav.map(item => ( 22 |
23 |
{item.title}
24 |
25 | {item.items.map(sub => ( 26 | 31 | {sub.title} 32 | 33 | 34 | ))} 35 |
36 |
37 | ))} 38 |
39 | ); 40 | } 41 | 42 | export default Home; 43 | -------------------------------------------------------------------------------- /site/page/Home/config.ts: -------------------------------------------------------------------------------- 1 | const nav = [ 2 | { 3 | title: '通用组件', 4 | items: [ 5 | { path: 'button', title: 'Button 按钮' }, 6 | { path: 'cell', title: 'Cell 单元格' }, 7 | { path: 'icon', title: 'Icon 图标' }, 8 | { path: 'image', title: 'Image 图片' }, 9 | { path: 'popup', title: 'Popup 弹出层' }, 10 | { path: 'ripple', title: 'Ripple 波纹' }, 11 | { path: 'timeline', title: 'Timeline 时间线' }, 12 | { path: 'swipe', title: 'Swipe 轮播' } 13 | ] 14 | }, 15 | { 16 | title: '表单组件', 17 | items: [ 18 | { path: 'switch', title: 'Switch 开关' }, 19 | { path: 'radio', title: 'Radio 单选框' }, 20 | { path: 'checkbox', title: 'Checkbox 复选框' }, 21 | { path: 'select', title: 'Select 选择框' }, 22 | { path: 'picker', title: 'Picker 选择器' }, 23 | { path: 'date-picker', title: 'DatePicker 日期选择器' } 24 | ] 25 | }, 26 | { 27 | title: '反馈组件', 28 | items: [ 29 | { path: 'action-sheet', title: 'ActionSheet 动作面板' }, 30 | { path: 'dialog', title: 'Dialog 弹出框' }, 31 | { path: 'loading', title: 'Loading 加载' }, 32 | { path: 'pull-refresh', title: 'PullRefresh 下拉刷新' }, 33 | { path: 'toast', title: 'Toast 提示' } 34 | ] 35 | }, 36 | { 37 | title: '导航组件', 38 | items: [ 39 | { path: 'nav-bar', title: 'NavBar 导航栏' }, 40 | { path: 'tabs', title: 'Tabs 标签页' } 41 | ] 42 | } 43 | ]; 44 | export default nav; 45 | -------------------------------------------------------------------------------- /site/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #323233; 3 | font-family: PingFang SC, "Helvetica Neue", Arial, sans-serif; 4 | background-color: #f7f8fa; 5 | } 6 | 7 | .forward-enter { 8 | z-index: 2; 9 | /* opacity: 0; */ 10 | transform: translateX(100%); 11 | } 12 | 13 | .forward-enter-active { 14 | z-index: 2; 15 | /* opacity: 1; */ 16 | transform: translateX(0); 17 | transition: all 500ms; 18 | } 19 | 20 | .forward-exit { 21 | z-index: 1; 22 | /* opacity: 1; */ 23 | } 24 | 25 | .forward-exit-active { 26 | z-index: 1; 27 | /* opacity: .3; */ 28 | transition: all 500ms; 29 | } 30 | 31 | .back-enter { 32 | z-index: 1; 33 | /* opacity: .3; */ 34 | } 35 | 36 | .back-enter-active { 37 | z-index: 1; 38 | /* opacity: 1; */ 39 | transform: translateX(0); 40 | transition: all 500ms; 41 | } 42 | 43 | .back-exit { 44 | z-index: 2; 45 | /* opacity: 1; */ 46 | transform: translateX(0); 47 | } 48 | 49 | .back-exit-active { 50 | z-index: 2; 51 | /* opacity: 0; */ 52 | transform: translate(100%); 53 | transition: all 500ms; 54 | } 55 | 56 | 57 | .page-box{ 58 | position: absolute; 59 | top: 0; 60 | left: 0; 61 | width: 100vw; 62 | height: 100vh; 63 | background-color: #f7f8fa; 64 | } 65 | 66 | 67 | .demo-block__title { 68 | margin: 0; 69 | padding: 32px 16px 16px; 70 | color: rgba(69, 90, 100, 0.6); 71 | font-weight: normal; 72 | font-size: 16px; 73 | line-height: 16px; 74 | } -------------------------------------------------------------------------------- /components/swipe/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @swipePrefixCls: sty-swipe; 4 | 5 | .@{swipePrefixCls}-wrapper { 6 | position: relative; 7 | overflow: hidden; 8 | width: 100%; 9 | height: 150px; 10 | 11 | .@{swipePrefixCls} { 12 | display: flex; 13 | width: 100%; 14 | height: 100%; 15 | 16 | &-item { 17 | flex-shrink: 0; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | 22 | } 23 | 24 | .@{swipePrefixCls}-dots-box { 25 | position: absolute; 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | flex-wrap: wrap; 30 | bottom: 0; 31 | width: 100%; 32 | height: 18px; 33 | text-align: center; 34 | font-size: 18px; 35 | color: #000; 36 | 37 | .@{swipePrefixCls}-dot { 38 | display: block; 39 | width: 8px; 40 | height: 8px; 41 | margin: 0 3px; 42 | border-radius: 50%; 43 | background: #ccc; 44 | 45 | &.active { 46 | background-color: #fff; 47 | } 48 | } 49 | } 50 | 51 | &&_vertical { 52 | 53 | .@{swipePrefixCls} { 54 | flex-direction: column; 55 | } 56 | 57 | .@{swipePrefixCls}-dots-box { 58 | flex-direction: column; 59 | width: 18px; 60 | height: 100%; 61 | left: 2px; 62 | 63 | .@{swipePrefixCls}-dot { 64 | margin: 3px 0; 65 | } 66 | } 67 | } 68 | } 69 | 70 | 71 | 72 | 73 | ::-webkit-scrollbar { 74 | width: 0; 75 | background: transparent; 76 | } 77 | -------------------------------------------------------------------------------- /site/page/Timeline/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Timeline } from '@/components/index'; 3 | 4 | function TimelineDemo() { 5 | return ( 6 |
7 |
基础用法
8 | 9 | 10 | 君不见黄河之水天上来,奔流到海不复回。 11 | 君不见高堂明镜悲白发,朝如青丝暮成雪。 12 | 13 | 14 | 人生得意须尽欢,莫使金樽空对月。 天生我材必有用,千金散尽还复来。 15 | 16 | 17 | 烹羊宰牛且为乐,会须一饮三百杯。 岑夫子,丹丘生,将进酒,杯莫停。 18 | 19 | 20 | 与君歌一曲,请君为我倾耳听。 钟鼓馔玉不足贵,但愿长醉不愿醒。 21 | 22 | 23 |
隐藏时间
24 | 25 | 26 | 君不见黄河之水天上来,奔流到海不复回。 27 | 君不见高堂明镜悲白发,朝如青丝暮成雪。 28 | 29 | 30 | 人生得意须尽欢,莫使金樽空对月。 天生我材必有用,千金散尽还复来。 31 | 32 | 33 | 烹羊宰牛且为乐,会须一饮三百杯。 岑夫子,丹丘生,将进酒,杯莫停。 34 | 35 | 36 | 与君歌一曲,请君为我倾耳听。 钟鼓馔玉不足贵,但愿长醉不愿醒。 37 | 38 | 39 |
40 | ); 41 | } 42 | export default TimelineDemo; 43 | -------------------------------------------------------------------------------- /components/switch/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @switchPrefixCls: sty-switch; 4 | 5 | .@{switchPrefixCls} { 6 | position: relative; 7 | display: inline-block; 8 | width: 2em; 9 | height: 1em; 10 | border-radius: 1em; 11 | font-size: 30px; 12 | background-color: #e5e5e5; 13 | transition: all .3s; 14 | 15 | &::after, 16 | &-node { 17 | position: absolute; 18 | content: ''; 19 | top: 1.5px; 20 | left: 1.5px; 21 | width: calc(2em - 3px); 22 | height: calc(1em - 3px); 23 | border-radius: calc(1em - 3px); 24 | background-color: #fff; 25 | transition: all .2s; 26 | z-index: 1; 27 | } 28 | 29 | &-node { 30 | width: calc(1em - 3px); 31 | transform: translateX(0); 32 | box-shadow: 2px 2px 4px rgba(0, 0, 0, .21); 33 | z-index: 2; 34 | } 35 | 36 | &&-on { 37 | background-color: @base-color; 38 | 39 | &::after { 40 | transform: scale(0); 41 | } 42 | 43 | .@{switchPrefixCls}-node { 44 | transform: translateX(calc(1em - 1px)); 45 | 46 | .switch-loading { 47 | color: @base-color; 48 | } 49 | } 50 | } 51 | 52 | &&-disabled { 53 | cursor: not-allowed; 54 | opacity: 0.5; 55 | } 56 | 57 | .switch-loading { 58 | width: 50%; 59 | height: 50%; 60 | top: 25%; 61 | left: 25%; 62 | } 63 | 64 | 65 | input[type=checkbox] { 66 | // position: absolute; 67 | // top: 0; 68 | // left: 0; 69 | // width: 100%; 70 | // height: 100%; 71 | // border: none; 72 | // opacity: 0; 73 | display: none; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /components/button/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | 4 | .sty-button { 5 | position: relative; 6 | display: block; 7 | height: 47px; 8 | line-height: 47px; 9 | text-align: center; 10 | font-size: 16px; 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | word-break: break-word; 14 | white-space: nowrap; 15 | color: @base-text-color; 16 | background-color: #fff; 17 | border-radius: @base-border-radius; 18 | border: 1px solid @base-border-color; 19 | 20 | &&-disabled { 21 | color: rgba(0, 0, 0, .3); 22 | opacity: .6; 23 | } 24 | 25 | &&-primary { 26 | color: #fff; 27 | background-color: @base-color; 28 | border: none; 29 | } 30 | 31 | &&-warning { 32 | color: #fff; 33 | background-color: @warning-color; 34 | border: none; 35 | } 36 | 37 | &&-ghost { 38 | color: @base-color; 39 | border-color: @base-color; 40 | } 41 | 42 | &&-round{ 43 | border-radius: 999px; 44 | } 45 | 46 | &&-inline { 47 | display: inline-block; 48 | padding: 0 15px; 49 | } 50 | 51 | &-icon{ 52 | vertical-align: middle; 53 | font-size: 1.2em; 54 | 55 | & + .sty-button-text{ 56 | margin-left: .5em; 57 | } 58 | } 59 | 60 | & &-loading{ 61 | display: inline-flex; 62 | height: 100%; 63 | color: inherit; 64 | vertical-align: top; 65 | 66 | & + .sty-button-text{ 67 | margin-left: 5px; 68 | } 69 | } 70 | 71 | & &-ripple { 72 | position: absolute; 73 | top: 0; 74 | left: 0; 75 | width: 100%; 76 | height: 100%; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /components/cell/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @cellPrefixCls: sty-cell; 4 | 5 | 6 | .@{cellPrefixCls} { 7 | position: relative; 8 | display: flex; 9 | padding: 10px 16px; 10 | line-height: 24px; 11 | background-color: #fff; 12 | color: @base-text-color; 13 | font-size: 14px; 14 | 15 | &-center { 16 | align-items: center; 17 | } 18 | 19 | &-clickable:active { 20 | background-color: #f2f3f5; 21 | } 22 | 23 | &::after { 24 | position: absolute; 25 | box-sizing: border-box; 26 | content: ' '; 27 | pointer-events: none; 28 | right: 0; 29 | bottom: 0; 30 | left: 16px; 31 | border-bottom: 1px solid @base-border-color; 32 | -webkit-transform: scaleY(0.5); 33 | transform: scaleY(0.5); 34 | } 35 | 36 | &-title { 37 | margin-right: 8px; 38 | 39 | .@{cellPrefixCls}-label { 40 | margin-top: 3px; 41 | color: @base-text-label-color; 42 | font-size: 12px; 43 | line-height: 18px; 44 | } 45 | } 46 | 47 | &-value { 48 | flex: 1; 49 | position: relative; 50 | overflow: hidden; 51 | color: @base-text-label-color; 52 | text-align: right; 53 | vertical-align: middle; 54 | word-wrap: break-word; 55 | 56 | .sty-switch { 57 | font-size: 24px; 58 | } 59 | } 60 | 61 | .arrow-icon { 62 | min-width: 1em; 63 | height: 24px; 64 | font-size: 16px; 65 | line-height: 24px; 66 | color: @base-text-label-color; 67 | margin-left: 5px; 68 | } 69 | 70 | &-ripple { 71 | position: absolute; 72 | top: 0; 73 | left: 0; 74 | width: 100%; 75 | height: 100%; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /components/checkbox/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @checkboxPrefixCls: sty-checkbox; 4 | 5 | .@{checkboxPrefixCls}-group { 6 | 7 | &&-horizontal { 8 | display: flex; 9 | flex-wrap: wrap; 10 | 11 | .@{checkboxPrefixCls} { 12 | margin-right: 12px; 13 | } 14 | } 15 | 16 | } 17 | 18 | .@{checkboxPrefixCls} { 19 | display: flex; 20 | align-items: center; 21 | margin-bottom: 8px; 22 | 23 | &-cell{ 24 | margin-bottom: 0; 25 | } 26 | 27 | &-icon { 28 | display: inline-block; 29 | width: 1em; 30 | height: 1em; 31 | font-size: 20px; 32 | line-height: 1em; 33 | text-align: center; 34 | box-sizing: border-box; 35 | 36 | 37 | .sty-icon { 38 | font-size: 0.8em; 39 | border: 1px solid #d9d9d9; 40 | color: transparent; 41 | transition: all .2s; 42 | } 43 | 44 | &&-round { 45 | .sty-icon { 46 | border-radius: 50%; 47 | } 48 | } 49 | 50 | &-checked { 51 | .sty-icon { 52 | background-color: @base-color; 53 | border-color: @base-color; 54 | color: #fff; 55 | 56 | .@{checkboxPrefixCls}-disabled & { 57 | background-color: @base-text-disabled; 58 | border-color: @base-text-disabled; 59 | } 60 | } 61 | } 62 | } 63 | 64 | &-label { 65 | margin-left: 8px; 66 | color: @base-text-color; 67 | line-height: 20px; 68 | 69 | .@{checkboxPrefixCls}-disabled & { 70 | color: @base-text-disabled; 71 | } 72 | } 73 | 74 | &-disabled { 75 | .sty-cell-title { 76 | color: @base-text-disabled; 77 | } 78 | } 79 | 80 | input { 81 | display: none; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /components/loading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as CSS from 'csstype'; 3 | import { classnames } from '../_utils/index'; 4 | import './index.less'; 5 | 6 | function LoadingIcon(type) { 7 | if (type === 'spinner') { 8 | const Spin = []; 9 | for (let i = 0; i < 12; i++) { 10 | Spin.push(); 11 | } 12 | return Spin; 13 | } 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export interface LoadingProps { 22 | type?: 'spinner' | 'circular'; 23 | size?: number; 24 | color?: CSS.Property.Color; 25 | vertical?: boolean; 26 | className?: string; 27 | style?: React.CSSProperties; 28 | children?: React.ReactNode; 29 | } 30 | 31 | function Loading(props: LoadingProps) { 32 | const { type, size, color, vertical, className, children, ...other } = props; 33 | 34 | const style: React.CSSProperties = { color }; 35 | if (size) { 36 | style.width = `${size}px`; 37 | style.height = `${size}px`; 38 | } 39 | return ( 40 |
48 | 55 | {LoadingIcon(type)} 56 | 57 | {children && {children}} 58 |
59 | ); 60 | } 61 | 62 | Loading.defaultProps = { 63 | type: 'circular' 64 | }; 65 | 66 | export default Loading; 67 | -------------------------------------------------------------------------------- /components/style/default.less: -------------------------------------------------------------------------------- 1 | //样式参考ant-mobile、vant 2 | 3 | @base-color: #3dbdaf; //基本颜色 4 | // @base-color: #80D0C7; //基本颜色 5 | @base-border-color: #ebedf0; 6 | @base-border-radius: 5px; 7 | @base-text-color: #323233; 8 | @base-text-disabled: #c8c9cc; 9 | @base-text-label-color: #969799; 10 | 11 | @warning-color: #ff976a; //警告颜色 12 | 13 | @tabs-text-color: #646566; 14 | 15 | @loading-color: #c8c9cc; 16 | @loading-text-color: @base-text-label-color; 17 | @border-width-base: 1px; 18 | 19 | @navbar-height: 47px; 20 | @navbar-text-color: #1989fa; 21 | 22 | @timeline-line-color: #dcdee3; 23 | @timeline-dot-color: #80d0c780; 24 | @timeline-dot-boder-color: #80d0c733; 25 | @timeline-content-color: #666; 26 | @timeline-time-color: @base-text-label-color; 27 | 28 | @tree-select-nav-bg: #f7f8fa; 29 | 30 | @popup-overlay-bg: rgba(0, 0, 0, 0.7); 31 | 32 | 33 | [class*='sty-hairline']::after { 34 | position: absolute; 35 | box-sizing: border-box; 36 | content: ' '; 37 | pointer-events: none; 38 | top: -50%; 39 | right: -50%; 40 | bottom: -50%; 41 | left: -50%; 42 | border: 0 solid #ebedf0; 43 | transform: scale(0.5); 44 | } 45 | 46 | .sty-hairline { 47 | 48 | &, 49 | &--top, 50 | &--left, 51 | &--right, 52 | &--bottom, 53 | &--surround, 54 | &--top-bottom { 55 | position: relative; 56 | } 57 | 58 | &--top::after { 59 | border-top-width: @border-width-base; 60 | } 61 | 62 | &--left::after { 63 | border-left-width: @border-width-base; 64 | } 65 | 66 | &--right::after { 67 | border-right-width: @border-width-base; 68 | } 69 | 70 | &--bottom::after { 71 | border-bottom-width: @border-width-base; 72 | } 73 | } 74 | 75 | .sty-ellipsis { 76 | overflow: hidden; 77 | white-space: nowrap; 78 | text-overflow: ellipsis; 79 | } 80 | -------------------------------------------------------------------------------- /components/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import { Icon, Overlay } from '../index'; 4 | import { OverlayProps } from '../overlay'; 5 | 6 | import './index.less'; 7 | export type PopupPosition = 'top' | 'bottom' | 'left' | 'right' | 'center'; 8 | export interface PopupProps extends OverlayProps { 9 | position?: PopupPosition; // 弹出层位置 10 | round?: boolean; // 是否是圆角 11 | closable?: boolean; // 是否显示关闭icon 12 | className?: string; 13 | style?: React.CSSProperties; 14 | } 15 | 16 | Popup.defaultProps = { 17 | position: 'center', 18 | round: false, 19 | closable: false 20 | }; 21 | 22 | function Popup(props: PopupProps) { 23 | const { 24 | position, 25 | round, 26 | closable, 27 | className, 28 | style, 29 | animation: animationProps, 30 | children, 31 | ...other 32 | } = props; 33 | 34 | const animation = useMemo(() => { 35 | if (animationProps) { 36 | return animationProps; 37 | } 38 | if (position === 'center') { 39 | return; 40 | } 41 | return { 42 | in: `slide_${position}In`, 43 | out: `slide_${position}Out` 44 | }; 45 | }, [animationProps, position]); 46 | 47 | return ( 48 | 49 |
58 | {closable && ( 59 | 60 | )} 61 | {children} 62 |
63 |
64 | ); 65 | } 66 | 67 | export default Popup; 68 | -------------------------------------------------------------------------------- /site/page/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@/components/index'; 3 | import './index.less'; 4 | 5 | function ButtonDemo() { 6 | return ( 7 |
8 |
按钮类型
9 |
10 | 11 | 12 | 13 | 14 |
15 |
行内按钮
16 |
17 | 20 | 23 | 26 | 29 | 32 |
33 |
禁用
34 |
35 | 38 | 41 |
42 |
加载和图标
43 |
44 | 47 | 50 | 53 |
54 |
55 | ); 56 | } 57 | export default ButtonDemo; 58 | -------------------------------------------------------------------------------- /components/_utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取变量类型 3 | * @param {*} v 4 | */ 5 | export function getType(v) { 6 | const string = Object.prototype.toString.call(v); 7 | const regexp = /(?= ).*(?=\]$)/; // 后行断言有的浏览器不支持 8 | return string.match(regexp)[0].slice(1); 9 | } 10 | 11 | /** 12 | * 类似classnames库的功能 13 | * @param {...any} arg 14 | */ 15 | export function classnames(...arg) { 16 | const classes = []; 17 | for (const item of arg) { 18 | const itemType = getType(item); 19 | switch (itemType) { 20 | case 'Object': { 21 | for (const [key, value] of Object.entries(item)) { 22 | if (value) { 23 | classes.push(key); 24 | } 25 | } 26 | break; 27 | } 28 | case 'Array': { 29 | const str = classnames(...item); 30 | classes.push(str); 31 | break; 32 | } 33 | default: { 34 | if (item) { 35 | classes.push(item); 36 | } 37 | } 38 | } 39 | } 40 | return classes.join(' '); 41 | } 42 | 43 | // 简易的防抖函数 44 | export function throttle(func, interval = 100) { 45 | let timeout; 46 | let startTime = Date.now(); 47 | return function (event) { 48 | event.persist && event.persist(); // 保留对事件的引用 49 | clearTimeout(timeout); 50 | const curTime = Date.now(); 51 | if (curTime - startTime <= interval) { 52 | // 小于规定时间间隔时,用setTimeout在指定时间后再执行 53 | timeout = setTimeout(() => { 54 | func(event); 55 | }, interval); 56 | } else { 57 | // 重新计时并执行函数 58 | startTime = curTime; 59 | func(event); 60 | } 61 | }; 62 | } 63 | 64 | // 取数范围[min,max] 65 | export function range(num: number, min: number, max: number): number { 66 | if (min > max) { 67 | [min, max] = [max, min]; 68 | } 69 | return Math.min(Math.max(num, min), max); 70 | } 71 | -------------------------------------------------------------------------------- /site/page/Popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Cell, Popup, Radio, Switch } from '@/components/index'; 3 | import { PopupPosition } from '@/components/popup'; 4 | 5 | function PopupDemo() { 6 | const [visible, setVisible] = useState(false); 7 | const [position, setPosition] = useState('top'); 8 | const [round, setRound] = useState(false); 9 | const [closable, setClosable] = useState(true); 10 | 11 | const isVertical = ['top', 'bottom'].includes(position); 12 | const style: React.CSSProperties = {}; 13 | if (isVertical) { 14 | style.height = '30vh'; 15 | } else { 16 | style.width = '30vw'; 17 | } 18 | return ( 19 |
20 |
基础用法
21 | setVisible(true)} /> 22 | setVisible(false)} 29 | /> 30 |
弹出层位置
31 | setPosition(v)} 34 | cell 35 | options={[ 36 | { label: '顶部弹出', value: 'top' }, 37 | { label: '底部弹出', value: 'bottom' }, 38 | { label: '左侧弹出', value: 'left' }, 39 | { label: '右侧弹出', value: 'right' }, 40 | { label: '中间弹出', value: 'center' } 41 | ]} 42 | /> 43 |
其他设置
44 | 45 | 显示关闭图标 46 | 47 | 48 | 是否圆角 49 | 50 |
51 | ); 52 | } 53 | export default PopupDemo; 54 | -------------------------------------------------------------------------------- /components/hooks/useTouch.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | 3 | type Touch = { 4 | startX?: number; // 起始时的相对文档x坐标 5 | startY?: number; // 起始时的相对文档y坐标 6 | direction?: 'vertical' | 'horizontal'; // 移动方向 7 | moveX?: number; // 水平移动距离 8 | moveY?: number; // 垂直移动距离 9 | }; 10 | 11 | function useTouch( 12 | disabled = false 13 | ): [Touch, React.MutableRefObject] { 14 | const DOMRef = useRef(); 15 | const touchRef = useRef(null); 16 | const [touch, setTouch] = useState(null); 17 | 18 | useEffect(() => { 19 | const DOM = DOMRef.current; 20 | if (DOM) { 21 | DOM.addEventListener('touchstart', onTouchStart); 22 | DOM.addEventListener('touchmove', onTouchMove); 23 | return () => { 24 | DOM.removeEventListener('touchstart', onTouchStart); 25 | DOM.removeEventListener('touchmove', onTouchMove); 26 | }; 27 | } 28 | }, []); 29 | 30 | useEffect(() => { 31 | touchRef.current = touch; 32 | }, [touch]); 33 | 34 | function onTouchStart(event: TouchEvent) { 35 | if (disabled) { 36 | return; 37 | } 38 | const touches = event.touches; 39 | setTouch({ 40 | startX: touches[0].pageX, 41 | startY: touches[0].pageY 42 | }); 43 | } 44 | function onTouchMove(event: TouchEvent) { 45 | if (!touchRef.current || disabled) { 46 | return; 47 | } 48 | const touches = event.touches; 49 | const moveX = touches[0].pageX - touchRef.current.startX; 50 | const moveY = touches[0].pageY - touchRef.current.startY; 51 | setTouch({ 52 | ...touchRef.current, 53 | moveX, 54 | moveY, 55 | direction: Math.abs(moveX) > Math.abs(moveY) ? 'horizontal' : 'vertical' 56 | }); 57 | } 58 | 59 | return [touch, DOMRef]; 60 | } 61 | 62 | export default useTouch; 63 | -------------------------------------------------------------------------------- /components/date-picker/interface.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CellPopupProps } from '../cell-popup'; 3 | import { GenerateConfig } from './generate'; 4 | import { Dayjs } from 'dayjs'; 5 | 6 | export type PanelMode = 'date' | 'month' | 'year' | 'decade'; 7 | export type PickerMode = Exclude; 8 | export type PickerValue = DateType | [DateType?, DateType?]; 9 | 10 | export type PanelSharedProps = { 11 | prefixCls?: string; 12 | generateConfig: GenerateConfig; 13 | 14 | prevIcon?: React.ReactNode; 15 | nextIcon?: React.ReactNode; 16 | superPrevIcon?: React.ReactNode; 17 | superNextIcon?: React.ReactNode; 18 | value: PickerValue; 19 | viewDate: DateType; 20 | picker: PickerMode; 21 | isRange?: boolean; 22 | disabledDate?: (date: DateType) => boolean; 23 | onSelect: (date: DateType) => unknown; // 选中日期的回调 24 | onViewDateChange: (value: DateType) => unknown; // 中间title日期变化的回调 25 | onPanelChange: (mode: PanelMode, viewValue: DateType) => void; // 面板模式改变的回调 26 | }; 27 | 28 | export interface DatePanelProps { 29 | picker?: PickerMode; 30 | prefixCls?: string; 31 | generateConfig?: GenerateConfig; 32 | value?: PickerValue; 33 | defaultValue?: PickerValue; 34 | isRange?: boolean; 35 | disabledDate?: (date: Dayjs) => boolean; 36 | onSelect?: (date: Dayjs) => unknown; // 选中日期的回调 37 | onChange?: (value: PickerValue) => unknown; // 值改变的回调 38 | onPanelChange?: (mode: PanelMode, viewValue: Dayjs) => void; // 面板模式改变的回调 39 | renderExtraFooter?: () => React.ReactNode; 40 | 41 | className?: string; 42 | style?: React.CSSProperties; 43 | } 44 | 45 | export interface DatePickerProps 46 | extends DatePanelProps, 47 | Omit { 48 | format?: string; 49 | onOk?: (value: PickerValue) => unknown; 50 | } 51 | -------------------------------------------------------------------------------- /site/page/asyncComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType } from 'react'; 2 | import { Loading } from '@/components/index'; 3 | 4 | const style = { 5 | display: 'flex', 6 | alignItems: 'center', 7 | justifyContent: 'center', 8 | width: '100vw', 9 | height: '100vh', 10 | background: '#fff' 11 | }; 12 | 13 | const DefaultLoading = () => { 14 | return ( 15 |
16 | 加载中... 17 |
18 | ); 19 | }; 20 | 21 | // 这个函数主要是解决路由懒加载和路由动画第一次不生效的 22 | 23 | /** 24 | 为什么没有使用React.lazy 25 | 在使用React.lazy时发现第一次进入路由时并没有切换动画,虽然我用在懒加载的代码外层包裹了div,但是Suspense的fallback 26 | 会在第一次加载时出现 27 | 28 | 29 |
30 | 懒加载组件 31 |
32 |
33 | 34 | 为什么下面函数的loading不会影响路由动画 35 | 36 |
37 | 是否加载 ? 懒加载组件 : loading 38 |
39 | 40 | 可以看到当路由加载时懒加载组件和loading都在我包裹的div里,这里的div会正常切换路由动画 41 | 42 | */ 43 | 44 | function asyncComponent(importComponent, Loading = ) { 45 | class AsyncComponent extends React.Component< 46 | {}, 47 | { component: ComponentType } 48 | > { 49 | constructor(props) { 50 | super(props); 51 | 52 | this.state = { 53 | component: null 54 | }; 55 | } 56 | 57 | async componentDidMount() { 58 | const { default: component } = await importComponent(); 59 | 60 | this.setState({ 61 | component: component 62 | }); 63 | 64 | // 模拟网络延时,可以很清楚的看到组件加载动画 65 | // setTimeout(() => { 66 | // this.setState({ 67 | // component: component 68 | // }); 69 | // }, 2000); 70 | } 71 | 72 | render() { 73 | const C = this.state.component; 74 | 75 | return C ? : Loading; 76 | } 77 | } 78 | 79 | return AsyncComponent; 80 | } 81 | 82 | export default asyncComponent; 83 | -------------------------------------------------------------------------------- /components/date-picker/hooks/useCellClassName.ts: -------------------------------------------------------------------------------- 1 | import { isInRange } from '../_utils/dateUtils'; 2 | import { GenerateConfig } from '../generate'; 3 | 4 | function useCellClassName({ 5 | cellPrefixCls, 6 | generateConfig, 7 | value, 8 | today, 9 | isSameCell, 10 | isInView 11 | }: { 12 | cellPrefixCls?: string; 13 | generateConfig: GenerateConfig; 14 | value?: DateType; 15 | today?: DateType; 16 | isSameCell?: (current: DateType, target: DateType) => boolean; 17 | isInView?: (date: DateType) => boolean; 18 | }) { 19 | const isRange = Array.isArray(value); 20 | function isSelected(v, current) { 21 | if (isRange) { 22 | return isSameCell(v[0], current) || isSameCell(v[1], current); 23 | } 24 | return isSameCell(v, current); 25 | } 26 | 27 | function isRangeStart(v, current) { 28 | if (!isRange) { 29 | return false; 30 | } 31 | return isSameCell(v[0], current); 32 | } 33 | function isRangeEnd(v, current) { 34 | if (!isRange) { 35 | return false; 36 | } 37 | return isSameCell(v[1], current); 38 | } 39 | 40 | function getClassName(currentDate: DateType) { 41 | return { 42 | [cellPrefixCls]: cellPrefixCls, 43 | [`${cellPrefixCls}-selected`]: isSelected(value, currentDate), 44 | [`${cellPrefixCls}-in-view`]: isInView && isInView(currentDate), 45 | [`${cellPrefixCls}-in-range`]: 46 | isRange && isInRange(generateConfig, value[0], value[1], currentDate), 47 | [`${cellPrefixCls}-range-start`]: isRangeStart(value, currentDate), 48 | [`${cellPrefixCls}-range-end`]: isRangeEnd(value, currentDate), 49 | [`${cellPrefixCls}-range-start-single`]: 50 | isRange && isRangeStart(value, currentDate) && !value[1], 51 | [`${cellPrefixCls}-today`]: isSameCell(today, currentDate) 52 | }; 53 | } 54 | return getClassName; 55 | } 56 | 57 | export default useCellClassName; 58 | -------------------------------------------------------------------------------- /components/cell/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import { Icon, Ripple } from '../index'; 4 | import './index.less'; 5 | 6 | export interface CellProps { 7 | title?: React.ReactNode; // 左侧标题 8 | label?: React.ReactNode; // 描述信息 9 | clickable?: boolean; // 是否开启点击反馈 10 | arrow?: 'left' | 'up' | 'right' | 'down' | 'none'; // 箭头方向 11 | center?: boolean; // 内容是否居中 12 | ripple?: boolean; // 是否开启水波纹效果 13 | onClick: (event: React.MouseEvent) => void; 14 | children?: React.ReactNode; 15 | className?: string; 16 | style?: React.CSSProperties; 17 | } 18 | function Cell(props: CellProps) { 19 | const { 20 | title, 21 | label, 22 | clickable, 23 | arrow, 24 | center, 25 | ripple, 26 | children, 27 | className, 28 | style 29 | } = props; 30 | 31 | function onClick(event: React.MouseEvent) { 32 | props.onClick(event); 33 | } 34 | 35 | return ( 36 |
46 |
47 |
{title}
48 | {label !== undefined &&
{label}
} 49 |
50 |
{children}
51 | {arrow !== 'none' && ( 52 | 53 | )} 54 | {ripple && } 55 |
56 | ); 57 | } 58 | 59 | Cell.defaultProps = { 60 | clickable: false, 61 | arrow: 'none', 62 | center: false, 63 | ripple: false, 64 | onClick: () => undefined 65 | }; 66 | 67 | export default Cell; 68 | -------------------------------------------------------------------------------- /components/radio/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Checkbox } from '../index'; 3 | import { CheckboxProps, GroupProps, OptionObjType } from '../checkbox'; 4 | 5 | export type RadioValueType = string | number; 6 | export interface RadioProps extends CheckboxProps {} 7 | export interface RadioGroupProps 8 | extends Omit, 'value' | 'defaultValue' | 'onChange'> { 9 | value?: T; 10 | defaultValue?: T; 11 | onChange?: (v: T, option: OptionObjType) => unknown; 12 | } 13 | 14 | function Radio(props: RadioProps) { 15 | const { ...other } = props; 16 | return ; 17 | } 18 | Radio.defaultProps = { 19 | disabled: false, 20 | defaultChecked: false, 21 | shape: 'round', 22 | onChange: () => undefined 23 | }; 24 | 25 | function RadioGroup(props: RadioGroupProps) { 26 | const { value, defaultValue, onChange, ...other } = props; 27 | const [selectValue, setSelectValue] = useState(defaultValue); 28 | 29 | useEffect(() => { 30 | setSelectValue(value); 31 | }, [value]); 32 | 33 | function onCheckChange(list: Array, optionList: Array>) { 34 | const lastIndex = list.length - 1; 35 | const v = list[lastIndex]; 36 | if (v === undefined) { 37 | return; 38 | } 39 | const option = optionList[lastIndex]; 40 | props.onChange(v, option); 41 | if (value === undefined) { 42 | setSelectValue(v); 43 | } 44 | } 45 | return ( 46 | 53 | ); 54 | } 55 | RadioGroup.defaultProps = { 56 | shape: 'round', 57 | onChange: () => undefined 58 | }; 59 | 60 | Radio.RadioGroup = RadioGroup; 61 | 62 | export default Radio; 63 | -------------------------------------------------------------------------------- /components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Ripple, Icon, Loading } from '../index'; 3 | import { classnames } from '../_utils/index'; 4 | import './index.less'; 5 | 6 | export interface ButtonProps { 7 | disabled?: boolean; 8 | inline?: boolean; // 是否是行内按钮 9 | loading?: boolean; 10 | ripple?: boolean; 11 | round?: boolean; 12 | icon?: string; 13 | type?: 'primary' | 'warning' | 'ghost' | 'default'; 14 | className?: string; 15 | style?: React.CSSProperties; 16 | children?: React.ReactNode; 17 | onClick?: (event: React.MouseEvent) => void; 18 | } 19 | 20 | function Button(props: ButtonProps) { 21 | const { 22 | disabled, 23 | inline, 24 | loading, 25 | ripple, 26 | round, 27 | icon, 28 | type, 29 | className, 30 | style, 31 | children 32 | } = props; 33 | 34 | const cls = { 35 | 'sty-button': true, 36 | [className]: className, 37 | [`sty-button-${type}`]: type, 38 | 'sty-button-disabled': disabled, 39 | 'sty-button-inline': inline, 40 | 'sty-button-round': round 41 | }; 42 | const iconEl = loading ? ( 43 | 44 | ) : ( 45 | icon 46 | ); 47 | 48 | function onClick(event: React.MouseEvent) { 49 | if (loading || disabled) { 50 | return; 51 | } 52 | props.onClick(event); 53 | } 54 | return ( 55 |
56 | {!disabled && !loading && ripple && ( 57 | 58 | )} 59 | {typeof iconEl === 'string' ? ( 60 | 61 | ) : ( 62 | iconEl 63 | )} 64 | {children && {children}} 65 |
66 | ); 67 | } 68 | 69 | Button.defaultProps = { 70 | ripple: true, 71 | type: 'default', 72 | onClick: () => undefined 73 | }; 74 | 75 | export default Button; 76 | -------------------------------------------------------------------------------- /site/page/Radio/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Radio } from '@/components/index'; 3 | 4 | const RadioGroup = Radio.RadioGroup; 5 | const data = [ 6 | { label: '苹果', value: 'apple' }, 7 | { label: '香蕉', value: 'banana' }, 8 | { label: '芒果', value: 'mango' } 9 | ]; 10 | 11 | function RadioDemo() { 12 | const [value, setValue] = useState('apple'); 13 | function onChange(v) { 14 | console.log('选择了:', v); 15 | setValue(v); 16 | } 17 | return ( 18 |
19 |
基础用法
20 |
21 | 27 |
28 |
水平排列
29 |
30 | 35 |
36 |
禁用状态
37 |
38 | 44 |
45 |
自定义颜色
46 |
47 | 54 |
55 |
cell和受控
56 |
57 | 64 |
65 |
66 | ); 67 | } 68 | export default RadioDemo; 69 | -------------------------------------------------------------------------------- /site/page/PullRefresh/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { PullRefresh, Tabs, Toast } from '@/components/index'; 3 | import './index.less'; 4 | 5 | function PullRefreshDemo() { 6 | const [count, setCount] = useState(0); 7 | const [loading, setLoading] = useState(false); 8 | function onRefresh() { 9 | return new Promise((resolve, reject) => { 10 | setTimeout(() => { 11 | setCount(count + 1); 12 | resolve(); 13 | }, 2000); 14 | }); 15 | } 16 | function onRefresh2() { 17 | setLoading(true); 18 | setTimeout(() => { 19 | Toast.info({ content: '刷新成功', duration: 2 }); 20 | setCount(count + 1); 21 | setLoading(false); 22 | }, 2000); 23 | } 24 | return ( 25 |
26 | 27 | 28 | 29 |
30 |

下拉刷新次数{count}

31 |

当onRefresh返回Promise时,可不传loading

32 |
33 |
34 |
35 | 36 | 37 |
38 |

下拉刷新次数{count}

39 |

由自己控制loading

40 |
41 |
42 |
43 | 44 | 48 | } 49 | loosingContent={ 50 | 54 | } 55 | > 56 |
下拉刷新次数{count}
57 |
58 |
59 |
60 |
61 | ); 62 | } 63 | export default PullRefreshDemo; 64 | -------------------------------------------------------------------------------- /components/picker/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @pickerPrefixCls: sty-picker; 4 | 5 | .@{pickerPrefixCls} { 6 | position: relative; 7 | background-color: #fff; 8 | 9 | } 10 | 11 | .@{pickerPrefixCls}-panel { 12 | background-color: #fff; 13 | height: 264px; 14 | 15 | &-loading { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | width: 100%; 20 | height: 100%; 21 | background-color: rgba(255, 255, 255, 0.9); 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | z-index: 4; 26 | 27 | .sty-loading { 28 | color: #1989fa; 29 | } 30 | } 31 | 32 | &-columns { 33 | position: relative; 34 | display: flex; 35 | height: 100%; 36 | cursor: grab; 37 | } 38 | 39 | &-column { 40 | flex: 1; 41 | user-select: none; 42 | font-size: 16px; 43 | color: @base-text-color; 44 | overflow: hidden; 45 | 46 | ul { 47 | width: 100%; 48 | height: 100%; 49 | transition-timing-function: cubic-bezier(0.23, 1, 0.68, 1); 50 | touch-action: none; 51 | 52 | li { 53 | &.disabled { 54 | opacity: 0.5; 55 | } 56 | 57 | width: 100%; 58 | height: 44px; 59 | line-height: 44px; 60 | text-align: center; 61 | } 62 | } 63 | } 64 | 65 | &-mask { 66 | position: absolute; 67 | top: 0; 68 | left: 0; 69 | width: 100%; 70 | height: 100%; 71 | background-image: linear-gradient(180deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4)), linear-gradient(0deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4)); 72 | background-repeat: no-repeat; 73 | background-position: top, bottom; 74 | pointer-events: none; //这个属性非常重要,因为层叠属性,事件作用到的对象是这一层,使用了这个属性后,鼠标或手势会穿透或冒泡 75 | z-index: 2; 76 | 77 | } 78 | 79 | &-frame { 80 | position: absolute !important; 81 | top: 50%; 82 | right: 16px; 83 | left: 16px; 84 | height: 44px; 85 | transform: translateY(-50%); 86 | z-index: 3; 87 | pointer-events: none; 88 | 89 | &::after { 90 | border-color: #ddddddcc !important; 91 | } 92 | } 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /site/page/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Checkbox } from '@/components/index'; 3 | 4 | const CheckboxGroup = Checkbox.CheckboxGroup; 5 | 6 | const data = [ 7 | { label: '苹果', value: 'apple' }, 8 | { label: '香蕉', value: 'banana' }, 9 | { label: '芒果', value: 'mango', disabled: true } 10 | ]; 11 | 12 | const data2 = [ 13 | { label: '苹果', value: 'apple' }, 14 | { label: '香蕉', value: 'banana' }, 15 | { label: '芒果', value: 'mango' } 16 | ]; 17 | 18 | function CheckboxDemo() { 19 | const [list, setList] = useState([]); 20 | function onChange(arr) { 21 | console.log('arr: ', arr); 22 | setList(arr); 23 | } 24 | return ( 25 |
26 |
基础用法
27 |
28 | console.log(c ? '同意' : '不同意')}> 29 | 同意 30 | 31 |
32 |
禁用状态
33 |
34 | 35 |
36 |
自定义颜色
37 |
38 | 43 |
44 |
自定义形状
45 |
46 | 47 |
48 |
垂直排列
49 |
50 | 57 |
58 |
cell和受控
59 |
60 | 61 |
62 |
63 | ); 64 | } 65 | export default CheckboxDemo; 66 | -------------------------------------------------------------------------------- /components/date-picker/panels/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface HeaderProps { 4 | prefixCls?: string; 5 | prevIcon?: React.ReactNode; 6 | nextIcon?: React.ReactNode; 7 | superPrevIcon?: React.ReactNode; 8 | superNextIcon?: React.ReactNode; 9 | 10 | onPrev?: () => unknown; 11 | onNext?: () => unknown; 12 | onSuperPrev?: () => unknown; 13 | onSuperNext?: () => unknown; 14 | 15 | children?: React.ReactNode; 16 | } 17 | 18 | function Header(props: HeaderProps) { 19 | const { 20 | prefixCls: prefixClsProps, 21 | prevIcon, 22 | nextIcon, 23 | superPrevIcon, 24 | superNextIcon, 25 | onPrev, 26 | onNext, 27 | onSuperPrev, 28 | onSuperNext, 29 | children 30 | } = props; 31 | const prefixCls = `${prefixClsProps}-header`; 32 | return ( 33 |
34 | {onSuperPrev && ( 35 | 43 | )} 44 | {onPrev && ( 45 | 53 | )} 54 |
{children}
55 | {onNext && ( 56 | 64 | )} 65 | {onSuperNext && ( 66 | 74 | )} 75 |
76 | ); 77 | } 78 | Header.defaultProps = { 79 | prevIcon: '\u2039', 80 | nextIcon: '\u203A', 81 | superPrevIcon: '\u00AB', 82 | superNextIcon: '\u00BB' 83 | }; 84 | 85 | export default Header; 86 | -------------------------------------------------------------------------------- /components/nav-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | CSSProperties, 3 | ReactNode, 4 | useEffect, 5 | useRef, 6 | useState 7 | } from 'react'; 8 | import { classnames } from '../_utils/index'; 9 | import { Icon } from '../index'; 10 | import './index.less'; 11 | 12 | export interface NavBarProps { 13 | title?: ReactNode; // 标题 14 | left?: ReactNode; // 左边节点 15 | right?: ReactNode; // 右边节点 16 | border?: boolean; // 是否显示下边框 17 | fixed?: boolean; // 是否固定到顶部 18 | className?: string; 19 | style?: CSSProperties; 20 | zIndex?: number; 21 | leftArrow?: boolean; 22 | placeholder?: boolean; // 固定在顶部时,是否在标签位置生成一个等高的占位元素 23 | onClickLeft?: (event: React.MouseEvent) => void; 24 | onClickRight?: (event: React.MouseEvent) => void; 25 | } 26 | 27 | function NavBar(props: NavBarProps) { 28 | const { 29 | title, 30 | left, 31 | right, 32 | className, 33 | style, 34 | border, 35 | fixed, 36 | zIndex, 37 | leftArrow, 38 | placeholder 39 | } = props; 40 | 41 | const box = useRef(); 42 | const [height, setHeight] = useState(47); 43 | 44 | useEffect(() => { 45 | setHeight(box.current.offsetHeight); 46 | }, []); 47 | 48 | return ( 49 |
50 |
60 |
61 | {leftArrow && } 62 | {left} 63 |
64 |
{title}
65 |
66 | {right} 67 |
68 |
69 | {fixed && placeholder &&
} 70 |
71 | ); 72 | } 73 | 74 | NavBar.defaultProps = { 75 | title: '标题', 76 | onClickLeft: () => {}, 77 | onClickRight: () => {} 78 | }; 79 | 80 | export default NavBar; 81 | -------------------------------------------------------------------------------- /site/page/ActionSheet/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ActionSheet, Cell, Empty } from '@/components/index'; 3 | import { ActionSheetProps } from '@/components/action-sheet'; 4 | 5 | const base = { 6 | actions: [ 7 | { name: '选项一' }, 8 | { name: '选项二' }, 9 | { name: '选项三', subname: '副文本' } 10 | ] 11 | }; 12 | const status = { 13 | actions: [ 14 | { name: '选项', color: 'rgb(7, 193, 96)' }, 15 | { name: '选项', loading: true }, 16 | { name: '禁用选项', disabled: true } 17 | ] 18 | }; 19 | function ActionSheetDemo() { 20 | const [visible, setVisible] = useState(false); 21 | const [config, setConfig] = useState({}); 22 | return ( 23 |
24 |
基础用法
25 | { 29 | setVisible(true); 30 | setConfig({ ...base }); 31 | }} 32 | /> 33 | { 37 | setVisible(true); 38 | setConfig({ ...base, cancelText: '取消' }); 39 | }} 40 | /> 41 | { 45 | setVisible(true); 46 | setConfig({ ...base, description: '描述信息' }); 47 | }} 48 | /> 49 |
选项状态
50 | { 54 | setVisible(true); 55 | setConfig({ ...status, onSelect: console.log }); 56 | }} 57 | /> 58 |
自定义面板
59 | { 63 | setVisible(true); 64 | setConfig({ 65 | children: ( 66 |
67 | 68 |
69 | ), 70 | title: '标题' 71 | }); 72 | }} 73 | /> 74 | 75 | setVisible(false)} 78 | {...config} 79 | /> 80 |
81 | ); 82 | } 83 | export default ActionSheetDemo; 84 | -------------------------------------------------------------------------------- /components/cell-popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Cell, Popup } from '../index'; 3 | import './index.less'; 4 | 5 | export interface CellPopupProps { 6 | cellTitle?: React.ReactNode; // cell标题 7 | cellContent?: React.ReactNode; // cell内容 8 | popupTitle?: React.ReactNode; // popup标题 9 | okText?: React.ReactNode; // 确定按钮文字 10 | cancelText?: React.ReactNode; // 取消按钮文字 11 | children?: React.ReactNode; 12 | onOk?: () => unknown; // 确定按钮回调 13 | onCancel?: () => unknown; // 取消按钮回调 14 | onVisibleChange?: (visible: boolean) => unknown; // 当显隐状态变化时回调函数 15 | className?: string; 16 | style?: React.CSSProperties; 17 | } 18 | 19 | function CellPopup(props: CellPopupProps) { 20 | const { 21 | cellTitle, 22 | cellContent, 23 | popupTitle, 24 | okText, 25 | cancelText, 26 | children, 27 | onOk: onOkProps, 28 | onCancel: onCancelProps, 29 | onVisibleChange, 30 | className, 31 | style 32 | } = props; 33 | const [visible, setVisible] = useState(false); 34 | 35 | useEffect(() => { 36 | onVisibleChange(visible); 37 | }, [visible]); 38 | 39 | function onCancel() { 40 | onCancelProps(); 41 | setVisible(false); 42 | } 43 | 44 | function onOk() { 45 | onOkProps(); 46 | setVisible(false); 47 | } 48 | return ( 49 |
50 | setVisible(true)}> 51 | {cellContent} 52 | 53 | setVisible(false)} 56 | position='bottom' 57 | closable={false} 58 | > 59 |
60 |
61 | {cancelText} 62 |
63 |
{popupTitle}
64 |
65 | {okText} 66 |
67 |
68 | {children} 69 |
70 |
71 | ); 72 | } 73 | 74 | CellPopup.defaultProps = { 75 | okText: '确认', 76 | cancelText: '取消', 77 | onOk: () => undefined, 78 | onCancel: () => undefined, 79 | onVisibleChange: () => undefined 80 | }; 81 | export default CellPopup; 82 | -------------------------------------------------------------------------------- /components/timeline/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @timelinePrefixCls: sty-timeline; 4 | 5 | .@{timelinePrefixCls} { 6 | padding: 16px; 7 | 8 | &-item { 9 | display: flex; 10 | overflow: hidden; 11 | word-wrap: break-word; 12 | word-break: break-all; 13 | 14 | &-time { 15 | width: 40px; 16 | flex-shrink: 0; 17 | text-align: center; 18 | font-size: 14px; 19 | color: @timeline-time-color; 20 | padding-top: 15px; 21 | line-height: 1.5em; 22 | 23 | } 24 | 25 | &-line-box { 26 | position: relative; 27 | width: 50px; 28 | flex-shrink: 0; 29 | 30 | 31 | .@{timelinePrefixCls}-item-icon { 32 | position: absolute; 33 | top: 20px; 34 | left: 50%; 35 | width: 24px; 36 | height: 24px; 37 | transform: translateX(-50%); 38 | border-radius: 50%; 39 | text-align: center; 40 | background-color: #fff; 41 | color: @timeline-dot-color; 42 | 43 | img { 44 | width: 100%; 45 | height: 100%; 46 | } 47 | } 48 | 49 | .@{timelinePrefixCls}-item-line { 50 | position: absolute; 51 | top: 0; 52 | bottom: 0; 53 | left: 50%; 54 | width: 1px; 55 | height: 100%; 56 | background-color: @timeline-line-color; 57 | transform: translateX(-50%); 58 | } 59 | 60 | .@{timelinePrefixCls}-item-dot { 61 | position: absolute; 62 | top: 20px; 63 | left: 50%; 64 | width: 8px; 65 | height: 8px; 66 | transform: translateX(-50%); 67 | border-radius: 50%; 68 | background-color: @timeline-dot-color; 69 | 70 | &.active { 71 | opacity: 1; 72 | background-color: @base-color; 73 | border: solid 4px @timeline-dot-boder-color; 74 | background-clip: padding-box; 75 | } 76 | } 77 | 78 | } 79 | 80 | &-content { 81 | position: relative; 82 | flex: 1; 83 | padding-top: 15px; 84 | padding-bottom: 15px; 85 | color: @timeline-content-color; 86 | line-height: 1.5em; 87 | 88 | .@{timelinePrefixCls}-item:not(:last-child) &::after { 89 | border-bottom-width: 1px; 90 | } 91 | } 92 | 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /components/date-picker/panels/MonthPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../Header'; 3 | import PanelBody from '../PanelBody'; 4 | import locale from '../../generate/locale'; 5 | import { PanelSharedProps } from '../../interface'; 6 | import useCellClassName from '../../hooks/useCellClassName'; 7 | import { isSameMonth } from '../../_utils/dateUtils'; 8 | 9 | export type MonthPanelProps = PanelSharedProps; 10 | export const MONTH_COL_COUNT = 3; 11 | export const MONTH_ROW_COUNT = 4; 12 | 13 | function MonthPanel(props: MonthPanelProps) { 14 | const { 15 | prefixCls, 16 | generateConfig, 17 | value, 18 | viewDate, 19 | picker, 20 | onViewDateChange, 21 | onPanelChange, 22 | onSelect 23 | } = props; 24 | const baseMonth = generateConfig.setMonth(viewDate, 0); 25 | const getCellClassName = useCellClassName({ 26 | cellPrefixCls: `${prefixCls}-cell`, 27 | generateConfig, 28 | value, 29 | isSameCell: (current, target) => 30 | isSameMonth(generateConfig, current, target), 31 | isInView: () => true 32 | }); 33 | const onYearChange = (diff: number) => { 34 | const newDate = generateConfig.addYear(viewDate, diff); 35 | onViewDateChange(newDate); 36 | }; 37 | function onYearClick() { 38 | onPanelChange('year', viewDate); 39 | } 40 | return ( 41 |
42 |
onYearChange(1)} 45 | onSuperPrev={() => onYearChange(-1)} 46 | > 47 |
48 | {generateConfig.locale.format({ 49 | date: viewDate, 50 | format: locale.yearFormat 51 | })} 52 |
53 |
54 | 55 | {...props} 56 | rowNum={MONTH_ROW_COUNT} 57 | colNum={MONTH_COL_COUNT} 58 | baseDate={baseMonth} 59 | getCellDate={generateConfig.addMonth} 60 | getCellText={date => 61 | generateConfig.locale.getShortMonths()[generateConfig.getMonth(date)] 62 | } 63 | getCellClassName={getCellClassName} 64 | onSelect={date => { 65 | onSelect(date); 66 | picker !== 'month' && onPanelChange('date', date); 67 | }} 68 | /> 69 |
70 | ); 71 | } 72 | 73 | export default MonthPanel; 74 | -------------------------------------------------------------------------------- /components/dialog/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @dialogPrefixCls: sty-dialog; 4 | 5 | .@{dialogPrefixCls} { 6 | width: 270px; 7 | padding: 15px 0 0 0; 8 | border-radius: 7px; 9 | overflow-y: auto; 10 | will-change: transform; 11 | 12 | &-header { 13 | font-size: 18px; 14 | line-height: 1; 15 | color: #000; 16 | text-align: center; 17 | padding: 6px 15px 15px; 18 | } 19 | 20 | &-body { 21 | padding: 0 15px 15px; 22 | color: #646566; 23 | line-height: 20px; 24 | word-wrap: break-word; 25 | word-break: break-all; 26 | text-align: center; 27 | } 28 | 29 | &-footer { 30 | 31 | &&-h { 32 | display: flex; 33 | 34 | .@{dialogPrefixCls}-button { 35 | flex: 1; 36 | 37 | &::before { 38 | content: ''; 39 | position: absolute; 40 | top: 0; 41 | right: 0; 42 | width: 1px; 43 | height: 100%; 44 | transform: scaleX(.5); 45 | background-color: #ddd; 46 | } 47 | } 48 | 49 | } 50 | 51 | 52 | .@{dialogPrefixCls}-button, 53 | .sty-button { 54 | border-radius: 0; 55 | border: none; 56 | 57 | &::after { 58 | content: ''; 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | width: 100%; 63 | height: 1px; 64 | transform: scaleY(.5); 65 | background-color: #ddd; 66 | } 67 | } 68 | } 69 | } 70 | 71 | .sty-zoom-in{ 72 | animation: zoomIn 300ms; 73 | } 74 | 75 | .sty-zoom-out{ 76 | animation: zoomOut 300ms; 77 | } 78 | 79 | @keyframes zoomIn{ 80 | from{ 81 | opacity: 0; 82 | transform: translate3d(-50%, -50%, 0) scale(0.2, 0.2); //多写一个translate3d是因为transform会覆盖,translate3d一定要写在前面否则形变中心不对 83 | } 84 | to{ 85 | opacity: 1; 86 | transform: translate3d(-50%, -50%, 0) scale(1, 1); 87 | transition: opacity 300ms, transform 300ms; 88 | transition-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); 89 | } 90 | } 91 | 92 | @keyframes zoomOut{ 93 | from{ 94 | opacity: 1; 95 | transform: translate3d(-50%, -50%, 0) scale(1, 1); 96 | } 97 | to{ 98 | opacity: 0; 99 | transform: translate3d(-50%, -50%, 0) scale(0, 0); 100 | transition: opacity 300ms, transform 300ms; 101 | transition-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); 102 | } 103 | } 104 | 105 | 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sty-react-template", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "yarn dev", 8 | "dev": "webpack-dev-server --open --config ./config/webpack.dev.js", 9 | "build": "webpack --config ./config/webpack.prod.js", 10 | "analyz": "webpack-bundle-analyzer --port 8001 ./build/stats.json", 11 | "lint": "eslint site components --ext .ts,.tsx,.jsx" 12 | }, 13 | "husky": { 14 | "hooks": { 15 | "pre-commit": "yarn lint", 16 | "pre-push": "yarn lint" 17 | } 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.7.2", 21 | "@babel/plugin-proposal-class-properties": "^7.7.0", 22 | "@babel/plugin-proposal-decorators": "^7.10.5", 23 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 24 | "@babel/preset-react": "^7.7.0", 25 | "@types/react": "^16.9.49", 26 | "@types/react-router-config": "^5.0.1", 27 | "@typescript-eslint/eslint-plugin": "^4.1.1", 28 | "@typescript-eslint/parser": "^4.1.1", 29 | "babel-eslint": "^10.0.3", 30 | "babel-loader": "^8.0.6", 31 | "clean-webpack-plugin": "^3.0.0", 32 | "css-loader": "^3.2.0", 33 | "eslint": "^6.6.0", 34 | "eslint-config-standard": "^14.1.0", 35 | "eslint-loader": "^3.0.2", 36 | "eslint-plugin-import": "^2.18.2", 37 | "eslint-plugin-node": "^10.0.0", 38 | "eslint-plugin-prettier": "^3.1.4", 39 | "eslint-plugin-promise": "^4.2.1", 40 | "eslint-plugin-react": "^7.16.0", 41 | "eslint-plugin-standard": "^4.0.1", 42 | "file-loader": "^6.2.0", 43 | "html-webpack-plugin": "^3.2.0", 44 | "husky": "^4.0.0-beta.5", 45 | "less": "^3.12.2", 46 | "less-loader": "^7.0.1", 47 | "mini-css-extract-plugin": "^0.8.0", 48 | "optimize-css-assets-webpack-plugin": "^5.0.3", 49 | "prettier": "^2.1.2", 50 | "style-loader": "^1.0.0", 51 | "ts-loader": "^8.0.4", 52 | "typescript": "^4.0.3", 53 | "url-loader": "^4.1.1", 54 | "webpack": "^4.41.2", 55 | "webpack-bundle-analyzer": "^3.6.0", 56 | "webpack-cli": "^3.3.10", 57 | "webpack-dev-server": "^3.9.0", 58 | "webpack-merge": "^4.2.2" 59 | }, 60 | "dependencies": { 61 | "@types/react-router-dom": "^5.1.6", 62 | "@vant/touch-emulator": "^1.2.0", 63 | "dayjs": "^1.10.3", 64 | "react": "^16.11.0", 65 | "react-dom": "^16.11.0", 66 | "react-router-config": "^5.1.1", 67 | "react-router-dom": "^5.2.0", 68 | "react-transition-group": "^4.4.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /site/page/Tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Tabs } from '@/components/index'; 3 | import './index.less'; 4 | 5 | function TabsDemo() { 6 | const [active, setActive] = useState(0); 7 | return ( 8 |
9 |
基础用法
10 | 15 | 内容一 16 | 内容二 17 | 内容三 18 | 内容四 19 | 内容五 20 | 内容六 21 | 22 | 23 |
垂直样式
24 | 29 | 内容一 30 | 内容二 31 | 内容三 32 | 内容四 33 | 内容五 34 | 内容六 35 | 36 | 37 |
无动画
38 | setActive(index)} 42 | contentStyle={{ height: 150 }} 43 | > 44 | 内容一 45 | 内容二 46 | 内容三 47 | 48 | 49 |
样式配置
50 | setActive(index)} 55 | contentStyle={{ height: 150 }} 56 | > 57 | 内容一 58 | 内容二 59 | 内容三 60 | 内容四 61 | 内容五 62 | 内容六 63 | 64 |
65 | ); 66 | } 67 | export default TabsDemo; 68 | -------------------------------------------------------------------------------- /components/loading/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @loadingPrefixCls: sty-loading; 4 | 5 | .@{loadingPrefixCls} { 6 | position: relative; 7 | display: flex; 8 | flex-direction: row; 9 | justify-content: center; 10 | align-items: center; 11 | font-size: 0; //少了这个样式后,button的旋转一直有问题,找了一下午bug 12 | color: @loading-color; 13 | line-height: 0; 14 | 15 | &&-vertical{ 16 | flex-direction: column; 17 | } 18 | 19 | &-text{ 20 | display: inline-block; 21 | margin-left: 8px; 22 | font-size: 14px; 23 | vertical-align: middle; 24 | color: @loading-text-color; 25 | 26 | .@{loadingPrefixCls}-vertical &{ 27 | margin-top: 10px; 28 | } 29 | } 30 | 31 | &-spinner { 32 | display: inline-block; 33 | position: relative; 34 | width: 30px; 35 | vertical-align: middle; 36 | height: 30px; 37 | max-width: 100%; 38 | max-height: 100%; 39 | animation: sty-rotate 0.8s linear infinite; 40 | 41 | & i { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | height: 100%; 47 | 48 | &::before { 49 | display: block; 50 | width: 2px; 51 | height: 25%; 52 | margin: 0 auto; 53 | background-color: currentColor; 54 | border-radius: 40%; 55 | content: ' '; 56 | } 57 | } 58 | } 59 | 60 | &-type-circular { 61 | animation-duration: 2s; 62 | 63 | & circle { 64 | animation: sty-circular 1.5s ease-in-out infinite; 65 | stroke: currentColor; 66 | stroke-width: 3; 67 | stroke-linecap: round; 68 | } 69 | } 70 | &-type-spinner{ 71 | animation-timing-function: steps(12); 72 | 73 | } 74 | } 75 | 76 | @keyframes sty-rotate { 77 | form { 78 | transform: rotate(0deg); 79 | } 80 | 81 | to { 82 | transform: rotate(360deg); 83 | } 84 | } 85 | 86 | @keyframes sty-circular { 87 | 0% { 88 | stroke-dasharray: 1, 200; 89 | stroke-dashoffset: 0; 90 | } 91 | 92 | 50% { 93 | stroke-dasharray: 90, 150; 94 | stroke-dashoffset: -40; 95 | } 96 | 97 | 100% { 98 | stroke-dasharray: 90, 150; 99 | stroke-dashoffset: -120; 100 | } 101 | } 102 | 103 | 104 | .generate-spinner(@n, @i: 1) when (@i =< @n) { 105 | .@{loadingPrefixCls}-spinner i:nth-child(@{i}) { 106 | transform: rotate(@i * 30deg); 107 | opacity: 1 - (0.75 / 12) * (@i - 1); 108 | } 109 | 110 | .generate-spinner(@n, (@i + 1)); 111 | } 112 | 113 | .generate-spinner(12); 114 | -------------------------------------------------------------------------------- /components/date-picker/panels/YearPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../Header'; 3 | import PanelBody from '../PanelBody'; 4 | import { PanelSharedProps } from '../../interface'; 5 | import useCellClassName from '../../hooks/useCellClassName'; 6 | import { isSameYear } from '../../_utils/dateUtils'; 7 | 8 | export type YearPanelProps = PanelSharedProps; 9 | const YEAR_COL_COUNT = 3; 10 | const YEAR_ROW_COUNT = 4; 11 | const YEAR_DECADE_COUNT = 10; 12 | 13 | function YearPanel(props: YearPanelProps) { 14 | const { 15 | prefixCls, 16 | generateConfig, 17 | viewDate, 18 | value, 19 | picker, 20 | onViewDateChange, 21 | onPanelChange, 22 | onSelect 23 | } = props; 24 | 25 | const yearNumber = generateConfig.getYear(viewDate); 26 | const startYear = 27 | Math.floor(yearNumber / YEAR_DECADE_COUNT) * YEAR_DECADE_COUNT; 28 | const endYear = startYear + YEAR_DECADE_COUNT - 1; 29 | const baseYear = generateConfig.setYear( 30 | viewDate, 31 | startYear - 32 | Math.ceil((YEAR_COL_COUNT * YEAR_ROW_COUNT - YEAR_DECADE_COUNT) / 2) 33 | ); 34 | 35 | const isInView = (date: DateType) => { 36 | const currentYearNumber = generateConfig.getYear(date); 37 | return startYear <= currentYearNumber && currentYearNumber <= endYear; 38 | }; 39 | const getCellClassName = useCellClassName({ 40 | cellPrefixCls: `${prefixCls}-cell`, 41 | generateConfig, 42 | value, 43 | isSameCell: (current, target) => 44 | isSameYear(generateConfig, current, target), 45 | isInView 46 | }); 47 | 48 | const onDecadeChange = (diff: number) => { 49 | const newDate = generateConfig.addYear(viewDate, diff * 10); 50 | onViewDateChange(newDate); 51 | }; 52 | 53 | function onDecadeClick() { 54 | onPanelChange('decade', viewDate); 55 | } 56 | return ( 57 |
58 |
onDecadeChange(-1)} 61 | onSuperNext={() => onDecadeChange(1)} 62 | > 63 |
64 | {startYear}-{endYear} 65 |
66 |
67 | 68 | {...props} 69 | colNum={YEAR_COL_COUNT} 70 | rowNum={YEAR_ROW_COUNT} 71 | baseDate={baseYear} 72 | getCellDate={generateConfig.addYear} 73 | getCellText={generateConfig.getYear} 74 | getCellClassName={getCellClassName} 75 | onSelect={date => { 76 | onSelect(date); 77 | picker !== 'year' && onPanelChange('month', date); 78 | }} 79 | /> 80 |
81 | ); 82 | } 83 | 84 | export default YearPanel; 85 | -------------------------------------------------------------------------------- /components/date-picker/panels/PanelBody.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classnames } from '@/components/_utils'; 3 | import { PanelMode } from '../interface'; 4 | import { GenerateConfig } from '../generate'; 5 | import { getCellDateDisabled } from '../_utils/dateUtils'; 6 | 7 | export interface PanelBodyProps { 8 | prefixCls?: string; 9 | headerCells?: React.ReactNode; 10 | rowNum: number; // 行数 11 | colNum: number; // 列数 12 | baseDate: DateType; 13 | mode?: PanelMode; 14 | disabledDate?: (date: DateType) => boolean; 15 | getCellDate: (date: DateType, offset: number) => DateType; 16 | getCellText: (date: DateType) => React.ReactNode; 17 | getCellClassName: (date: DateType) => Record; 18 | onSelect: (value: DateType) => unknown; 19 | generateConfig: GenerateConfig; 20 | } 21 | 22 | function PanelBody(props: PanelBodyProps) { 23 | const { 24 | prefixCls: prefixClsProps, 25 | headerCells, 26 | rowNum, 27 | colNum, 28 | baseDate, 29 | mode, 30 | disabledDate, 31 | getCellDate, 32 | getCellText, 33 | getCellClassName, 34 | onSelect, 35 | generateConfig 36 | } = props; 37 | const rows: React.ReactNode[] = []; 38 | 39 | const prefixCls = `${prefixClsProps}-body`; 40 | const cellPrefixCls = `${prefixClsProps}-cell`; 41 | for (let i = 0; i < rowNum; i++) { 42 | const row: React.ReactNode[] = []; 43 | for (let j = 0; j < colNum; j++) { 44 | const offset = i * colNum + j; 45 | const currentDate = getCellDate(baseDate, offset); 46 | const cellText = getCellText(currentDate); 47 | 48 | const disabled = getCellDateDisabled({ 49 | cellDate: currentDate, 50 | mode, 51 | disabledDate, 52 | generateConfig 53 | }); 54 | 55 | row.push( 56 | { 64 | if (!disabled) { 65 | onSelect(currentDate); 66 | } 67 | }} 68 | > 69 |
{cellText}
70 | 71 | ); 72 | } 73 | rows.push({row}); 74 | } 75 | 76 | return ( 77 |
78 | 79 | {headerCells && ( 80 | 81 | {headerCells} 82 | 83 | )} 84 | {rows} 85 |
86 |
87 | ); 88 | } 89 | 90 | export default PanelBody; 91 | -------------------------------------------------------------------------------- /site/page/Toast/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Toast, Button } from '@/components/index'; 3 | import { ToastProps } from '@/components/toast'; 4 | import './index.less'; 5 | 6 | const config: Array = [ 7 | { 8 | content: '文字提示', 9 | onClose: () => console.log('onClose'), 10 | afterClose: () => console.log('afterClose') 11 | }, 12 | { content: '这是一条长文字提示,超过一定字数就会换行' }, 13 | { content: '加载中...', type: 'loading' }, 14 | { content: '成功文案', type: 'success' }, 15 | { content: '失败文案', type: 'fail' }, 16 | { content: '自定义图标', icon: 'like-o' }, 17 | { content: '自定义图标', icon: 'cart-o' } 18 | ]; 19 | function ToastDemo() { 20 | function showToast(param) { 21 | Toast[param.type || 'info'](param); 22 | } 23 | 24 | function showToast2() { 25 | let secondsToGo = 5; 26 | const { close, update } = Toast.info({ 27 | content: '5秒后手动销毁', 28 | duration: 0 29 | }); 30 | const timer = setInterval(() => { 31 | secondsToGo -= 1; 32 | update({ 33 | content: `${secondsToGo}秒后手动销毁` 34 | }); 35 | }, 1000); 36 | setTimeout(() => { 37 | clearInterval(timer); 38 | close(); 39 | }, 5000); 40 | } 41 | return ( 42 |
43 |
基础用法
44 |
45 | 48 | 51 |
52 | 53 |
加载提示
54 |
55 | 58 |
59 |
成功失败
60 |
61 | 64 | 67 |
68 |
自定义图标
69 |
70 | 73 | 76 |
77 |
手动销毁和更新
78 |
79 | 82 |
83 |
84 | ); 85 | } 86 | export default ToastDemo; 87 | -------------------------------------------------------------------------------- /site/page/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image } from '@/components/index'; 3 | import { ImageProps, ObjectFit } from '@/components/image'; 4 | import './index.less'; 5 | 6 | const img = require('./img/cat.jpeg').default; 7 | 8 | const fitList: Array = [ 9 | 'contain', 10 | 'cover', 11 | 'fill', 12 | 'none', 13 | 'scale-down' 14 | ]; 15 | const tips: Array<{ props: ImageProps; text: string }> = [ 16 | { 17 | props: {}, 18 | text: '加载中' 19 | }, 20 | { 21 | props: { src: 'img', onError: () => console.log('图片加载失败') }, 22 | text: '加载失败' 23 | } 24 | ]; 25 | 26 | const arr = new Array(10).fill(true); 27 | 28 | function ImageDemo() { 29 | return ( 30 |
31 |
基础用法
32 |
33 | console.log('加载成功')} /> 34 |
35 | 36 |
填充模式
37 |
38 | {fitList.map(item => ( 39 |
40 | 41 |
{item}
42 |
43 | ))} 44 |
45 | 46 |
图片懒加载
47 |
48 | {arr.map((item, index) => ( 49 | 50 | ))} 51 |
52 |
53 | {arr.map((item, index) => ( 54 | 60 | ))} 61 |
62 | 63 |
加载提示
64 |
65 | {tips.map(item => ( 66 |
67 | 68 |
{item.text}
69 |
70 | ))} 71 |
72 | 73 |
自定义提示
74 |
75 | 加载失败
} /> 76 |
77 | 78 |
圆形图片
79 |
80 |
81 | 82 |
round
83 |
84 |
85 | 86 |
自定义radius
87 |
88 |
89 |
90 | ); 91 | } 92 | export default ImageDemo; 93 | -------------------------------------------------------------------------------- /components/date-picker/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react'; 2 | import { CellPopup } from '../index'; 3 | import DatePanel from './panels'; 4 | import { DatePickerProps, PickerValue } from './interface'; 5 | import { Dayjs } from 'dayjs'; 6 | import { formatDate } from './_utils/dateUtils'; 7 | import './index.less'; 8 | 9 | function DatePicker(props: DatePickerProps) { 10 | const { 11 | cellTitle, 12 | children, 13 | picker, 14 | format, 15 | value, 16 | defaultValue, 17 | onCancel, 18 | onVisibleChange: onVisibleChangeProps, 19 | onOk: onOkProps, 20 | onChange: onChangeProps, 21 | className, 22 | style 23 | } = props; 24 | const [innerValue, setInnerValue] = useState>(); 25 | const [selectedValue, setSelectedValue] = useState>( 26 | defaultValue 27 | ); 28 | 29 | useEffect(() => { 30 | setSelectedValue(value); 31 | }, [value]); 32 | 33 | useEffect(() => { 34 | setInnerValue(selectedValue); 35 | }, [selectedValue]); 36 | 37 | const formatString = useMemo(() => { 38 | if (format) { 39 | return format; 40 | } 41 | switch (picker) { 42 | case 'month': 43 | return 'YYYY-MM'; 44 | case 'year': 45 | return 'YYYY'; 46 | default: 47 | return 'YYYY-MM-DD'; 48 | } 49 | }, [picker, format]); 50 | 51 | const valueText = useMemo(() => { 52 | if (Array.isArray(selectedValue)) { 53 | return ` 54 | ${formatDate({ 55 | date: selectedValue[0], 56 | format: formatString 57 | })} ~ 58 | ${formatDate({ 59 | date: selectedValue[0], 60 | format: formatString 61 | })}`; 62 | } 63 | return formatDate({ 64 | date: selectedValue, 65 | format: formatString 66 | }); 67 | }, [selectedValue]); 68 | 69 | function onChange(v) { 70 | setInnerValue(v); 71 | onChangeProps && onChangeProps(v); 72 | } 73 | 74 | function onVisibleChange(visible) { 75 | if (visible) { 76 | setInnerValue(selectedValue); 77 | } 78 | onVisibleChangeProps && onVisibleChangeProps(visible); 79 | } 80 | 81 | function onOk() { 82 | if (value === undefined) { 83 | setSelectedValue(innerValue); 84 | } 85 | onOkProps && onOkProps(innerValue); 86 | } 87 | return ( 88 | 97 | 98 | 99 | ); 100 | } 101 | export default DatePicker; 102 | -------------------------------------------------------------------------------- /components/date-picker/generate/index.ts: -------------------------------------------------------------------------------- 1 | import dayjs, { Dayjs } from 'dayjs'; 2 | import localeData from 'dayjs/plugin/localeData'; 3 | import 'dayjs/locale/zh-cn'; 4 | 5 | dayjs.extend(localeData); 6 | dayjs.locale('zh-cn'); 7 | 8 | function getLocale(locale = 'zh-cn', date?): Dayjs { 9 | return dayjs(date).locale(locale); 10 | } 11 | export type GenerateConfig = { 12 | // get 13 | getNow: () => DateType; 14 | getYear: (value: DateType) => number; 15 | getMonth: (value: DateType) => number; 16 | getDate: (value: DateType) => number; 17 | getWeekDay: (value: DateType) => number; 18 | getEndDate: (value: DateType) => DateType; 19 | getSecond: (value: DateType) => number; 20 | getMinute: (value: DateType) => number; 21 | getHour: (value: DateType) => number; 22 | // set 23 | addYear: (value: DateType, diff: number) => DateType; 24 | setYear: (value: DateType, year: number) => DateType; 25 | setMonth: (value: DateType, month: number) => DateType; 26 | addMonth: (value: DateType, diff: number) => DateType; 27 | addDate: (value: DateType, diff: number) => DateType; 28 | setDate: (value: DateType, date: number) => DateType; 29 | // Compare 30 | isAfter: (date1: DateType, date2: DateType) => boolean; 31 | 32 | locale: { 33 | format: (options: { 34 | locale?: string; 35 | date?: DateType; 36 | format?: string; 37 | }) => string; 38 | getShortMonths?: (locale?: string) => string[]; 39 | getWeekFirstDay: (locale?: string) => number; 40 | getShortWeekDays?: (locale?: string) => string[]; 41 | }; 42 | }; 43 | 44 | const generateConfig: GenerateConfig = { 45 | // get 46 | getNow: () => dayjs(), 47 | getYear: date => date.year(), 48 | getMonth: date => date.month(), 49 | getDate: date => date.date(), 50 | getWeekDay: date => { 51 | const clone = date.locale('en'); 52 | return clone.day() + clone.localeData().firstDayOfWeek(); 53 | }, 54 | getEndDate: date => date.endOf('month'), 55 | getHour: date => date.hour(), 56 | getMinute: date => date.minute(), 57 | getSecond: date => date.second(), 58 | // set 59 | addYear: (date, diff) => date.add(diff, 'year'), 60 | setYear: (date, year) => date.year(year), 61 | setMonth: (date, month) => date.month(month), 62 | addMonth: (date, diff) => date.add(diff, 'month'), 63 | addDate: (date, diff) => date.add(diff, 'day'), 64 | setDate: (date, num) => date.date(num), 65 | // Compare 66 | isAfter: (date1, date2) => date1.isAfter(date2), 67 | 68 | // locale 69 | locale: { 70 | format: ({ locale, date, format }) => 71 | getLocale(locale, date).format(format), 72 | getShortMonths: locale => getLocale(locale).localeData().monthsShort(), 73 | getWeekFirstDay: locale => getLocale(locale).localeData().firstDayOfWeek(), 74 | getShortWeekDays: locale => getLocale(locale).localeData().weekdaysMin() 75 | } 76 | }; 77 | 78 | export default generateConfig; 79 | -------------------------------------------------------------------------------- /components/switch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import * as CSS from 'csstype'; 4 | import { Loading, Cell } from '../index'; 5 | import './index.less'; 6 | 7 | export interface SwitchProps { 8 | checked?: boolean; 9 | defaultChecked?: boolean; 10 | disabled?: boolean; 11 | loading?: boolean; 12 | color?: CSS.Property.Color; 13 | size?: number; 14 | children?: React.ReactNode; 15 | cell?: boolean; // 是否开启cell模式 16 | onChange?: (checked: boolean) => unknown; 17 | className?: string; 18 | style?: React.CSSProperties; 19 | } 20 | 21 | function Switch(props: SwitchProps) { 22 | const { 23 | checked, 24 | defaultChecked, 25 | disabled, 26 | loading, 27 | color, 28 | size, 29 | cell, 30 | className, 31 | style = {} 32 | } = props; 33 | const [value, setValue] = useState(defaultChecked); 34 | 35 | useEffect(() => { 36 | if (typeof checked === 'boolean') { 37 | setValue(checked); 38 | } 39 | }, [checked]); 40 | 41 | function onChange(e: React.ChangeEvent | boolean) { 42 | if (loading || disabled) { 43 | return; 44 | } 45 | let inputChecked: boolean; 46 | if (typeof e === 'boolean') { 47 | inputChecked = e; 48 | } else { 49 | e.persist(); 50 | inputChecked = e.target.checked; 51 | } 52 | props.onChange(inputChecked); 53 | // 如果是非受控模式,内部处理 54 | if (checked === undefined) { 55 | setValue(inputChecked); 56 | } 57 | } 58 | 59 | const sty = style; 60 | if (value && color) { 61 | sty.backgroundColor = color; 62 | } 63 | if (size) { 64 | sty.fontSize = `${size}px`; 65 | } 66 | 67 | function onCellClick(event) { 68 | event.persist(); 69 | if (event.target.nodeName === 'DIV') { 70 | onChange(!value); 71 | } 72 | } 73 | 74 | if (cell) { 75 | return ( 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | return ( 83 | 103 | ); 104 | } 105 | 106 | Switch.defaultProps = { 107 | defaultChecked: false, 108 | disabled: false, 109 | loading: false, 110 | cell: false, 111 | onChange: () => undefined 112 | }; 113 | export default Switch; 114 | -------------------------------------------------------------------------------- /components/date-picker/panels/DecadePanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../Header'; 3 | import PanelBody from '../PanelBody'; 4 | import { PanelSharedProps } from '../../interface'; 5 | 6 | export type DecadePanelProps = PanelSharedProps; 7 | export const DECADE_UNIT_DIFF = 10; 8 | export const DECADE_DISTANCE_COUNT = DECADE_UNIT_DIFF * 10; 9 | export const DECADE_COL_COUNT = 3; 10 | export const DECADE_ROW_COUNT = 4; 11 | export const DECADE_UNIT_DIFF_DES = DECADE_UNIT_DIFF - 1; 12 | 13 | function DecadePanel(props: DecadePanelProps) { 14 | const { 15 | generateConfig, 16 | viewDate, 17 | prefixCls, 18 | onViewDateChange, 19 | onPanelChange, 20 | onSelect 21 | } = props; 22 | const cellPrefixCls = `${prefixCls}-cell`; 23 | 24 | const yearNumber = generateConfig.getYear(viewDate); 25 | const startYear = 26 | Math.floor(yearNumber / DECADE_DISTANCE_COUNT) * DECADE_DISTANCE_COUNT; 27 | const endYear = startYear + DECADE_DISTANCE_COUNT - 1; 28 | const decadeYearNumber = 29 | Math.floor(yearNumber / DECADE_UNIT_DIFF) * DECADE_UNIT_DIFF; 30 | 31 | const baseDecadeYear = generateConfig.setYear( 32 | viewDate, 33 | startYear - 34 | Math.ceil( 35 | (DECADE_COL_COUNT * DECADE_ROW_COUNT * DECADE_UNIT_DIFF - 36 | DECADE_DISTANCE_COUNT) / 37 | 2 38 | ) 39 | ); 40 | 41 | const getCellClassName = (date: DateType) => { 42 | const startDecadeNumber = generateConfig.getYear(date); 43 | const endDecadeNumber = startDecadeNumber + DECADE_UNIT_DIFF_DES; 44 | 45 | return { 46 | [`${cellPrefixCls}-in-view`]: 47 | startYear <= startDecadeNumber && endDecadeNumber <= endYear, 48 | [`${cellPrefixCls}-selected`]: startDecadeNumber === decadeYearNumber 49 | }; 50 | }; 51 | 52 | function onDecadesChange(diff: number) { 53 | const newDate = generateConfig.addYear( 54 | viewDate, 55 | diff * DECADE_DISTANCE_COUNT 56 | ); 57 | onViewDateChange(newDate); 58 | } 59 | return ( 60 |
61 |
onDecadesChange(1)} 64 | onSuperPrev={() => onDecadesChange(-1)} 65 | > 66 | {startYear} - {endYear} 67 |
68 | 69 | {...props} 70 | rowNum={DECADE_ROW_COUNT} 71 | colNum={DECADE_COL_COUNT} 72 | baseDate={baseDecadeYear} 73 | getCellText={date => { 74 | const startDecadeNumber = generateConfig.getYear(date); 75 | return `${startDecadeNumber}-${ 76 | startDecadeNumber + DECADE_UNIT_DIFF_DES 77 | }`; 78 | }} 79 | getCellDate={(date, offset) => 80 | generateConfig.addYear(date, offset * DECADE_UNIT_DIFF) 81 | } 82 | getCellClassName={getCellClassName} 83 | onSelect={date => { 84 | onSelect(date); 85 | onPanelChange('year', date); 86 | }} 87 | /> 88 |
89 | ); 90 | } 91 | 92 | export default DecadePanel; 93 | -------------------------------------------------------------------------------- /components/timeline/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import { Icon } from '../index'; 4 | import './index.less'; 5 | 6 | export interface TimelineProps { 7 | showTime?: boolean; 8 | className?: string; 9 | children?: React.ReactNode; 10 | activeIndex?: boolean; 11 | style?: React.CSSProperties; 12 | } 13 | 14 | function Timeline(props: TimelineProps) { 15 | const { showTime, activeIndex, className, style, ...other } = props; 16 | 17 | function renderItem() { 18 | const { children, showTime } = props; 19 | const length = React.Children.count(children); 20 | return React.Children.map(children, (child: React.ReactElement, index) => { 21 | if (child.type === Item) { 22 | return React.cloneElement(child, { 23 | ...child.props, 24 | showTime, 25 | index, 26 | length, 27 | activeIndex 28 | }); 29 | } 30 | }); 31 | } 32 | 33 | return ( 34 |
35 | {renderItem()} 36 |
37 | ); 38 | } 39 | Timeline.defaultProps = { 40 | showTime: true 41 | }; 42 | 43 | export interface ItemProps { 44 | showTime?: boolean; 45 | index?: number; 46 | length?: number; // 子节点总长度 47 | icon?: any; 48 | children?: React.ReactNode; 49 | active?: boolean; 50 | time?: string; // YYYY-MM-DD时间 51 | activeIndex?: number; 52 | className?: string; 53 | style?: React.CSSProperties; 54 | } 55 | function Item(props: ItemProps) { 56 | let { 57 | showTime, 58 | index, 59 | length, 60 | icon, 61 | className, 62 | children, 63 | time, 64 | active, 65 | activeIndex, 66 | ...other 67 | } = props; 68 | const isLast = index === length - 1; 69 | const lineSty = { 70 | top: index === 0 ? 24 : 0, 71 | height: isLast ? 20 : '100%' 72 | }; 73 | if (typeof icon === 'string') { 74 | icon = ; 75 | } 76 | const times = time.split('-'); 77 | return ( 78 |
79 | {showTime && ( 80 |
81 |
{times[0]}
82 |
83 | {times[1]}-{times[2]} 84 |
85 |
86 | )} 87 |
88 |
89 | {icon ? ( 90 |
{icon}
91 | ) : ( 92 |
99 | )} 100 |
101 |
{children}
102 |
103 | ); 104 | } 105 | 106 | Timeline.Item = Item; 107 | export default Timeline; 108 | -------------------------------------------------------------------------------- /components/toast/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import { Loading, Overlay } from '../index'; 4 | import { OverlayProps } from '../overlay'; 5 | import './index.less'; 6 | import Icon from '../icon'; 7 | 8 | export interface ToastProps extends OverlayProps { 9 | type?: 'info' | 'success' | 'fail' | 'loading'; 10 | duration?: number; // 显示时间 11 | content?: React.ReactNode; 12 | icon?: React.ReactNode; // 自定义图标 13 | className?: string; 14 | style?: React.CSSProperties; 15 | } 16 | 17 | function Toast(props: ToastProps) { 18 | const { 19 | visible: visibleProps, 20 | type, 21 | duration, 22 | content, 23 | icon, 24 | className, 25 | style, 26 | ...other 27 | } = props; 28 | const [visible, setVisible] = useState(true); 29 | const box = useRef(); 30 | 31 | useEffect(() => { 32 | setVisible(true); 33 | if (duration) { 34 | setTimeout(() => { 35 | props.onClose(); 36 | setVisible(false); 37 | }, duration * 1000); 38 | } 39 | }, []); 40 | 41 | useEffect(() => { 42 | if (typeof visibleProps === 'boolean') { 43 | setVisible(visibleProps); 44 | } 45 | }, [visibleProps]); 46 | function renderIcon() { 47 | if (type === 'info' && !icon) { 48 | return; 49 | } 50 | if (type === 'loading') { 51 | return ; 52 | } 53 | if (typeof icon === 'string') { 54 | return ; 55 | } else if (icon) { 56 | return icon; 57 | } 58 | return ; 59 | } 60 | 61 | return ( 62 | 68 |
78 | {renderIcon()} 79 |
{content}
80 |
81 |
82 | ); 83 | } 84 | 85 | Toast.defaultProps = { 86 | duration: 2, 87 | type: 'info', 88 | onClose: () => undefined 89 | }; 90 | 91 | // 返回关闭函数和更新函数 92 | function notice(config: ToastProps) { 93 | return Overlay.show(config, Toast); 94 | } 95 | 96 | export default { 97 | info(config: ToastProps) { 98 | return notice({ 99 | ...config, 100 | type: 'info' 101 | }); 102 | }, 103 | loading(config: ToastProps) { 104 | return notice({ 105 | ...config, 106 | type: 'loading' 107 | }); 108 | }, 109 | fail(config: ToastProps) { 110 | return notice({ 111 | ...config, 112 | type: 'fail' 113 | }); 114 | }, 115 | success(config: ToastProps) { 116 | return notice({ 117 | ...config, 118 | type: 'success' 119 | }); 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /components/checkbox/Group.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import Checkbox, { OptionObjType, OptionValueType, GroupProps } from './index'; 4 | import './index.less'; 5 | 6 | function Group(props: GroupProps) { 7 | const { 8 | type, 9 | value, 10 | defaultValue, 11 | options, 12 | direction: directionProps, 13 | shape, 14 | color, 15 | disabled, 16 | cell, 17 | iconAlign, 18 | className, 19 | style 20 | } = props; 21 | 22 | const [list, setList] = useState>(defaultValue); 23 | let direction = directionProps; 24 | 25 | if (cell) { 26 | direction = 'vertical'; 27 | } 28 | 29 | useEffect(() => { 30 | setList(Array.isArray(value) ? value : []); 31 | }, [value]); 32 | 33 | const newOptions: Array> = useMemo(() => { 34 | if (Array.isArray(options)) { 35 | return options.map(option => { 36 | if (typeof option === 'string' || typeof option === 'number') { 37 | return { 38 | label: option, 39 | value: option 40 | }; 41 | } 42 | return option as OptionObjType; 43 | }); 44 | } 45 | return []; 46 | }, [options]); 47 | 48 | function onChange(isChecked: boolean, option: OptionObjType) { 49 | const valueList = new Set(list); 50 | if (isChecked) { 51 | valueList.add(option.value); 52 | } else { 53 | valueList.delete(option.value); 54 | } 55 | const newList = Array.from(valueList); 56 | const optionList = newList.reduce((total, cur) => { 57 | total.push(newOptions.find(i => i.value === cur)); 58 | return total; 59 | }, []); 60 | props.onChange(newList, optionList); 61 | if (value === undefined) { 62 | setList(newList); 63 | } 64 | } 65 | 66 | return ( 67 |
75 | {newOptions.map(option => ( 76 | onChange(isChecked, option)} 86 | disabled={ 87 | typeof option.disabled === 'boolean' ? option.disabled : disabled 88 | } 89 | cell={cell} 90 | > 91 | {option.label} 92 | 93 | ))} 94 |
95 | ); 96 | } 97 | 98 | Group.defaultProps = { 99 | type: 'checkbox', 100 | defaultValue: [], 101 | options: [], 102 | direction: 'horizontal', 103 | shape: 'square', 104 | iconAlign: 'right', 105 | disabled: false, 106 | onChange: () => undefined 107 | }; 108 | export default Group; 109 | -------------------------------------------------------------------------------- /components/date-picker/panels/DatePanel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../Header'; 3 | import PanelBody from '../PanelBody'; 4 | import { PanelSharedProps } from '../../interface'; 5 | import useCellClassName from '../../hooks/useCellClassName'; 6 | import { 7 | isSameDate, 8 | isSameMonth, 9 | getWeekStartDate 10 | } from '../../_utils/dateUtils'; 11 | import locale from '../../generate/locale'; 12 | 13 | export type DatePanelProps = PanelSharedProps; 14 | export const DATE_ROW_COUNT = 6; 15 | export const WEEK_DAY_COUNT = 7; 16 | 17 | function DatePanel(props: DatePanelProps) { 18 | const { 19 | generateConfig, 20 | prefixCls, 21 | value, 22 | viewDate, 23 | onViewDateChange, 24 | onPanelChange 25 | } = props; 26 | const baseDate = getWeekStartDate(undefined, generateConfig, viewDate); 27 | 28 | const headerCells: React.ReactNode[] = []; 29 | const weekFirstDay = generateConfig.locale.getWeekFirstDay(); 30 | const weekDaysLocale: string[] = generateConfig.locale.getShortWeekDays(); 31 | for (let i = 0; i < WEEK_DAY_COUNT; i += 1) { 32 | headerCells.push( 33 | {weekDaysLocale[(i + weekFirstDay) % WEEK_DAY_COUNT]} 34 | ); 35 | } 36 | const getCellClassName = useCellClassName({ 37 | cellPrefixCls: `${prefixCls}-cell`, 38 | generateConfig, 39 | value, 40 | today: generateConfig.getNow(), 41 | isSameCell: (current, target) => 42 | isSameDate(generateConfig, current, target), 43 | isInView: date => isSameMonth(generateConfig, date, viewDate) 44 | }); 45 | 46 | const onYearChange = (diff: number) => { 47 | const newDate = generateConfig.addYear(viewDate, diff); 48 | onViewDateChange(newDate); 49 | }; 50 | const onMonthChange = (diff: number) => { 51 | const newDate = generateConfig.addMonth(viewDate, diff); 52 | onViewDateChange(newDate); 53 | }; 54 | function onYearClick() { 55 | onPanelChange('year', viewDate); 56 | } 57 | function onMonthClick() { 58 | onPanelChange('month', viewDate); 59 | } 60 | return ( 61 |
62 |
onYearChange(-1)} 65 | onSuperNext={() => onYearChange(1)} 66 | onPrev={() => onMonthChange(-1)} 67 | onNext={() => onMonthChange(1)} 68 | > 69 |
70 | 71 | {generateConfig.locale.format({ 72 | date: viewDate, 73 | format: locale.yearFormat 74 | })} 75 | 76 |   77 | 78 | {generateConfig.locale.format({ 79 | date: viewDate, 80 | format: locale.monthFormat 81 | })} 82 | 83 |
84 |
85 | 86 | {...props} 87 | rowNum={DATE_ROW_COUNT} 88 | colNum={WEEK_DAY_COUNT} 89 | baseDate={baseDate} 90 | getCellDate={generateConfig.addDate} 91 | getCellText={generateConfig.getDate} 92 | getCellClassName={getCellClassName} 93 | headerCells={headerCells} 94 | /> 95 |
96 | ); 97 | } 98 | export default DatePanel; 99 | -------------------------------------------------------------------------------- /components/ripple/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { classnames, range } from '../_utils/index'; 3 | import * as CSS from 'csstype'; 4 | import './index.less'; 5 | 6 | /** 7 | * 水波纹效果 8 | * 原理:在鼠标点击的位置生成一个div,div进行扩散动画,动画结束后删除div。 9 | */ 10 | export interface RippleProps { 11 | center?: boolean; // 是否中心点触发波纹 12 | color?: CSS.Property.Color; // 波纹颜色 13 | className?: string; 14 | style?: React.CSSProperties; 15 | } 16 | 17 | interface IPoint { 18 | top: number; 19 | left: number; 20 | width: number; 21 | height: number; 22 | } 23 | 24 | function Ripple(props: RippleProps) { 25 | const { center, color, style, className } = props; 26 | const rippleBox = useRef(); 27 | const shadowDiv = useRef(); 28 | const point = useRef(); 29 | const timer = useRef(); 30 | 31 | function onTouchStart(event: React.TouchEvent) { 32 | shadowDiv.current && 33 | shadowDiv.current.parentNode && 34 | shadowDiv.current.parentNode.removeChild(shadowDiv.current); 35 | clearTimeout(timer.current); 36 | 37 | point.current = getPoint(event.touches[0]); 38 | shadowDiv.current = createShadowDiv(point.current); 39 | rippleBox.current.appendChild(shadowDiv.current); 40 | } 41 | 42 | function onTouchEnd() { 43 | const { width, height } = point.current; 44 | const max = Math.max(height, width); 45 | const duration = range(max / 400, 0.6, 2); // [0.6,2] 46 | 47 | shadowDiv.current.style.transition = `transform ${duration}s ease-in-out 0s, opacity ${ 48 | duration - 0.3 49 | }s linear 0s`; 50 | shadowDiv.current.style.transform = `scale(${max / 5})`; 51 | shadowDiv.current.style.opacity = '0'; 52 | 53 | timer.current = window.setTimeout(() => { 54 | shadowDiv.current && 55 | shadowDiv.current.parentNode && 56 | shadowDiv.current.parentNode.removeChild(shadowDiv.current); 57 | }, duration * 1000); 58 | } 59 | // 创建阴影div 60 | function createShadowDiv(position: IPoint) { 61 | const div = document.createElement('div'); 62 | div.classList.add('sty-ripple-shadow'); 63 | 64 | div.style.cssText = ` 65 | top:${position.top}px; 66 | left:${position.left}px; 67 | transition:transform 0.25s ease-in-out 0s; 68 | transform:scale(1.4); 69 | background-color:${color} 70 | `; 71 | return div; 72 | } 73 | 74 | // 获取触摸点 75 | function getPoint(touch: React.Touch): IPoint { 76 | const rippleBoxRect = rippleBox.current.getBoundingClientRect(); 77 | if (center) { 78 | return { 79 | top: rippleBoxRect.height / 2, 80 | left: rippleBoxRect.width / 2, 81 | width: rippleBoxRect.width, 82 | height: rippleBoxRect.height 83 | }; 84 | } 85 | return { 86 | top: touch.clientY - rippleBoxRect.top, 87 | left: touch.clientX - rippleBoxRect.left, 88 | width: rippleBoxRect.width, 89 | height: rippleBoxRect.height 90 | }; 91 | } 92 | return ( 93 |
100 | ); 101 | } 102 | 103 | export default Ripple; 104 | -------------------------------------------------------------------------------- /components/empty/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import './index.less'; 4 | 5 | export interface EmptyProps { 6 | description?: React.ReactNode; 7 | className?: string; 8 | style?: React.CSSProperties; 9 | } 10 | 11 | function Empty(props: EmptyProps) { 12 | const { description, className, ...other } = props; 13 | return ( 14 |
15 |
16 | 23 | 24 | 25 | 32 | 36 | 41 | 45 | 49 | 50 | 54 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
{description}
65 |
66 | ); 67 | } 68 | 69 | export default Empty; 70 | -------------------------------------------------------------------------------- /docs/static/js/13.4963ed6a.chunk.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"] = window["webpackJsonp"] || []).push([[13],{ 2 | 3 | /***/ "./site/page/NavBar/index.tsx": 4 | /*!************************************!*\ 5 | !*** ./site/page/NavBar/index.tsx ***! 6 | \************************************/ 7 | /*! exports provided: default */ 8 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 9 | 10 | "use strict"; 11 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/_react@16.14.0@react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _components_index__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @/components/index */ \"./components/index.ts\");\n\n\nfunction NavBarDemo() {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", {\n className: 'demo-block__title'\n }, \"\\u57FA\\u7840\\u7528\\u6CD5\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"NavBar\"], {\n title: '\\u6807\\u9898',\n border: true,\n right: '\\u53F3\\u8FB9',\n leftArrow: true\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", {\n className: 'demo-block__title'\n }, \"\\u4F7F\\u7528\\u4E8B\\u4EF6\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"NavBar\"], {\n title: '\\u6807\\u9898',\n border: true,\n right: /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"Icon\"], {\n type: 'search',\n size: 18\n }),\n leftArrow: true,\n onClickLeft: () => console.log('click left'),\n onClickRight: () => console.log('click right')\n }));\n}\n/* harmony default export */ __webpack_exports__[\"default\"] = (NavBarDemo);//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zaXRlL3BhZ2UvTmF2QmFyL2luZGV4LnRzeC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy8uL3NpdGUvcGFnZS9OYXZCYXIvaW5kZXgudHN4PzU3MWUiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0JztcbmltcG9ydCB7IE5hdkJhciwgSWNvbiB9IGZyb20gJ0AvY29tcG9uZW50cy9pbmRleCc7XG5cbmZ1bmN0aW9uIE5hdkJhckRlbW8oKSB7XG4gIHJldHVybiAoXG4gICAgPGRpdj5cbiAgICAgIDxkaXYgY2xhc3NOYW1lPSdkZW1vLWJsb2NrX190aXRsZSc+5Z+656GA55So5rOVPC9kaXY+XG4gICAgICA8TmF2QmFyIHRpdGxlPSfmoIfpopgnIGJvcmRlciByaWdodD0n5Y+z6L65JyBsZWZ0QXJyb3cgLz5cblxuICAgICAgPGRpdiBjbGFzc05hbWU9J2RlbW8tYmxvY2tfX3RpdGxlJz7kvb/nlKjkuovku7Y8L2Rpdj5cbiAgICAgIDxOYXZCYXJcbiAgICAgICAgdGl0bGU9J+agh+mimCdcbiAgICAgICAgYm9yZGVyXG4gICAgICAgIHJpZ2h0PXs8SWNvbiB0eXBlPSdzZWFyY2gnIHNpemU9ezE4fSAvPn1cbiAgICAgICAgbGVmdEFycm93XG4gICAgICAgIG9uQ2xpY2tMZWZ0PXsoKSA9PiBjb25zb2xlLmxvZygnY2xpY2sgbGVmdCcpfVxuICAgICAgICBvbkNsaWNrUmlnaHQ9eygpID0+IGNvbnNvbGUubG9nKCdjbGljayByaWdodCcpfVxuICAgICAgLz5cbiAgICA8L2Rpdj5cbiAgKTtcbn1cbmV4cG9ydCBkZWZhdWx0IE5hdkJhckRlbW87XG4iXSwibWFwcGluZ3MiOiJBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFDQTtBQUVBO0FBQ0E7QUFFQTtBQUFBO0FBQ0E7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUVBO0FBQUE7QUFFQTtBQUNBO0FBQ0E7QUFBQTtBQUFBO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUlBO0FBQ0EiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./site/page/NavBar/index.tsx\n"); 12 | 13 | /***/ }) 14 | 15 | }]); -------------------------------------------------------------------------------- /components/action-sheet/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import Popup, { PopupProps } from '../popup'; 4 | import { Icon, Button, Empty } from '../index'; 5 | import * as CSS from 'csstype'; 6 | import './index.less'; 7 | 8 | export interface ActionSheetProps extends PopupProps { 9 | title?: React.ReactNode; // 顶部标题 10 | description?: React.ReactNode; // 选项上方的描述信息 11 | cancelText?: React.ReactNode; // 取消按钮文字 12 | onSelect?: (index: number, action: ActionType) => unknown; // 选中的回调函数 13 | actions?: Array; // 面板选项列表 14 | } 15 | 16 | export interface ActionType { 17 | name?: React.ReactNode; // 标题 18 | subname?: React.ReactNode; // 二级标题 19 | color?: CSS.Property.Color; // 选项文字颜色 20 | loading?: boolean; 21 | disabled?: boolean; 22 | className?: string; 23 | style?: React.CSSProperties; 24 | } 25 | 26 | const prefixCls = 'sty-actionSheet'; 27 | 28 | function ActionSheet(props: ActionSheetProps) { 29 | const { 30 | title, 31 | description, 32 | cancelText, 33 | actions, 34 | onSelect, 35 | children, 36 | ...other 37 | } = props; 38 | 39 | function renderHeader() { 40 | if (title) { 41 | return ( 42 |
43 | {title} 44 | 45 |
46 | ); 47 | } 48 | } 49 | function renderDescription() { 50 | if (description) { 51 | return
{description}
; 52 | } 53 | } 54 | function renderActions() { 55 | if (Array.isArray(actions) && actions.length > 0) { 56 | return actions.map((item, index) => ( 57 | 85 | )); 86 | } 87 | return !children && ; 88 | } 89 | function renderCancel() { 90 | if (cancelText) { 91 | return ( 92 |
93 | 96 |
97 | ); 98 | } 99 | } 100 | return ( 101 | 102 | {renderHeader()} 103 | {renderDescription()} 104 | {renderActions()} 105 | {children} 106 | {renderCancel()} 107 | 108 | ); 109 | } 110 | ActionSheet.defaultProps = { 111 | position: 'bottom', 112 | round: true, 113 | onSelect: () => undefined 114 | }; 115 | export default ActionSheet; 116 | -------------------------------------------------------------------------------- /docs/static/js/17.cd451688.chunk.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"] = window["webpackJsonp"] || []).push([[17],{ 2 | 3 | /***/ "./site/page/Ripple/index.tsx": 4 | /*!************************************!*\ 5 | !*** ./site/page/Ripple/index.tsx ***! 6 | \************************************/ 7 | /*! exports provided: default */ 8 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 9 | 10 | "use strict"; 11 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/_react@16.14.0@react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _components_index__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @/components/index */ \"./components/index.ts\");\n\n\nfunction RippleDemo() {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", {\n className: 'demo-block__title'\n }, \"\\u57FA\\u7840\\u7528\\u6CD5\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"Ripple\"], {\n style: {\n height: 100,\n background: '#fff'\n }\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", {\n className: 'demo-block__title'\n }, \"\\u989C\\u8272\\u6CE2\\u7EB9\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"Ripple\"], {\n style: {\n height: 100,\n background: '#fff'\n },\n color: 'skyblue'\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", {\n className: 'demo-block__title'\n }, \"\\u4E2D\\u5FC3\\u6CE2\\u7EB9\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"Ripple\"], {\n style: {\n height: 100,\n background: '#fff'\n },\n center: true\n }));\n}\n/* harmony default export */ __webpack_exports__[\"default\"] = (RippleDemo);//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zaXRlL3BhZ2UvUmlwcGxlL2luZGV4LnRzeC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy8uL3NpdGUvcGFnZS9SaXBwbGUvaW5kZXgudHN4PzRlNjAiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0JztcbmltcG9ydCB7IFJpcHBsZSB9IGZyb20gJ0AvY29tcG9uZW50cy9pbmRleCc7XG5cbmZ1bmN0aW9uIFJpcHBsZURlbW8oKSB7XG4gIHJldHVybiAoXG4gICAgPGRpdj5cbiAgICAgIDxkaXYgY2xhc3NOYW1lPSdkZW1vLWJsb2NrX190aXRsZSc+5Z+656GA55So5rOVPC9kaXY+XG4gICAgICA8UmlwcGxlIHN0eWxlPXt7IGhlaWdodDogMTAwLCBiYWNrZ3JvdW5kOiAnI2ZmZicgfX0gLz5cblxuICAgICAgPGRpdiBjbGFzc05hbWU9J2RlbW8tYmxvY2tfX3RpdGxlJz7popzoibLms6Lnurk8L2Rpdj5cbiAgICAgIDxSaXBwbGUgc3R5bGU9e3sgaGVpZ2h0OiAxMDAsIGJhY2tncm91bmQ6ICcjZmZmJyB9fSBjb2xvcj0nc2t5Ymx1ZScgLz5cblxuICAgICAgPGRpdiBjbGFzc05hbWU9J2RlbW8tYmxvY2tfX3RpdGxlJz7kuK3lv4Pms6Lnurk8L2Rpdj5cbiAgICAgIDxSaXBwbGUgc3R5bGU9e3sgaGVpZ2h0OiAxMDAsIGJhY2tncm91bmQ6ICcjZmZmJyB9fSBjZW50ZXIgLz5cbiAgICA8L2Rpdj5cbiAgKTtcbn1cbmV4cG9ydCBkZWZhdWx0IFJpcHBsZURlbW87XG4iXSwibWFwcGluZ3MiOiJBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFDQTtBQUVBO0FBQ0E7QUFFQTtBQUFBO0FBQ0E7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUVBO0FBQUE7QUFDQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFFQTtBQUFBO0FBQ0E7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBR0E7QUFDQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./site/page/Ripple/index.tsx\n"); 12 | 13 | /***/ }) 14 | 15 | }]); -------------------------------------------------------------------------------- /components/picker/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { CheckboxOptionType, OptionObjType } from '../checkbox'; 3 | import { CellPopup } from '../index'; 4 | import PickerPanel from './PickerPanel'; 5 | import { CellPopupProps } from '../cell-popup'; 6 | import './index.less'; 7 | 8 | const prefixCls = 'sty-picker'; 9 | 10 | export type PickerValueType = Array; 11 | export type ColumnOptions = Array; 12 | export interface CascadeType extends OptionObjType { 13 | children?: Array; 14 | } 15 | export type PickerDataSourceType = 16 | | Array 17 | | ColumnOptions 18 | | Array; 19 | 20 | export interface PickerPanelProps { 21 | value?: PickerValueType; 22 | defaultValue?: PickerValueType; 23 | dataSource?: PickerDataSourceType; 24 | loading?: boolean; 25 | onChange?: (val: PickerValueType, index: number) => unknown; 26 | visible?: boolean; // 这个属性的作用就是当在弹窗里使用picker,弹出层显示时需要重新计算高度,默认为true 27 | className?: string; 28 | style?: React.CSSProperties; 29 | } 30 | 31 | export interface PickerColumnProps { 32 | list?: ColumnOptions; 33 | wrapHeight?: number; 34 | value?: string | number; 35 | onChange?: (selected: string | number) => unknown; 36 | } 37 | 38 | export interface PickerProps 39 | extends PickerPanelProps, 40 | Omit { 41 | title?: React.ReactNode; // 标题 42 | children?: React.ReactNode; 43 | onOk?: (val: PickerValueType) => unknown; // 确定按钮回调 44 | onCancel?: () => unknown; // 取消按钮回调 45 | onVisibleChange?: (visible: boolean) => unknown; // 当显隐状态变化时回调函数 46 | } 47 | 48 | function Picker(props: PickerProps) { 49 | const { 50 | value: valueProps, 51 | defaultValue: defaultValueProps, 52 | onChange: onPropsChange, 53 | cellTitle, 54 | cellContent, 55 | popupTitle, 56 | title, 57 | okText, 58 | cancelText, 59 | children, 60 | onOk: onOkProps, 61 | onCancel: onCancelProps, 62 | onVisibleChange: onVisibleChangeProps, 63 | ...other 64 | } = props; 65 | const [value, setValue] = useState(defaultValueProps || []); 66 | const [viewValue, setViewValue] = useState( 67 | defaultValueProps || [] 68 | ); 69 | const [visible, setVisible] = useState(false); 70 | 71 | useEffect(() => { 72 | if (Array.isArray(valueProps)) { 73 | setViewValue(valueProps); 74 | } 75 | }, [valueProps]); 76 | 77 | function onCancel() { 78 | onCancelProps(); 79 | setValue(viewValue); 80 | } 81 | 82 | function onOk() { 83 | onOkProps(value); 84 | setViewValue(value); 85 | } 86 | function onVisibleChange(v) { 87 | onVisibleChangeProps(v); 88 | setVisible(v); 89 | } 90 | function onChange(v, index) { 91 | onPropsChange(v, index); 92 | setValue(v); 93 | } 94 | return ( 95 | 104 | 110 | 111 | ); 112 | } 113 | 114 | Picker.defaultProps = { 115 | okText: '确定', 116 | cancelText: '取消', 117 | onCancel: () => undefined, 118 | onOk: () => undefined, 119 | onVisibleChange: () => undefined, 120 | onChange: () => undefined 121 | }; 122 | Picker.PickerPanel = PickerPanel; 123 | 124 | export default Picker; 125 | -------------------------------------------------------------------------------- /site/page/Dialog/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Dialog, Cell, Button } from '@/components/index'; 3 | import { DialogProps } from '@/components/dialog'; 4 | 5 | function DialogDemo() { 6 | const [visible, setVisible] = useState(false); 7 | const [config, setConfig] = useState({}); 8 | function asyncClose() { 9 | return new Promise(resolve => { 10 | setTimeout(() => { 11 | resolve(); 12 | setVisible(false); 13 | }, 2000); 14 | }); 15 | } 16 | return ( 17 |
18 |
基础用法
19 | { 23 | setConfig({ 24 | footerActions: ['ok'], 25 | children: '代码是写出来给人看的,附带能在机器上运行' 26 | }); 27 | setVisible(true); 28 | }} 29 | /> 30 | { 34 | setConfig({ 35 | footerActions: ['ok'], 36 | children: '代码是写出来给人看的,附带能在机器上运行', 37 | title: '' 38 | }); 39 | setVisible(true); 40 | }} 41 | /> 42 | { 46 | setConfig({ 47 | children: '代码是写出来给人看的,附带能在机器上运行', 48 | okProps: { style: { color: 'red' } } 49 | }); 50 | setVisible(true); 51 | }} 52 | /> 53 |
异步关闭
54 | { 58 | setConfig({ 59 | children: '代码是写出来给人看的,附带能在机器上运行', 60 | onOk: asyncClose 61 | }); 62 | setVisible(true); 63 | }} 64 | /> 65 |
更多按钮
66 | { 70 | setConfig({ 71 | children: '代码是写出来给人看的,附带能在机器上运行', 72 | onOk: asyncClose, 73 | footer: [ 74 | , 77 | , 80 | 83 | ] 84 | }); 85 | setVisible(true); 86 | }} 87 | /> 88 | 89 | { 92 | console.log('ok'); 93 | setVisible(false); 94 | }} 95 | onCancel={() => { 96 | console.log('onCancel'); 97 | setVisible(false); 98 | }} 99 | {...config} 100 | > 101 | {config.children} 102 | 103 | 104 |
组件调用
105 | { 109 | Dialog.show({ 110 | title: '提示', 111 | content: '这是提示弹窗' 112 | }); 113 | }} 114 | /> 115 | { 119 | Dialog.confirm({ 120 | title: '提示', 121 | content: '这是确认弹窗', 122 | onOk: () => console.log('ok'), 123 | onCancel: () => console.log('onCancel'), 124 | onClose: () => console.log('onClose') 125 | }); 126 | }} 127 | /> 128 |
129 | ); 130 | } 131 | export default DialogDemo; 132 | -------------------------------------------------------------------------------- /components/tabs/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @tabsPrefixCls: sty-tabs; 4 | 5 | .@{tabsPrefixCls} { 6 | display: flex; 7 | overflow: hidden; 8 | background-color: #fff; 9 | 10 | &-top { 11 | flex-direction: column; 12 | } 13 | 14 | &-bottom { 15 | flex-direction: column-reverse; 16 | } 17 | 18 | &-left { 19 | flex-direction: row; 20 | align-items: stretch; 21 | } 22 | 23 | &-right { 24 | flex-direction: row-reverse; 25 | align-items: stretch; 26 | } 27 | 28 | &-tab-bar-wrapper { 29 | background-color: #fff; 30 | position: relative; 31 | height: 100%; 32 | 33 | .@{tabsPrefixCls}-left &, 34 | .@{tabsPrefixCls}-right & { 35 | width: 25%; 36 | } 37 | 38 | &::after { 39 | position: absolute; 40 | box-sizing: border-box; 41 | content: ' '; 42 | pointer-events: none; 43 | top: -50%; 44 | right: -50%; 45 | bottom: -50%; 46 | left: -50%; 47 | transform: scale(0.5); 48 | 49 | .@{tabsPrefixCls}-left & { 50 | border-right: 1px solid @base-border-color; 51 | } 52 | 53 | .@{tabsPrefixCls}-left & { 54 | border-left: 1px solid @base-border-color; 55 | } 56 | 57 | .@{tabsPrefixCls}-top &, 58 | .@{tabsPrefixCls}-bottom & { 59 | border-bottom: 1px solid @base-border-color; 60 | } 61 | 62 | } 63 | 64 | } 65 | 66 | &-tab-bar { 67 | display: flex; 68 | position: relative; 69 | overflow: auto; 70 | flex-shrink: 0; 71 | 72 | .@{tabsPrefixCls}-top &, 73 | .@{tabsPrefixCls}-bottom & { 74 | flex-direction: row; 75 | width: 100%; 76 | 77 | .@{tabsPrefixCls}-bar-tab { 78 | min-width: 25%; 79 | padding: 16px 5px; 80 | } 81 | } 82 | 83 | .@{tabsPrefixCls}-left &, 84 | .@{tabsPrefixCls}-right & { 85 | flex-direction: column; 86 | height: 100%; 87 | width: 100%; 88 | 89 | .@{tabsPrefixCls}-bar-tab { 90 | width: 100%; 91 | min-height: 25%; 92 | padding: 0 8px; 93 | } 94 | } 95 | 96 | &::-webkit-scrollbar { 97 | display: none; 98 | } 99 | 100 | .@{tabsPrefixCls}-bar-tab { 101 | display: flex; 102 | justify-content: center; 103 | align-items: center; 104 | flex: 1; 105 | box-sizing: border-box; 106 | color: @tabs-text-color; 107 | 108 | &-title { 109 | text-overflow: ellipsis; 110 | white-space: nowrap; 111 | overflow: hidden; 112 | } 113 | } 114 | 115 | .@{tabsPrefixCls}-bar-tab-active { 116 | color: @base-color; 117 | } 118 | 119 | 120 | .@{tabsPrefixCls}-line { 121 | position: absolute; 122 | background-color: @base-color; 123 | 124 | .@{tabsPrefixCls}-left & { 125 | right: 0; 126 | width: 2px; 127 | } 128 | 129 | .@{tabsPrefixCls}-right & { 130 | left: 0; 131 | width: 2px; 132 | } 133 | 134 | .@{tabsPrefixCls}-top &, 135 | .@{tabsPrefixCls}-bottom & { 136 | bottom: 0; 137 | height: 2px; 138 | } 139 | } 140 | } 141 | 142 | &-content { 143 | display: flex; 144 | width: 100%; 145 | 146 | &&-animated { 147 | transition: transform .3s cubic-bezier(.35, 0, .25, 1), left .3s cubic-bezier(.35, 0, .25, 1), top .3s cubic-bezier(.35, 0, .25, 1), -webkit-transform .3s cubic-bezier(.35, 0, .25, 1); 148 | will-change: transform; 149 | } 150 | 151 | .@{tabsPrefixCls}-right &, 152 | .@{tabsPrefixCls}-left & { 153 | display: block; 154 | } 155 | 156 | .@{tabsPrefixCls}-tabPane { 157 | flex-shrink: 0; 158 | width: 100%; 159 | height: 100%; 160 | overflow: auto; 161 | 162 | // display: flex; 163 | // justify-content: center; 164 | // align-items: center; 165 | 166 | word-wrap: break-word; 167 | word-break: break-all; 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /components/date-picker/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @dataPanelPrefixCls: sty-date-panel; 4 | 5 | @range-background-color: #e6f5f4; 6 | 7 | 8 | .@{dataPanelPrefixCls}{ 9 | background-color: #fff; 10 | } 11 | 12 | .@{dataPanelPrefixCls}-header { 13 | display: flex; 14 | padding: 0 8px; 15 | color: rgba(0, 0, 0, .85); 16 | border-bottom: 1px solid #f0f0f0; 17 | 18 | button { 19 | padding: 0; 20 | color: rgba(0, 0, 0, .25); 21 | line-height: 40px; 22 | border: 0; 23 | background: none; 24 | outline: none; 25 | cursor: pointer; 26 | transition: color .3s; 27 | min-width: 1.6em; 28 | font-size: 28px; 29 | opacity: .8; 30 | } 31 | 32 | &-view { 33 | flex: auto; 34 | text-align: center; 35 | font-weight: 500; 36 | line-height: 40px; 37 | } 38 | } 39 | 40 | .@{dataPanelPrefixCls}-body { 41 | padding: 8px 12px; 42 | 43 | th,td{ 44 | text-align: center; 45 | vertical-align: middle; 46 | } 47 | 48 | .@{dataPanelPrefixCls}-cell { 49 | position: relative; 50 | color: #aaa; 51 | padding: 3px 0; 52 | 53 | &::before { 54 | content: ""; 55 | position: absolute; 56 | top: 50%; 57 | right: 0; 58 | left: 0; 59 | height: 30px; 60 | transform: translateY(-50%); 61 | z-index: 1; 62 | } 63 | 64 | 65 | 66 | .@{dataPanelPrefixCls}-cell-inner { 67 | position: relative; 68 | display: inline-block; 69 | min-width: 80%; 70 | height: 30px; 71 | line-height: 30px; 72 | text-align: center; 73 | border-radius: 2px; 74 | z-index: 2; 75 | } 76 | 77 | &-in-view { 78 | color: #333; 79 | } 80 | 81 | &-in-range:not(.@{dataPanelPrefixCls}-cell-selected)::before { 82 | background-color: @range-background-color; 83 | } 84 | 85 | &-range-start { 86 | &:not(.@{dataPanelPrefixCls}-cell-range-start-single)::before { 87 | background-color: @range-background-color; 88 | left: 50%; 89 | } 90 | } 91 | 92 | &-range-end { 93 | &::before { 94 | background-color: @range-background-color; 95 | right: 50%; 96 | } 97 | } 98 | 99 | &-selected .@{dataPanelPrefixCls}-cell-inner { 100 | background-color: @base-color; 101 | color: #fff; 102 | } 103 | 104 | &-today { 105 | color: @base-color; 106 | } 107 | 108 | &-disabled { 109 | opacity: 0.2; 110 | } 111 | } 112 | 113 | 114 | &-content { 115 | height: 246px; 116 | width: 100%; 117 | table-layout: fixed; 118 | border-collapse: collapse; 119 | } 120 | } 121 | 122 | 123 | .@{dataPanelPrefixCls}-date-panel { 124 | th{ 125 | height: 30px; 126 | } 127 | .@{dataPanelPrefixCls}-cell { 128 | .@{dataPanelPrefixCls}-cell-inner { 129 | min-width: 30px; 130 | border-radius: 50%; 131 | } 132 | 133 | &-in-range:last-child { 134 | &::before { 135 | right: 50%; 136 | } 137 | 138 | .@{dataPanelPrefixCls}-cell-inner { 139 | background-color: @range-background-color; 140 | } 141 | } 142 | 143 | &-in-range:first-child { 144 | &::before { 145 | left: 50%; 146 | } 147 | 148 | .@{dataPanelPrefixCls}-cell-inner { 149 | background-color: @range-background-color; 150 | } 151 | } 152 | } 153 | 154 | } 155 | 156 | 157 | .@{dataPanelPrefixCls}-footer { 158 | display: flex; 159 | justify-content: flex-end; 160 | align-items: center; 161 | height: 40px; 162 | padding: 0 12px; 163 | border-top: 1px solid #f0f0f0; 164 | 165 | &>.split { 166 | width: 10px; 167 | text-align: center; 168 | } 169 | &>.date{ 170 | font-size: 14px; 171 | text-align: center; 172 | } 173 | 174 | &.@{dataPanelPrefixCls}-date-footer>.date { 175 | width: 98px; 176 | } 177 | 178 | &.@{dataPanelPrefixCls}-month-footer>.date { 179 | width: 68px; 180 | } 181 | 182 | &.@{dataPanelPrefixCls}-year-footer>.date { 183 | width: 43px; 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /docs/static/js/18.b7e3bcf8.chunk.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"] = window["webpackJsonp"] || []).push([[18],{ 2 | 3 | /***/ "./site/page/Select/index.tsx": 4 | /*!************************************!*\ 5 | !*** ./site/page/Select/index.tsx ***! 6 | \************************************/ 7 | /*! exports provided: default */ 8 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 9 | 10 | "use strict"; 11 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/_react@16.14.0@react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _components_index__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @/components/index */ \"./components/index.ts\");\n\n\nconst dataSource1 = new Array(10).fill({}).map((item, index) => ({\n label: `选项${index + 1}`,\n value: index + 1\n}));\nconst dataSource2 = new Array(10).fill({}).map((item, index) => ({\n label: `选项${index + 1}`,\n value: index + 1,\n disabled: !!(index % 2)\n}));\nfunction SelectDemo() {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(\"div\", {\n className: 'demo-block__title'\n }, \"\\u57FA\\u7840\\u7528\\u6CD5\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"Select\"], {\n dataSource: dataSource1,\n mode: 'single'\n }, \"\\u5355\\u9009\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"Select\"], {\n dataSource: dataSource1,\n mode: 'multiple'\n }, \"\\u591A\\u9009\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"Select\"], {\n dataSource: dataSource2,\n mode: 'multiple'\n }, \"\\u7981\\u7528\\u9009\\u9879\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_components_index__WEBPACK_IMPORTED_MODULE_1__[\"Select\"], {\n dataSource: dataSource1,\n mode: 'multiple',\n loading: true\n }, \"\\u52A0\\u8F7D\\u72B6\\u6001\"));\n}\n/* harmony default export */ __webpack_exports__[\"default\"] = (SelectDemo);//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zaXRlL3BhZ2UvU2VsZWN0L2luZGV4LnRzeC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy8uL3NpdGUvcGFnZS9TZWxlY3QvaW5kZXgudHN4PzFkZDciXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0JztcbmltcG9ydCB7IFNlbGVjdCB9IGZyb20gJ0AvY29tcG9uZW50cy9pbmRleCc7XG5cbmNvbnN0IGRhdGFTb3VyY2UxID0gbmV3IEFycmF5KDEwKS5maWxsKHt9KS5tYXAoKGl0ZW0sIGluZGV4KSA9PiAoe1xuICBsYWJlbDogYOmAiemhuSR7aW5kZXggKyAxfWAsXG4gIHZhbHVlOiBpbmRleCArIDFcbn0pKTtcblxuY29uc3QgZGF0YVNvdXJjZTIgPSBuZXcgQXJyYXkoMTApLmZpbGwoe30pLm1hcCgoaXRlbSwgaW5kZXgpID0+ICh7XG4gIGxhYmVsOiBg6YCJ6aG5JHtpbmRleCArIDF9YCxcbiAgdmFsdWU6IGluZGV4ICsgMSxcbiAgZGlzYWJsZWQ6ICEhKGluZGV4ICUgMilcbn0pKTtcblxuZnVuY3Rpb24gU2VsZWN0RGVtbygpIHtcbiAgcmV0dXJuIChcbiAgICA8ZGl2PlxuICAgICAgPGRpdiBjbGFzc05hbWU9J2RlbW8tYmxvY2tfX3RpdGxlJz7ln7rnoYDnlKjms5U8L2Rpdj5cbiAgICAgIDxTZWxlY3QgZGF0YVNvdXJjZT17ZGF0YVNvdXJjZTF9IG1vZGU9J3NpbmdsZSc+XG4gICAgICAgIOWNlemAiVxuICAgICAgPC9TZWxlY3Q+XG4gICAgICA8U2VsZWN0IGRhdGFTb3VyY2U9e2RhdGFTb3VyY2UxfSBtb2RlPSdtdWx0aXBsZSc+XG4gICAgICAgIOWkmumAiVxuICAgICAgPC9TZWxlY3Q+XG4gICAgICA8U2VsZWN0IGRhdGFTb3VyY2U9e2RhdGFTb3VyY2UyfSBtb2RlPSdtdWx0aXBsZSc+XG4gICAgICAgIOemgeeUqOmAiemhuVxuICAgICAgPC9TZWxlY3Q+XG4gICAgICA8U2VsZWN0IGRhdGFTb3VyY2U9e2RhdGFTb3VyY2UxfSBtb2RlPSdtdWx0aXBsZScgbG9hZGluZz5cbiAgICAgICAg5Yqg6L2954q25oCBXG4gICAgICA8L1NlbGVjdD5cbiAgICA8L2Rpdj5cbiAgKTtcbn1cbmV4cG9ydCBkZWZhdWx0IFNlbGVjdERlbW87XG4iXSwibWFwcGluZ3MiOiJBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFFQTtBQUFBO0FBQ0E7QUFBQTtBQUFBO0FBR0E7QUFBQTtBQUFBO0FBR0E7QUFBQTtBQUFBO0FBR0E7QUFBQTtBQUFBO0FBQUE7QUFLQTtBQUNBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///./site/page/Select/index.tsx\n"); 12 | 13 | /***/ }) 14 | 15 | }]); -------------------------------------------------------------------------------- /site/routes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Home from './page/Home'; 3 | import asyncComponent from './page/asyncComponent'; 4 | import { RouteConfig } from 'react-router-config'; 5 | import { NavBar } from '@/components/index'; 6 | import { useHistory } from 'react-router-dom'; 7 | 8 | const ButtonDemo = asyncComponent(() => import('./page/Button')); 9 | const IconDemo = asyncComponent(() => import('./page/Icon')); 10 | const NavBarDemo = asyncComponent(() => import('./page/NavBar')); 11 | const RippleDemo = asyncComponent(() => import('./page/Ripple')); 12 | const LoadingDemo = asyncComponent(() => import('./page/Loading')); 13 | const SwitchDemo = asyncComponent(() => import('./page/Switch')); 14 | const TimelineDemo = asyncComponent(() => import('./page/Timeline')); 15 | const CellDemo = asyncComponent(() => import('./page/Cell')); 16 | const CheckboxDemo = asyncComponent(() => import('./page/Checkbox')); 17 | const RadioDemo = asyncComponent(() => import('./page/Radio')); 18 | const ToastDemo = asyncComponent(() => import('./page/Toast')); 19 | const PopupDemo = asyncComponent(() => import('./page/Popup')); 20 | const ActionSheetDemo = asyncComponent(() => import('./page/ActionSheet')); 21 | const DialogDemo = asyncComponent(() => import('./page/Dialog')); 22 | const ImageDemo = asyncComponent(() => import('./page/Image')); 23 | const TabsDemo = asyncComponent(() => import('./page/Tabs')); 24 | const PullRefreshDemo = asyncComponent(() => import('./page/PullRefresh')); 25 | const PickerDemo = asyncComponent(() => import('./page/Picker')); 26 | const SwipeDemo = asyncComponent(() => import('./page/Swipe')); 27 | const DatePickerDemo = asyncComponent(() => import('./page/DatePicker')); 28 | const SelectDemo = asyncComponent(() => import('./page/Select')); 29 | 30 | let routes: RouteConfig[] = [ 31 | { 32 | path: '/', 33 | exact: true, 34 | component: Home 35 | }, 36 | { 37 | path: '/button', 38 | component: ButtonDemo 39 | }, 40 | { 41 | path: '/icon', 42 | component: IconDemo 43 | }, 44 | { 45 | path: '/nav-bar', 46 | component: NavBarDemo 47 | }, 48 | { 49 | path: '/ripple', 50 | component: RippleDemo 51 | }, 52 | { 53 | path: '/loading', 54 | component: LoadingDemo 55 | }, 56 | { 57 | path: '/switch', 58 | component: SwitchDemo 59 | }, 60 | { 61 | path: '/timeline', 62 | component: TimelineDemo 63 | }, 64 | { 65 | path: '/cell', 66 | component: CellDemo 67 | }, 68 | { 69 | path: '/checkbox', 70 | component: CheckboxDemo 71 | }, 72 | { 73 | path: '/radio', 74 | component: RadioDemo 75 | }, 76 | { 77 | path: '/toast', 78 | component: ToastDemo 79 | }, 80 | { 81 | path: '/popup', 82 | component: PopupDemo 83 | }, 84 | { 85 | path: '/action-sheet', 86 | component: ActionSheetDemo 87 | }, 88 | { 89 | path: '/dialog', 90 | component: DialogDemo 91 | }, 92 | { 93 | path: '/image', 94 | component: ImageDemo 95 | }, 96 | { 97 | path: '/tabs', 98 | component: TabsDemo 99 | }, 100 | { 101 | path: '/pull-refresh', 102 | component: PullRefreshDemo 103 | }, 104 | { 105 | path: '/picker', 106 | component: PickerDemo 107 | }, 108 | { 109 | path: '/swipe', 110 | component: SwipeDemo 111 | }, 112 | { 113 | path: '/date-picker', 114 | component: DatePickerDemo 115 | }, 116 | { 117 | path: '/select', 118 | component: SelectDemo 119 | } 120 | ]; 121 | // 用div将懒加载的代码包裹起来,防止路由动画不起作用 122 | routes = routes.map(i => ({ 123 | ...i, 124 | component: props => { 125 | const history = useHistory(); 126 | return ( 127 |
128 | {i.path !== '/' && ( 129 | 136 | )} 137 | 138 |
139 | ); 140 | } 141 | })); 142 | export default routes; 143 | -------------------------------------------------------------------------------- /components/select/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react'; 2 | import { CellPopup, Checkbox, Radio, Loading } from '../index'; 3 | import { CellPopupProps } from '../cell-popup'; 4 | import { 5 | OptionValueType, 6 | CheckboxOptionType, 7 | GroupProps, 8 | OptionObjType 9 | } from '../checkbox'; 10 | import './index.less'; 11 | 12 | export type SelectValueType = T | Array; 13 | export type DataSourceType = Array>; 14 | 15 | const CheckboxGroup = Checkbox.CheckboxGroup; 16 | const RadioGroup = Radio.RadioGroup; 17 | 18 | export interface SelectProps 19 | extends Omit, 'value' | 'defaultValue'>, 20 | Omit { 21 | mode?: 'single' | 'multiple'; 22 | value?: SelectValueType; 23 | defaultValue?: SelectValueType; 24 | dataSource?: DataSourceType; 25 | loading?: boolean; 26 | onOk?: (value: SelectValueType) => unknown; 27 | onChange?: (value: SelectValueType) => unknown; 28 | children?: React.ReactNode; 29 | className?: string; 30 | style?: React.CSSProperties; 31 | } 32 | 33 | function Select(props: SelectProps) { 34 | const { 35 | mode, 36 | value: valueProps, 37 | defaultValue: defaultValueProps, 38 | dataSource, 39 | loading, 40 | cellTitle, 41 | children, 42 | shape, 43 | onOk: onOkProps, 44 | onChange: onChangeProps, 45 | onCancel, 46 | onVisibleChange: onVisibleChangeProps, 47 | className, 48 | style 49 | } = props; 50 | const [innerValue, setInnerValue] = useState>(); 51 | const [value, setValue] = useState>(defaultValueProps); 52 | 53 | const valueText = useMemo(getValueText, [value]); 54 | 55 | useEffect(() => { 56 | setValue(valueProps || defaultValueProps); 57 | }, [valueProps]); 58 | 59 | function getValueText() { 60 | const texts = []; 61 | const valueList = Array.isArray(value) ? value : [value]; 62 | valueList.forEach(item => { 63 | const text = dataSource.find( 64 | option => option === item || (option as OptionObjType).value === item 65 | ); 66 | if (typeof text === 'object') { 67 | texts.push(text.label); 68 | } else { 69 | texts.push(text); 70 | } 71 | }); 72 | return texts.join(','); 73 | } 74 | 75 | function onOk() { 76 | onOkProps(innerValue); 77 | onChangeProps(innerValue); 78 | // 如果不受控则由内部控制值 79 | if (valueProps === undefined) { 80 | setValue(innerValue); 81 | } 82 | } 83 | 84 | function onVisibleChange(visible) { 85 | if (visible) { 86 | setInnerValue(value); 87 | } 88 | onVisibleChangeProps && onVisibleChangeProps(visible); 89 | } 90 | function renderContent() { 91 | const baseShape: 'round' | 'square' = 92 | shape || mode === 'single' ? 'round' : 'square'; 93 | const baseIconAlign: 'right' | 'left' = 'left'; 94 | const baseProps = { 95 | options: dataSource, 96 | cell: true, 97 | shape: baseShape, 98 | iconAlign: baseIconAlign 99 | }; 100 | return mode === 'single' ? ( 101 | setInnerValue(v)} 105 | /> 106 | ) : ( 107 | } 110 | onChange={setInnerValue} 111 | /> 112 | ); 113 | } 114 | 115 | return ( 116 | 125 | {loading && } 126 |
{renderContent()}
127 |
128 | ); 129 | } 130 | 131 | Select.defaultProps = { 132 | mode: 'single', 133 | onOk: () => undefined, 134 | onChange: () => undefined 135 | }; 136 | export default Select; 137 | -------------------------------------------------------------------------------- /components/picker/PickerColumn.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useRef, useState } from 'react'; 2 | import { PickerColumnProps } from './index'; 3 | import { OptionObjType } from '../checkbox'; 4 | import { useTouch } from '../hooks'; 5 | import { classnames, range } from '../_utils'; 6 | 7 | const prefixCls = 'sty-picker-panel'; 8 | const DEFAULT_DURATION = 300; 9 | const ITEM_HEIGHT = 44; 10 | 11 | function PickerColumn(props: PickerColumnProps) { 12 | const { list, wrapHeight, value, onChange } = props; 13 | const [touch, boxRef] = useTouch(); 14 | const [translateY, setTranslateY] = useState(0); 15 | const [duration, setDuration] = useState(DEFAULT_DURATION); 16 | const startTranslateY = useRef(0); 17 | const [activeIndex, setActiveIndex] = useState(); 18 | 19 | const newList: Array = useMemo(() => { 20 | if (!Array.isArray(list)) { 21 | return []; 22 | } 23 | const arr = list.filter(item => item); // 过滤空值 24 | return arr.map(option => { 25 | if (typeof option === 'string' || typeof option === 'number') { 26 | return { 27 | label: option, 28 | value: option 29 | }; 30 | } 31 | return option; 32 | }); 33 | }, [list]); 34 | 35 | const topBaseOffset = useMemo(() => wrapHeight / 2 - ITEM_HEIGHT / 2, [ 36 | wrapHeight 37 | ]); 38 | useEffect(() => { 39 | if (activeIndex !== undefined) { 40 | setIndex(activeIndex); 41 | } 42 | }, [activeIndex]); 43 | 44 | useEffect(() => { 45 | const index = getIndex(value); 46 | setIndex(index); 47 | }, [value, newList]); 48 | 49 | function onTouchStart() { 50 | startTranslateY.current = translateY; 51 | setDuration(0); 52 | } 53 | function onTouchMove() { 54 | if (!touch) { 55 | return; 56 | } 57 | let move = startTranslateY.current + touch.moveY; 58 | 59 | move = range(move, -ITEM_HEIGHT * newList.length, ITEM_HEIGHT); // 限制偏移区间 下滑不能超过中线下的第一个,上滑不能超过中线上的第一个 60 | setTranslateY(move); 61 | } 62 | function onTouchEnd() { 63 | setDuration(DEFAULT_DURATION); 64 | const index = getIndexByOffset(translateY); 65 | setIndex(index); 66 | } 67 | 68 | // 主要是解决disabled选项 69 | function adjustIndex(index) { 70 | const arr = newList.slice(); 71 | index = range(index, 0, arr.length - 1); 72 | for (let i = index; i < arr.length; i++) { 73 | if (!arr[i].disabled) { 74 | return i; 75 | } 76 | } 77 | for (let i = index - 1; i >= 0; i--) { 78 | if (!arr[i].disabled) { 79 | return i; 80 | } 81 | } 82 | return index; 83 | } 84 | 85 | function getIndexByOffset(offset) { 86 | return range(Math.round(-offset / ITEM_HEIGHT), 0, newList.length - 1); 87 | } 88 | 89 | function setIndex(index) { 90 | index = adjustIndex(index); 91 | setActiveIndex(index); 92 | setTranslateY(-44 * index); 93 | newList[index] !== undefined && onChange(newList[index].value); 94 | } 95 | 96 | function getIndex(v) { 97 | let index = 0; 98 | const findIndex = newList.findIndex(i => i.value === v); 99 | if (findIndex !== -1) { 100 | index = findIndex; 101 | } 102 | return index; 103 | } 104 | return ( 105 |
112 |
    118 | {newList.map(item => ( 119 |
  • 124 | {item.label} 125 | {item.disabled && (禁用)} 126 |
  • 127 | ))} 128 |
129 |
130 | ); 131 | } 132 | 133 | PickerColumn.defaultProps = { 134 | list: [], 135 | onChange: () => undefined 136 | }; 137 | export default PickerColumn; 138 | -------------------------------------------------------------------------------- /components/picker/PickerPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import { PickerPanelProps, CascadeType } from './index'; 4 | import { Loading } from '../index'; 5 | import PickerColumn from './PickerColumn'; 6 | import './index.less'; 7 | 8 | const prefixCls = 'sty-picker-panel'; 9 | 10 | function PickerPanel(props: PickerPanelProps) { 11 | const { 12 | value: valueProps, 13 | defaultValue, 14 | onChange: onPropsChange, 15 | visible, 16 | dataSource, 17 | loading, 18 | className, 19 | style 20 | } = props; 21 | const wrapper = useRef(); 22 | const [wrapHeight, setWrapHeight] = useState(0); 23 | const [valueList, setValueList] = useState(defaultValue || []); 24 | const [columns, setColumns] = useState([]); 25 | const isCascade = (dataSource[0] as CascadeType)?.children; 26 | 27 | useEffect(() => { 28 | if (visible) { 29 | setWrapHeight(wrapper.current.offsetHeight); 30 | } 31 | }, [visible]); 32 | 33 | useEffect(() => { 34 | if (Array.isArray(valueProps)) { 35 | setValueList(valueProps); 36 | } 37 | }, [valueProps]); 38 | 39 | useEffect(() => { 40 | const arr = formatColumns(); 41 | setColumns(arr); 42 | }, [dataSource]); 43 | 44 | function formatColumns() { 45 | if (!Array.isArray(dataSource)) { 46 | return []; 47 | } 48 | const firstColumn = dataSource[0] || {}; 49 | if (Array.isArray(firstColumn)) { 50 | return dataSource; 51 | } 52 | return [dataSource || []]; 53 | } 54 | 55 | function formatCascade(startColumnIndex, startColumnValue?, arr?) { 56 | arr = arr || columns.slice(0, startColumnIndex + 1); 57 | const itemIndex = getIndexOfValue( 58 | startColumnValue ?? valueList[startColumnIndex], 59 | columns[startColumnIndex] 60 | ); 61 | let cursor = { children: arr[startColumnIndex][itemIndex].children }; 62 | for (let i = startColumnIndex + 1; cursor && cursor.children; i++) { 63 | arr.push(cursor.children); 64 | const index = getIndexOfValue(valueList[i], cursor.children); 65 | cursor = { children: cursor.children[index].children }; 66 | } 67 | return arr; 68 | } 69 | 70 | function getIndexOfValue(v, list) { 71 | if (!Array.isArray(list)) { 72 | return 0; 73 | } 74 | let index = 0; 75 | const findIndex = list.findIndex(i => { 76 | if (typeof i === 'string' || typeof i === 'number') { 77 | return i === v; 78 | } 79 | return i.value === v; 80 | }); 81 | if (findIndex !== -1) { 82 | index = findIndex; 83 | } 84 | return index; 85 | } 86 | 87 | function renderColumns() { 88 | return columns.map((item, index) => ( 89 | onColumnChange(v, index)} 94 | value={valueList[index]} 95 | /> 96 | )); 97 | } 98 | 99 | function onColumnChange(v, index) { 100 | setValueList(pre => { 101 | const arr = pre.slice(); 102 | arr[index] = v; 103 | if (JSON.stringify(pre) !== JSON.stringify(arr)) { 104 | setTimeout(() => { 105 | onPropsChange(arr, index); 106 | }); 107 | } 108 | if (isCascade) { 109 | setColumns(formatCascade(index, v)); 110 | } 111 | return arr; 112 | }); 113 | } 114 | return ( 115 |
120 | {loading && ( 121 |
122 | 123 |
124 | )} 125 |
126 | {renderColumns()} 127 |
131 |
134 |
135 |
136 | ); 137 | } 138 | 139 | PickerPanel.defaultProps = { 140 | visible: true, 141 | onChange: () => undefined 142 | }; 143 | export default PickerPanel; 144 | -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const TerserJSPlugin = require('terser-webpack-plugin'); 6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 7 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 8 | 9 | // 从根目录走 10 | function resolve(dir) { 11 | return path.join(__dirname, '..', dir); 12 | } 13 | 14 | module.exports = { 15 | context: path.resolve(__dirname, '../'), // 入口起点根目录 16 | entry: { 17 | app: './site/index.tsx' 18 | }, 19 | output: { 20 | path: resolve('build'), 21 | filename: '[name].js', 22 | chunkFilename: 'static/js/[name].[contenthash:8].chunk.js', 23 | libraryTarget: 'umd' 24 | }, 25 | resolve: { 26 | alias: { 27 | '@': resolve('./') 28 | }, 29 | extensions: ['.tsx', '.ts', '.js', '.jsx'] 30 | }, 31 | // externals: { 32 | // react: 'React', 33 | // 'react-dom': 'ReactDOM' 34 | // }, 35 | optimization: { 36 | // splitChunks: { 37 | // chunks: 'all' 38 | // }, 39 | minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})] 40 | }, 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.(js|jsx|ts|tsx)$/, 45 | exclude: /(node_modules|bower_components)/, 46 | use: [ 47 | { 48 | loader: 'babel-loader', 49 | options: { 50 | presets: ['@babel/preset-react'], 51 | plugins: [ 52 | ['@babel/plugin-proposal-decorators', { legacy: true }], 53 | '@babel/plugin-proposal-class-properties', 54 | '@babel/plugin-syntax-dynamic-import' 55 | ] 56 | } 57 | }, 58 | 'ts-loader', 59 | 'eslint-loader' 60 | ] 61 | }, 62 | { 63 | test: /\.css$/, 64 | use: [ 65 | { 66 | loader: MiniCssExtractPlugin.loader, 67 | options: { 68 | // you can specify a publicPath here 69 | // by default it uses publicPath in webpackOptions.output 70 | publicPath: '../', 71 | hmr: process.env.NODE_ENV === 'development' 72 | } 73 | }, 74 | 'css-loader' 75 | ] 76 | }, 77 | { 78 | test: /\.less$/, 79 | use: [ 80 | { 81 | loader: MiniCssExtractPlugin.loader, 82 | options: { 83 | hmr: process.env.NODE_ENV === 'development', 84 | // if hmr does not work, this is a forceful method. 85 | reloadAll: true 86 | } 87 | }, 88 | 'css-loader', 89 | 'less-loader' 90 | ] 91 | }, 92 | { 93 | test: /\.(png|jpe?g|svg|gif)(\?.*)?$/, 94 | loader: 'url-loader', 95 | options: { 96 | limit: 10000, 97 | name: 'static/img/[name].[hash:7].[ext]' 98 | } 99 | }, 100 | { 101 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 102 | loader: 'url-loader', 103 | options: { 104 | limit: 10000, 105 | name: 'static/font/[name].[hash:7].[ext]', 106 | publicPath: '../../' // 字体在css中,路径要退两层 107 | } 108 | }, 109 | { 110 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 111 | loader: 'url-loader', 112 | options: { 113 | limit: 10000, 114 | name: 'static/media/[name].[hash:7].[ext]' 115 | } 116 | } 117 | ] 118 | }, 119 | plugins: [ 120 | new HtmlWebpackPlugin({ 121 | template: './site/index.html', 122 | minify: { 123 | collapseWhitespace: true// 删除空格、换行 124 | } 125 | }), 126 | new CleanWebpackPlugin(), 127 | new MiniCssExtractPlugin({ 128 | filename: 'static/css/[name].css', // 打包到static的css目录下 129 | chunkFilename: 'static/css/[name].[contenthash:8].chunk.css', 130 | ignoreOrder: false // Enable to remove warnings about conflicting order 131 | }), 132 | new BundleAnalyzerPlugin({ 133 | analyzerMode: 'disabled', 134 | generateStatsFile: true 135 | }) 136 | ] 137 | }; 138 | -------------------------------------------------------------------------------- /components/popup/index.less: -------------------------------------------------------------------------------- 1 | @import '../style/default.less'; 2 | 3 | @popupPrefixCls: sty-popup; 4 | 5 | 6 | .@{popupPrefixCls} { 7 | position: fixed; 8 | background-color: #fff; 9 | z-index: 1000; 10 | will-change: transform; 11 | max-height: 100%; 12 | max-width: 100%; 13 | 14 | .close-icon { 15 | position: absolute; 16 | z-index: 1; 17 | color: @base-text-disabled; 18 | font-size: 22px; 19 | cursor: pointer; 20 | } 21 | 22 | &-center { 23 | padding: 30px; 24 | top: 50%; 25 | left: 50%; 26 | transform: translate3d(-50%, -50%, 0); 27 | 28 | .close-icon { 29 | top: 16px; 30 | right: 16px; 31 | } 32 | 33 | &.@{popupPrefixCls}-round { 34 | border-radius: 20px; 35 | } 36 | } 37 | 38 | &-top { 39 | top: 0; 40 | left: 0; 41 | width: 100vw; 42 | 43 | .close-icon { 44 | bottom: 16px; 45 | right: 16px; 46 | } 47 | 48 | &.@{popupPrefixCls}-round { 49 | border-radius: 0 0 20px 20px; 50 | } 51 | 52 | } 53 | 54 | &-bottom { 55 | bottom: 0; 56 | left: 0; 57 | width: 100vw; 58 | 59 | .close-icon { 60 | top: 16px; 61 | right: 16px; 62 | } 63 | 64 | &.@{popupPrefixCls}-round { 65 | border-radius: 20px 20px 0 0; 66 | } 67 | } 68 | 69 | &-left { 70 | top: 0; 71 | left: 0; 72 | height: 100vh; 73 | 74 | .close-icon { 75 | top: 16px; 76 | right: 16px; 77 | } 78 | 79 | &.@{popupPrefixCls}-round { 80 | border-radius: 0 20px 20px 0; 81 | } 82 | } 83 | 84 | &-right { 85 | top: 0; 86 | right: 0; 87 | height: 100vh; 88 | 89 | .close-icon { 90 | top: 16px; 91 | left: 16px; 92 | } 93 | 94 | &.@{popupPrefixCls}-round { 95 | border-radius: 20px 0 0 20px; 96 | } 97 | } 98 | } 99 | 100 | 101 | .slide_topIn { 102 | animation: slide-top-in 300ms; 103 | } 104 | 105 | .slide_topOut { 106 | animation: slide-top-out 300ms; 107 | } 108 | 109 | .slide_bottomIn { 110 | animation: slide-bottom-in 300ms; 111 | } 112 | 113 | .slide_bottomOut { 114 | animation: slide-bottom-out 300ms; 115 | } 116 | 117 | .slide_rightIn { 118 | animation: slide-right-in 300ms; 119 | } 120 | 121 | .slide_rightOut { 122 | animation: slide-right-out 300ms; 123 | } 124 | 125 | .slide_leftIn { 126 | animation: slide-left-in 300ms; 127 | } 128 | 129 | .slide_leftOut { 130 | animation: slide-left-out 300ms; 131 | } 132 | 133 | 134 | 135 | @keyframes slide-top-in { 136 | from { 137 | transform: translate3d(0, -100%, 0); 138 | } 139 | 140 | to { 141 | transform: translate3d(0, 0, 0); 142 | } 143 | } 144 | 145 | @keyframes slide-top-out { 146 | from { 147 | transform: translate3d(0, 0, 0); 148 | } 149 | 150 | to { 151 | transform: translate3d(0, -100%, 0); 152 | } 153 | } 154 | 155 | @keyframes slide-bottom-in { 156 | from { 157 | transform: translate3d(0, 100%, 0); 158 | } 159 | 160 | to { 161 | transform: translate3d(0, 0, 0); 162 | } 163 | } 164 | 165 | @keyframes slide-bottom-out { 166 | from { 167 | transform: translate3d(0, 0, 0); 168 | } 169 | 170 | to { 171 | transform: translate3d(0, 100%, 0); 172 | } 173 | } 174 | 175 | @keyframes slide-left-in { 176 | from { 177 | transform: translate3d(-100%, 0, 0); 178 | } 179 | 180 | to { 181 | transform: translate3d(0, 0, 0); 182 | } 183 | } 184 | 185 | @keyframes slide-left-out { 186 | from { 187 | transform: translate3d(0, 0, 0); 188 | } 189 | 190 | to { 191 | transform: translate3d(-100%, 0, 0); 192 | } 193 | } 194 | 195 | @keyframes slide-right-in { 196 | from { 197 | transform: translate3d(100%, 0, 0); 198 | } 199 | 200 | to { 201 | transform: translate3d(0, 0, 0); 202 | } 203 | } 204 | 205 | @keyframes slide-right-out { 206 | from { 207 | transform: translate3d(0, 0, 0); 208 | } 209 | 210 | to { 211 | transform: translate3d(100%, 0, 0); 212 | } 213 | } 214 | 215 | 216 | 217 | .slide_topIn, 218 | .slide_bottomIn, 219 | .slide_leftIn, 220 | .slide_rightIn { 221 | animation-timing-function: ease-out; 222 | animation-fill-mode: forwards; 223 | } 224 | 225 | .slide_topOut, 226 | .slide_bottomOut, 227 | .slide_leftOut, 228 | .slide_rightOut { 229 | animation-timing-function: ease-in; 230 | animation-fill-mode: forwards; 231 | } 232 | -------------------------------------------------------------------------------- /site/page/Picker/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Picker } from '@/components/index'; 3 | import { PickerDataSourceType } from '@/components/picker'; 4 | 5 | const PickerPanel = Picker.PickerPanel; 6 | 7 | const cities = ['杭州', '宁波', '温州', '绍兴', '湖州', '嘉兴', '金华', '衢州']; 8 | const cities2 = { 9 | 浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'], 10 | 福建: ['福州', '厦门', '莆田', '三明', '泉州'] 11 | }; 12 | const date = [ 13 | ['周一', '周二', '周三', '周四', '周五'], 14 | ['上午', '下午', '晚上'] 15 | ]; 16 | const date2: PickerDataSourceType = [ 17 | [ 18 | { label: '周一', value: '周一' }, 19 | { label: '周二', value: '周二' }, 20 | { label: '周三', value: '周三' }, 21 | { label: '周四', value: '周四', disabled: true }, 22 | { label: '周五', value: '周五' } 23 | ], 24 | [ 25 | { label: '上午', value: '上午' }, 26 | { label: '下午', value: '下午', disabled: true }, 27 | { label: '晚上', value: '晚上', style: { color: 'skyblue' } } 28 | ] 29 | ]; 30 | 31 | const cascadeData = [ 32 | { 33 | label: '浙江', 34 | value: '浙江', 35 | children: [ 36 | { 37 | label: '杭州', 38 | value: '杭州', 39 | children: [ 40 | { label: '西湖区', value: '西湖区' }, 41 | { label: '余杭区', value: '余杭区' } 42 | ] 43 | }, 44 | { 45 | label: '温州', 46 | value: '温州', 47 | children: [ 48 | { label: '鹿城区', value: '鹿城区' }, 49 | { label: '瓯海区', value: '瓯海区' } 50 | ] 51 | } 52 | ] 53 | }, 54 | { 55 | label: '福建', 56 | value: '福建', 57 | children: [ 58 | { 59 | label: '福州', 60 | value: '福州', 61 | children: [ 62 | { label: '鼓楼区', value: '鼓楼区' }, 63 | { label: '台江区', value: '台江区' } 64 | ] 65 | }, 66 | { 67 | label: '厦门', 68 | value: '厦门', 69 | children: [ 70 | { label: '思明区', value: '思明区' }, 71 | { label: '海沧区', value: '海沧区' } 72 | ] 73 | } 74 | ] 75 | } 76 | ]; 77 | function onChange(v, index) { 78 | console.log('onChange: ', v, 'col: ', index); 79 | } 80 | function onOk(v) { 81 | console.log('onOk: ', v); 82 | } 83 | function onCancel() { 84 | console.log('onCancel'); 85 | } 86 | function onVisibleChange(visible) { 87 | console.log('onVisibleChange', visible); 88 | } 89 | 90 | function PickerDemo() { 91 | const [loading, setLoading] = useState(false); 92 | const [columns, setColumns] = useState([]); 93 | const [value, setValue] = useState(['周三', '晚上']); 94 | 95 | return ( 96 |
97 |
基础用法
98 | 106 | 单列选择 107 | 108 | 114 | 多列选择 115 | 116 | 117 | 加载状态 118 | 119 | 120 | 禁用状态/自定义样式 121 | 122 | { 128 | if (visible && !columns.length) { 129 | setLoading(true); 130 | setTimeout(() => { 131 | setColumns([Object.keys(cities2), cities2['浙江']]); 132 | setLoading(false); 133 | }, 2000); 134 | } 135 | }} 136 | onChange={(values, col) => { 137 | if (col === 0 && values) { 138 | const list = columns.slice(); 139 | list[1] = cities2[values[0]]; 140 | setColumns(list); 141 | } 142 | }} 143 | > 144 | 动态设置选项 145 | 146 | 147 | 级联选择 148 | 149 |
面板用法(演示受控)
150 | ) => { 154 | console.log('v: ', v); 155 | setValue(v); 156 | }} 157 | /> 158 | 159 |
160 | ); 161 | } 162 | export default PickerDemo; 163 | -------------------------------------------------------------------------------- /components/image/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { classnames } from '../_utils/index'; 3 | import { Loading, Icon } from '../index'; 4 | import * as CSS from 'csstype'; 5 | import './index.less'; 6 | 7 | export type ObjectFit = CSS.Property.ObjectFit; 8 | 9 | export interface ImageProps extends React.ImgHTMLAttributes { 10 | src?: string; // 图片地址 11 | width?: number | string; // 图片宽度 12 | height?: number | string; // 图片高度 13 | round?: boolean; // 是否圆角 14 | radius?: number | string; // 圆角大小 15 | lazy?: boolean; // 是否懒加载 16 | fit?: ObjectFit; // 填充模式 17 | errorIcon?: React.ReactNode | string; // 失败时提示图标 18 | onLoad?: ( 19 | event: React.SyntheticEvent | Event 20 | ) => unknown; 21 | onError?: ( 22 | event: React.SyntheticEvent | Event 23 | ) => unknown; 24 | onClick?: ( 25 | event: React.MouseEvent | MouseEvent 26 | ) => unknown; 27 | className?: string; 28 | style?: React.CSSProperties; 29 | } 30 | const prefixCls = 'sty-img'; 31 | 32 | function Image(props: ImageProps) { 33 | const { 34 | src, 35 | width, 36 | height, 37 | round, 38 | radius, 39 | lazy, 40 | fit, 41 | errorIcon, 42 | onLoad: onPropsLoad, 43 | onError: onPropsError, 44 | onClick: onPropsClick, 45 | className, 46 | style, 47 | ...other 48 | } = props; 49 | const [loading, setLoading] = useState(true); 50 | const [error, setError] = useState(false); 51 | const imgRef = useRef(); 52 | 53 | const boxSty: React.CSSProperties = { 54 | width, 55 | height, 56 | borderRadius: radius || (round ? '50%' : 0), 57 | ...style 58 | }; 59 | 60 | const imgSty: React.CSSProperties = { 61 | objectFit: fit, 62 | opacity: loading || error ? 0 : 1 63 | }; 64 | 65 | useEffect(() => { 66 | if (lazy) { 67 | if ('IntersectionObserver' in window) { 68 | const target = imgRef.current; 69 | let observer = new IntersectionObserver(entries => { 70 | entries.forEach(item => { 71 | if (item.isIntersecting) { 72 | let image = document.createElement('img'); 73 | image.src = target.dataset.src; 74 | image.onload = onLoad; 75 | image.onerror = function (event: Event) { 76 | onError(event); 77 | }; 78 | image.onclick = onPropsClick; 79 | target.src = target.dataset.src; 80 | observer.unobserve(target); 81 | image = null; 82 | } 83 | }); 84 | }); 85 | observer.observe(target); 86 | return () => { 87 | observer = null; 88 | }; 89 | } else { 90 | console.log('浏览器暂不支持IntersectionObserver属性,请用谷歌'); 91 | } 92 | } 93 | }, []); 94 | 95 | function onLoad( 96 | event: React.SyntheticEvent | Event 97 | ) { 98 | setLoading(false); 99 | onPropsLoad(event); 100 | } 101 | function onError( 102 | event: React.SyntheticEvent | Event 103 | ) { 104 | setLoading(false); 105 | setError(true); 106 | onPropsError(event); 107 | } 108 | 109 | function renderImage() { 110 | if (lazy && 'IntersectionObserver' in window) { 111 | return ; 112 | } 113 | return ( 114 | 122 | ); 123 | } 124 | function renderPlaceholder() { 125 | if (loading) { 126 | return ( 127 |
128 | 129 |
130 | ); 131 | } 132 | if (error) { 133 | return ( 134 |
135 | {typeof errorIcon === 'string' ? ( 136 | 137 | ) : ( 138 | errorIcon 139 | )} 140 |
141 | ); 142 | } 143 | } 144 | 145 | return ( 146 |
147 | {renderImage()} 148 | {renderPlaceholder()} 149 |
150 | ); 151 | } 152 | 153 | Image.defaultProps = { 154 | errorIcon: 'photo-fail', 155 | onLoad: () => undefined, 156 | onError: () => undefined, 157 | onClick: () => undefined 158 | }; 159 | export default Image; 160 | -------------------------------------------------------------------------------- /components/date-picker/panels/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import MonthPanel from './MonthPanel'; 3 | import DecadePanel from './DecadePanel'; 4 | import YearPanel from './YearPanel'; 5 | import DatePanel from './DatePanel'; 6 | import { DatePanelProps, PanelMode, PickerValue } from '../interface'; 7 | import generateConfig from '../generate'; 8 | import { Dayjs } from 'dayjs'; 9 | import { isEqual, formatDate } from '../_utils/dateUtils'; 10 | import { classnames } from '@/components/_utils'; 11 | 12 | function DatePanelIndex(props: DatePanelProps) { 13 | const { 14 | prefixCls, 15 | value: valueProps, 16 | defaultValue, 17 | generateConfig, 18 | picker, 19 | isRange, 20 | onSelect: onSelectProps, 21 | onChange: onChangeProps, 22 | onPanelChange: onPanelChangeProps, 23 | renderExtraFooter, 24 | className, 25 | style 26 | } = props; 27 | const [value, setValue] = useState>(defaultValue); 28 | const [viewDate, setViewDate] = useState(() => { 29 | if (isRange) { 30 | return valueProps?.[0] || defaultValue?.[0] || generateConfig.getNow(); 31 | } 32 | return valueProps || defaultValue || generateConfig.getNow(); 33 | }); 34 | const [mergedMode, setMergedMode] = useState(picker); 35 | 36 | useEffect(() => { 37 | setValue(valueProps || defaultValue); 38 | }, [valueProps]); 39 | 40 | function onSelect(v) { 41 | setViewDate(v); 42 | if (picker === mergedMode) { 43 | let newValue: PickerValue; 44 | if (isRange) { 45 | newValue = [value?.[0], value?.[1]]; 46 | const index = v > newValue[0] ? 1 : 0; 47 | newValue[index] = v; 48 | if ( 49 | !isEqual(generateConfig, value?.[0], newValue[0]) || 50 | !isEqual(generateConfig, value?.[1], newValue[1]) 51 | ) { 52 | onChangeProps(newValue); 53 | } 54 | } else { 55 | newValue = v; 56 | if (!isEqual(generateConfig, value, newValue)) { 57 | onChangeProps(newValue); 58 | } 59 | } 60 | onSelectProps(v); 61 | 62 | // 如果是非受控模式,内部处理 63 | if (valueProps === undefined) { 64 | setValue(newValue); 65 | } 66 | } 67 | } 68 | 69 | const onInternalPanelChange = (newMode: PanelMode, viewValue: Dayjs) => { 70 | setMergedMode(newMode); 71 | 72 | onPanelChangeProps(newMode, viewValue); 73 | }; 74 | 75 | const pickerProps = { 76 | ...props, 77 | generateConfig, 78 | picker, 79 | viewDate, 80 | value, 81 | mode: mergedMode, 82 | onSelect, 83 | onViewDateChange: setViewDate, 84 | onPanelChange: onInternalPanelChange 85 | }; 86 | 87 | let panelNode: React.ReactNode; 88 | 89 | switch (mergedMode) { 90 | case 'month': { 91 | panelNode = {...pickerProps} />; 92 | break; 93 | } 94 | case 'year': { 95 | panelNode = {...pickerProps} />; 96 | break; 97 | } 98 | case 'decade': { 99 | panelNode = {...pickerProps} />; 100 | break; 101 | } 102 | default: 103 | panelNode = {...pickerProps} />; 104 | } 105 | 106 | function renderRangeFooter() { 107 | let formatString = 'YYYY-MM-DD'; 108 | if (picker === 'month') { 109 | formatString = 'YYYY-MM'; 110 | } 111 | if (picker === 'year') { 112 | formatString = 'YYYY'; 113 | } 114 | return ( 115 |
121 |
setViewDate(value?.[0])}> 122 | {formatDate({ 123 | date: value?.[0], 124 | format: formatString 125 | })} 126 |
127 |
~
128 |
setViewDate(value?.[1])}> 129 | {formatDate({ 130 | date: value?.[1], 131 | format: formatString 132 | })} 133 |
134 |
135 | ); 136 | } 137 | 138 | return ( 139 |
140 | {panelNode} 141 | {renderExtraFooter && renderExtraFooter()} 142 | {isRange && renderRangeFooter()} 143 |
144 | ); 145 | } 146 | 147 | DatePanelIndex.defaultProps = { 148 | prefixCls: 'sty-date-panel', 149 | picker: 'date', 150 | generateConfig, 151 | onSelect: () => undefined, 152 | onChange: () => undefined, 153 | onPanelChange: () => undefined 154 | }; 155 | 156 | export default DatePanelIndex; 157 | --------------------------------------------------------------------------------