├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------