├── .babelrc ├── .gitattributes ├── .gitignore ├── .gitlab-ci.yml ├── .npmignore ├── LICENSE ├── README-en_US.md ├── README.md ├── babel.config.js ├── build ├── build.js ├── config.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.lib.conf.js └── webpack.prod.conf.js ├── ci └── deploy_stage.sh ├── dist ├── demo.png ├── favicon.ico └── index.html ├── index.html ├── lib └── index.js ├── package-lock.json ├── package.json ├── public ├── demo.png ├── favicon.ico └── index.html ├── src ├── components │ ├── calendar │ │ ├── index.tsx │ │ └── style.styl │ ├── datetimePicker │ │ ├── index.tsx │ │ └── style.styl │ ├── index.js │ └── timePicker │ │ ├── index.tsx │ │ └── style.styl ├── examples │ └── index.tsx ├── index.tsx ├── language │ ├── cn.ts │ ├── en.ts │ └── index.ts ├── style │ ├── common.css │ ├── common.styl │ ├── reset.css │ └── reset.styl └── utils │ ├── constant.ts │ ├── eq.ts │ ├── type.ts │ └── util.ts ├── tsconfig.json ├── type.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.tsx linguist-language=TypeScript 2 | *.js linguist-language=React 3 | -------------------------------------------------------------------------------- /.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 | dist 13 | lib 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - deploy 3 | 4 | deploy_develop: 5 | stage: deploy 6 | script: 7 | - ci/deploy_develop.sh 8 | only: 9 | - develop 10 | 11 | deploy_stage: 12 | stage: deploy 13 | script: 14 | - chmod +x ci/deploy_stage.sh 15 | - ci/deploy_stage.sh 16 | only: 17 | - master 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | examples 4 | public 5 | dist 6 | ci 7 | build 8 | src 9 | .svn 10 | .gitignore 11 | .travis.yml 12 | .gitlab-ci.yml 13 | branches 14 | tags 15 | trunk 16 | test.html 17 | 18 | # local env files 19 | .env.local 20 | .env.*.local 21 | 22 | # Log files 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Editor directories and files 28 | .idea 29 | .vscode 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw* 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TangSY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-en_US.md: -------------------------------------------------------------------------------- 1 | [![version](https://img.shields.io/npm/v/react-hash-calendar.svg)](https://www.npmjs.com/package/react-hash-calendar) 2 | [![download](https://img.shields.io/npm/dt/react-hash-calendar.svg)](https://www.npmjs.com/package/react-hash-calendar) 3 | ![license](https://img.shields.io/badge/license-MIT-blue.svg) 4 | [![author](https://img.shields.io/badge/author-HashTang-orange.svg)](https://www.hxkj.vip) 5 | 6 | [简体中文](https://github.com/TangSY/react-hash-calendar/blob/master/README-zh_CN.md) | English 7 | 8 | ## Using Effects 9 | 10 | ![calendar.gif](https://www.hxkj.vip/demo/calendar/calendar.gif) 11 | ![dot.gif](https://www.hxkj.vip/demo/calendar/dot.gif) 12 | ![week.gif](https://www.hxkj.vip/demo/calendar/week.gif) 13 | 14 | The same calendar for Vue:[https://github.com/TangSY/vue-hash-calendar](https://github.com/TangSY/vue-hash-calendar) 15 | 16 | # react-hash-calendar 17 | 18 | - Support gesture sliding operation 19 | - Slide up and down to switch weekly / monthly mode 20 | 21 | > [week mode] slide left and right to switch the previous week / next week 22 | 23 | > [month mode] slide left and right to switch the previous month / next month 24 | 25 | ## Install 26 | 27 | ``` 28 | npm i react-hash-calendar 29 | ``` 30 | ``` 31 | import { ReactHashCalendar } from 'react-hash-calendar' 32 | 33 | function App () { 34 | return ( 35 |
36 | 37 |
38 | ); 39 | } 40 | 41 | export default App; 42 | ``` 43 | 44 | ## Demo 45 | 46 | ![demo_qrcode.png](https://www.hxkj.vip/demo/react-calendar/demo.png) 47 | 48 | online demo:[https://www.hxkj.vip/demo/react-calendar/](https://www.hxkj.vip/demo/react-calendar/) 49 | 50 | - 🎉 can you give me a star? 🎉 51 | 52 | ### github link:[https://github.com/TangSY/react-hash-calendar](https://github.com/TangSY/react-hash-calendar) 53 | 54 | ## API 55 | 56 | | name | describle | type | default | 57 | | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------ | :------------: | 58 | | visible | To control the display or hide of calendar components | boolean | false | 59 | | onVisibleChange | params { visible } | (visible: boolean) => void | - | 60 | | scrollChangeDate | Controls whether the selected date is modified when sliding | boolean | true | 61 | | model | What form is the calendar component displayed. Inline: the way to inline. Dialog: pop up mode | string | inline | 62 | | defaultDatetime | -- | Date | now | 63 | | format | The date format returned by the callback event when confirming the date. eg: "YY / mm / DD HH: mm" , "MM DD,YY at hh:mm F" | string | YY/MM/DD hh:mm | 64 | | weekStart | Use the day of the week as the starting week of each week in the calendar. choose: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] | string | sunday | 65 | | pickerType | Selector Type. choose: 'datetime', 'date', 'time' | string | datetime | 66 | | showTodayButton | -- | boolean | true | 67 | | isShowWeekView | -- | boolean | false | 68 | | isShowAction | -- | boolean | true | 69 | | disabledWeekView | -- | boolean | false | 70 | | disabledDate | Set the disabled status of the date (returned true to disabled) | Function | - | 71 | | disabledScroll | Set the no sliding direction of the calendar. choose: 'left', 'right', 'up', 'down', 'horizontal', 'vertical', true, false] | string | '' | 72 | | markDate | he date to be marked can be grouped according to different colors and mark types (no grouped, the default is blue). eg:[{color: 'red',date: ['2019/02/25']},{color: 'blue',type: 'dot',date: ['2019/01/20']},'2019/03/20'] | Array | [] | 73 | | markType | Mark pattern type. choose: 'dot', 'circle', 'dot+circle' | string | dot | 74 | | minuteStep | -- | number | 1 | 75 | | lang | Language. choose: 'CN', 'EN' | string | CN | 76 | | dateClickCallback | -- | (date: Date \| string) => void | - | 77 | | dateConfirmCallback | -- | (date: Date \| string) => void | - | 78 | | touchStartCallback | -- | (event: React.TouchEvent) => void | - | 79 | | touchMoveCallback | -- | (event: React.TouchEvent) => void | - | 80 | | touchEndCallback | -- | (event: React.TouchEvent) => void | - | 81 | | slideChangeCallback | -- | (direction: string) => void | - | 82 | | weekSlot | Customize week content and style。 | (week: string) => React.ReactNode | - | 83 | | daySlot | Customize date content and style | (date, extendAttr) => React.ReactNode | - | 84 | | todaySlot | Customize today button content and style | () => React.ReactNode | - | 85 | | confirmSlot | Customize dconfirmate button content and style | () => React.ReactNode | - | 86 | | actionSlot | Customize action content and style | () => React.ReactNode | - | 87 | 88 | ### Other 89 | 90 | - If there are other problems or incompatible functions. Can communicate by email 't@tsy6.com', or GitHub submits the issue. 91 | 92 | ### Sponsor 93 | 94 | ![pay.jpg](https://www.hxkj.vip/demo/calendar/pay.jpg) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![version](https://img.shields.io/npm/v/react-hash-calendar.svg)](https://www.npmjs.com/package/react-hash-calendar) 2 | [![download](https://img.shields.io/npm/dt/react-hash-calendar.svg)](https://www.npmjs.com/package/react-hash-calendar) 3 | ![license](https://img.shields.io/badge/license-MIT-blue.svg) 4 | [![author](https://img.shields.io/badge/author-HashTang-orange.svg)](https://www.hxkj.vip) 5 | 6 | 简体中文 | [English](https://github.com/TangSY/react-hash-calendar/blob/master/README-en_US.md) 7 | 8 | # 按照惯例,先上效果图 9 | 10 | ![calendar.gif](https://www.hxkj.vip/demo/calendar/calendar.gif) 11 | ![dot.gif](https://www.hxkj.vip/demo/calendar/dot.gif) 12 | ![week.gif](https://www.hxkj.vip/demo/calendar/week.gif) 13 | 14 | vue 版本同款日历:[https://github.com/TangSY/vue-hash-calendar](https://github.com/TangSY/vue-hash-calendar) 15 | 16 | # react-hash-calendar 17 | 18 | - 支持手势滑动操作 19 | - 上下滑动 切换 周/月 模式 20 | > 【周模式中】 左右滑动可切换 上一周/下一周 21 | > 【月模式中】 左右滑动可切换 上一月/下一月 22 | 23 | # 安装使用说明 24 | 25 | ``` 26 | npm i react-hash-calendar 27 | ``` 28 | ``` 29 | import { ReactHashCalendar } from 'react-hash-calendar' 30 | 31 | function App () { 32 | return ( 33 |
34 | 35 |
36 | ); 37 | } 38 | 39 | export default App; 40 | ``` 41 | 42 | # Demo 43 | 44 | ![demo_qrcode.png](https://www.hxkj.vip/demo/react-calendar/demo.png) 45 | 46 | 或者请用浏览器的手机模式查看:[https://www.hxkj.vip/demo/react-calendar/](https://www.hxkj.vip/demo/react-calendar/) 47 | 48 | - 🎉 觉得好用可以给一个 star 哦~~ 🎉 49 | 50 | ## github 地址:[https://github.com/TangSY/react-hash-calendar](https://github.com/TangSY/react-hash-calendar) 51 | 52 | # API 53 | 54 | | 属性 | 说明 | 类型 | 默认 | 55 | | :------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----------------------------------: | :------------: | 56 | | visible | 控制日历组件的显示或隐藏,需使用 `.sync` 修饰符 | boolean | false | 57 | | onVisibleChange | 日历显示状态改变时调用,参数为 { visible } | (visible: boolean) => void | - | 58 | | scrollChangeDate | 控制滑动的时候是否修改选中的日期 | boolean | true | 59 | | model | 日历组件以哪种形式展示。inline:内联的方式。dialog:弹窗的方式 | string | inline | 60 | | defaultDatetime | 指定默认时间。 | Date | now | 61 | | format | 确认日期时,回调事件返回的日期格式。如“YY/MM/DD hh:mm” 、“YY 年 MM 月第 DD 天,当前时间 hh 时 mm 分”、“MM DD,YY at hh:mm F” | string | YY/MM/DD hh:mm | 62 | | weekStart | 以星期几作为日历每一周的起始星期。可选['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] | string | sunday | 63 | | pickerType | 选择器类型 datetime:日期+时间 date:日期 time:时间 | string | datetime | 64 | | showTodayButton | 是否显示返回今日按钮 | boolean | true | 65 | | isShowWeekView | 是否以周视图展示组件 | boolean | false | 66 | | isShowAction | 是否显示日历组件操作栏(标题栏) | boolean | true | 67 | | disabledWeekView | 禁用周视图(设置为 true 后,无法上下滑动进行周/月切换) | boolean | false | 68 | | disabledDate | 设置日期的禁用状态,参数为当前日期,要求返回 boolean (禁用该日期需返回 true) | Function | - | 69 | | disabledScroll | 设置日历的禁止滑动方向。可选['left', 'right', 'up', 'down', 'horizontal', 'vertical', 'all', ''] 。可取其一控制单个方向。 | string | '' | 70 | | markDate | 需要被标记的日期,可按不同颜色不同标记类型分组标记(不分组默认蓝色)。如:[{color: 'red',date: ['2019/02/25']},{color: 'blue',type: 'dot',date: ['2019/01/20']},'2019/03/20'] | Array | [] | 71 | | markType | 标记图案类型 dot:小圆点(日期下方小圆点标记) circle:小圆圈(日期被小圆圈包围) dot+circle:同时使用小圆点与圆圈标记 | string | dot | 72 | | minuteStep | 间隔时间。(分钟的步长) | number | 1 | 73 | | lang | 选择的语言版本。可选值:['CN', 'EN'] | string | CN | 74 | | dateClickCallback | 日历被点击时调用,参数为 { date }。(返回的日期格式取决于 format 属性) | (date: Date \| string) => void | - | 75 | | dateConfirmCallback | 点击确定按钮时调用,参数为 { date }。(返回的日期格式取决于 format 属性) | (date: Date \| string) => void | - | 76 | | touchStartCallback | 开始滑动日历时调用,参数为 { event } | (event: React.TouchEvent) => void | - | 77 | | touchMoveCallback | 日历滑动中时调用,参数为 { event } | (event: React.TouchEvent) => void | - | 78 | | touchEndCallback | 日历滑动结束时调用,参数为 { event } | (event: React.TouchEvent) => void | - | 79 | | slideChangeCallback | 日历滑动的方向,参数为 { direction }。(返回值有 right、left、up、down 其中之一) | (direction: string) => void | - | 80 | | weekSlot | 自定义星期内容。例如可用于自定义星期样式等等,参数为 { week }。 | (week: string) => React.ReactNode | - | 81 | | daySlot | 自定义日期内容。例如可用于添加农历之类的。参数为 { date, extendAttr },其中 extendAttr 参数包含 `isMarked`(该日期是否被标记)、`isDisabledDate`(该日期是否被禁用)、`isToday`(该日期是否为今天)、`isChecked`(该日期是否被选中)、`isCurrentMonthDay`(该日期是否为本月日期)、`isFirstDayOfMonth`(该日期是否为当月第一天),可用于一些特殊需求 | (date, extendAttr) => React.ReactNode | - | 82 | | todaySlot | 自定义 "今天" 按钮文字内容以及样式 | () => React.ReactNode | - | 83 | | confirmSlot | 自定义 "确定" 按钮文字内容以及样式 | () => React.ReactNode | - | 84 | | actionSlot | 自定义操作栏(标题栏)内容以及样式 | () => React.ReactNode | - | 85 | 86 | ## Other 87 | 88 | - 如果有其他问题, 或者功能上不兼容的。可以邮件沟通 t@tsy6.com,或者 github 提交 issue。 89 | 90 | ## 赞助 91 | 92 | ![pay.jpg](https://www.hxkj.vip/demo/calendar/pay.jpg) 93 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-env", "@babel/preset-react"] 3 | }; -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = '' 2 | 3 | const webpack = require('webpack') 4 | const webpackConf = require('./webpack.lib.conf') 5 | const ora = require('ora') 6 | const chalk = require('chalk') 7 | 8 | const spinner = ora('building start').start() 9 | 10 | webpack(webpackConf, function (error, stats) { 11 | spinner.stop() 12 | if (error) throw error 13 | process.stdout.write(stats.toString({ 14 | colors: true, 15 | modules: false, 16 | children: false, 17 | chunks: false, 18 | chunkModules: false 19 | }) + '\n\n') 20 | 21 | if (stats.hasErrors()) { 22 | console.log(chalk.red(' Build failed with errors.\n')) 23 | process.exit(1) 24 | } 25 | 26 | console.log(chalk.cyan(' Build complete.\n')) 27 | }) 28 | -------------------------------------------------------------------------------- /build/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = {} 4 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolve = p => path.resolve(__dirname, p) 3 | 4 | module.exports = { 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'awesome-typescript-loader', 10 | exclude: /node_modules/ 11 | }, 12 | { 13 | test: /\.jsx?$/, 14 | exclude: /node_modules/, 15 | loader: "babel-loader" 16 | }, 17 | { 18 | test: /\.styl$/, 19 | exclude: /node_modules/, 20 | use: ['style-loader', 'css-loader', 'stylus-loader'] 21 | }, 22 | { 23 | test: /\.(jpg|png|svg)$/, 24 | loader: ["file-loader"] 25 | }, 26 | { 27 | test: /\.(woff|woff2|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 28 | loader: "url-loader?limit=10000" 29 | }, 30 | { 31 | test: /\.css$/, 32 | use: ['style-loader', 'css-loader'] 33 | } 34 | ] 35 | }, 36 | 37 | resolve: { 38 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'] 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const baseConf = require('./webpack.base.conf') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const merge = require('webpack-merge') 4 | 5 | const path = require('path') 6 | const resolve = p => path.resolve(__dirname, p) 7 | 8 | module.exports = merge(baseConf, { 9 | entry: './src/index.tsx', 10 | output: { 11 | filename: 'bundle.js', 12 | path: resolve('../dist') 13 | }, 14 | devtool: "source-map", 15 | devServer: { 16 | contentBase: resolve('../'), 17 | port: 3000, 18 | open: false, 19 | hot: false 20 | }, 21 | plugins: [ 22 | new HtmlWebpackPlugin({ 23 | template: resolve('../public/index.html'), 24 | filename: resolve('../index.html'), 25 | alwaysWriteToDisk: true, 26 | hash: true, 27 | }) 28 | ] 29 | }) 30 | 31 | -------------------------------------------------------------------------------- /build/webpack.lib.conf.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const baseConf = require('./webpack.base.conf') 3 | const path = require('path') 4 | const resolve = p => path.resolve(__dirname, p) 5 | module.exports = merge(baseConf, { 6 | entry: './src/components/index', 7 | output: { 8 | filename: 'index.js', 9 | path: resolve('../lib'), 10 | library: 'react-hash-calendar', 11 | libraryTarget: 'umd' 12 | }, 13 | externals: { 14 | react: { 15 | root: 'React', 16 | commonjs2: 'react', 17 | commonjs: 'react', 18 | amd: 'react' 19 | }, 20 | 'react-dom': { 21 | root: 'ReactDOM', 22 | commonjs2: 'react-dom', 23 | commonjs: 'react-dom', 24 | amd: 'react-dom' 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const baseConf = require('./webpack.base.conf') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | var { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const copyWebpackPlugin = require('copy-webpack-plugin'); 5 | const merge = require('webpack-merge') 6 | 7 | const path = require('path') 8 | const resolve = p => path.resolve(__dirname, p) 9 | 10 | module.exports = merge(baseConf, { 11 | entry: './src/index.tsx', 12 | output: { 13 | filename: "[name].[contenthash].js", 14 | path: resolve('../dist') 15 | }, 16 | plugins: [ 17 | new CleanWebpackPlugin(), 18 | new copyWebpackPlugin({ 19 | patterns: 20 | [{ 21 | from: resolve('../public'),//打包的静态资源目录地址 22 | to: resolve('../dist') //打包到dist下面的public 23 | }] 24 | }), 25 | new HtmlWebpackPlugin({ 26 | template: resolve('../public/index.html') 27 | }) 28 | ] 29 | }) 30 | 31 | -------------------------------------------------------------------------------- /ci/deploy_stage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm install 3 | npm run build 4 | cd ./dist 5 | tar -zcvf calendar.tar.gz * 6 | rm -rf /usr/share/nginx/hxkj/dist/demo/react-calendar/* 7 | mv calendar.tar.gz /usr/share/nginx/hxkj/dist/demo/react-calendar/ 8 | cd /usr/share/nginx/hxkj/dist/demo/react-calendar/ 9 | tar -zxvf calendar.tar.gz 10 | rm -f calendar.tar.gz -------------------------------------------------------------------------------- /dist/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSY/react-hash-calendar/875b1411cc4daef211f4ddecc879639f3b2ffc9c/dist/demo.png -------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSY/react-hash-calendar/875b1411cc4daef211f4ddecc879639f3b2ffc9c/dist/favicon.ico -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 19 | 28 | React App 29 | 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 19 | 28 | React App 29 | 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports["react-hash-calendar"]=t(require("react")):e["react-hash-calendar"]=t(e.React)}(window,(function(e){return function(e){var t={};function n(a){if(t[a])return t[a].exports;var r=t[a]={i:a,l:!1,exports:{}};return e[a].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,a){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(a,r,function(t){return e[t]}.bind(null,r));return a},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=12)}([function(t,n){t.exports=e},function(e,t,n){var a; 2 | /*! 3 | Copyright (c) 2018 Jed Watson. 4 | Licensed under the MIT License (MIT), see 5 | http://jedwatson.github.io/classnames 6 | */!function(){"use strict";var n={}.hasOwnProperty;function r(){for(var e=[],t=0;t=0&&h.splice(t,1)}function y(e){var t=document.createElement("style");return e.attrs.type="text/css",g(t,e.attrs),p(e,t),t}function g(e,t){Object.keys(t).forEach((function(n){e.setAttribute(n,t[n])}))}function v(e,t){var n,a,r,o;if(t.transform&&e.css){if(!(o=t.transform(e.css)))return function(){};e.css=o}if(t.singleton){var i=l++;n=c||(c=y(t)),a=k.bind(null,n,i,!1),r=k.bind(null,n,i,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=function(e){var t=document.createElement("link");return e.attrs.type="text/css",e.attrs.rel="stylesheet",g(t,e.attrs),p(e,t),t}(t),a=D.bind(null,n,t),r=function(){m(n),n.href&&URL.revokeObjectURL(n.href)}):(n=y(t),a=S.bind(null,n),r=function(){m(n)});return a(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;a(e=t)}else r()}}e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(t=t||{}).attrs="object"==typeof t.attrs?t.attrs:{},t.singleton||(t.singleton=i()),t.insertInto||(t.insertInto="head"),t.insertAt||(t.insertAt="bottom");var n=f(e,t);return d(n,t),function(e){for(var a=[],r=0;r=12?"pm":"am").replace(/ss/g,d[u]||u+"").replace(/mm/g,d[h]||h+"").replace(/hh/g,l>12&&t.includes("F")?l-12+"":t.includes("F")?l+"":d[l]||l+"").replace(/DD/g,d[c]||c+"").replace(/MM/g,"EN"===a?r.MONTH[s-1]:d[s]||s)},p=n(1),m=n.n(p),y=(n(5),u=function(e,t){return(u=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}u(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),g=function(){return(g=Object.assign||function(e){for(var t,n=1,a=arguments.length;n15&&(d=u.lastIndexOf(e.day));var f=Math.ceil((d+1)/7)-1;t.setState({calendarY:-h*f,isShowWeek:!0,calendarGroupHeight:h});var p,m=7*f,y=m+7;p=i[1].slice(m,y);var g=0,w=!1;for(var b in p)p[b].day===e.day&&(g=parseInt(b));t.setState({selectedDayIndex:g});var k,S=p[0],D=p[6];D.dayMath.abs(d)){if(u<0&&!t.isCanScroll("left")||u>0&&!t.isCanScroll("right"))return;t.setState({touch:{x:u/s,y:0}})}else{if(l||d<0&&!t.isCanScroll("up")||d>0&&!t.isCanScroll("down"))return;t.setState({touch:{x:0,y:d/i}})}},t.touchEnd=function(e){var n=t.state,a=n.touch,r=n.isShowWeek,o=n.transitionDuration,i=n.calendarRef,s=i&&i.offsetHeight||0,c=t.props,l=c.slideChangeCallback,h=c.touchEndCallback;h&&h(e),t.setState({isTouching:!1}),Math.abs(a.x)>Math.abs(a.y)&&Math.abs(a.x)>.2&&t.setState({currentChangeIsScroll:!0},(function(){a.x>0?(l&&l("right"),t.getLastMonth(),r&&setTimeout((function(){t.setState({isTouching:!0}),t.setState({currentChangeIsScroll:!0},(function(){t.getLastWeek()}))}),1e3*o)):a.x<0&&(l&&l("left"),t.getNextMonth(),r&&setTimeout((function(){t.setState({isTouching:!0}),t.setState({currentChangeIsScroll:!0},(function(){t.getNextWeek()}))}),1e3*o))})),Math.abs(a.y)>Math.abs(a.x)&&Math.abs(a.y*s)>50?a.y>0&&r?(l&&l("down"),t.showMonth()):a.y<0&&!r&&(l&&l("up"),t.showWeek()):t.setState({touch:{x:0,y:0}})},t.getLastWeek=function(){var e=t.state,n=e.lastWeek,a=e.selectedDayIndex,r=e.currentChangeIsScroll,o=t.props.scrollChangeDate,i=n[a];t.showWeek(i),t.formatDisabledDate(i)||(o||!r?t.setState({checkedDate:i}):t.setState({currentChangeIsScroll:!1}))},t.getNextWeek=function(){var e=t.state,n=e.nextWeek,a=e.selectedDayIndex,r=e.currentChangeIsScroll,o=t.props.scrollChangeDate,i=n[a];t.showWeek(i),t.formatDisabledDate(i)||(o||!r?t.setState({checkedDate:i}):t.setState({currentChangeIsScroll:!1}))},t.getLastMonth=function(){var e=t.state,n=e.yearOfCurrentShow,a=e.monthOfCurrentShow,r=e.lastMonthYear,o=e.lastMonth,i=e.isLastWeekInCurrentMonth,s=e.isShowWeek;t.setState((function(e){return{translateIndex:e.translateIndex+1}}));var c=n,l=a;if(i||(c=r,l=o),s&&i)return null;t.setState({yearOfCurrentShow:c,monthOfCurrentShow:l}),t.calculateCalendarOfThreeMonth(c,l)},t.getNextMonth=function(){var e=t.state,n=e.yearOfCurrentShow,a=e.monthOfCurrentShow,r=e.nextMonthYear,o=e.nextMonth,i=e.isNextWeekInCurrentMonth,s=e.isShowWeek;t.setState((function(e){return{translateIndex:e.translateIndex-1}}));var c=n,l=a;if(i||(c=r,l=o),s&&i)return null;t.setState({yearOfCurrentShow:c,monthOfCurrentShow:l}),t.calculateCalendarOfThreeMonth(c,l)},t.isCanScroll=function(e){var n=t.props.disabledScroll;return!{up:["all","up","vertical"],down:["all","down","vertical"],left:["all","left","horizontal"],right:["all","right","horizontal"]}[e].some((function(e){return e===n}))},t.formatDisabledDate=function(e){return(0,t.props.disabledDate)(new Date(e.year+"/"+(e.month+1)+"/"+e.day))},t.isFirstDayOfMonth=function(e,n){return 1===e.day&&!t.isNotCurrentMonthDay(e,n)},t.isNotCurrentMonthDay=function(e,n){var a=t.state.calendarOfMonth;if(!a[n])return!1;var r=a[n][15];return!!r&&(e.year!==r.year||e.month!==r.month)},t.markDateColor=function(e,n){var a=t.state,r=a.markDateTypeObj,o=a.markDateColorObj,i=e.year+"/"+t.fillNumber(e.month+1)+"/"+t.fillNumber(e.day);return-1===(r[i]||"").indexOf(n)?"":o[i]},t.fillNumber=function(e){return e>9?e:"0"+e},t.isToday=function(e){return w===e.year&&b===e.month&&k===e.day},t.isCheckedDay=function(e){if(t.formatDisabledDate(e))return!1;var n=t.state.checkedDate;return n.year===e.year&&n.month===e.month&&n.day===e.day},t.clickCalendarDay=function(e,n){if(n&&!t.formatDisabledDate(n)){var a=t.props.dateClickCallback;t.setState({checkedDate:{year:n.year,month:n.month,day:n.day}},(function(){var e=t.state,a=e.lastMonth,r=e.lastMonthYear,o=e.nextMonth,i=e.nextMonthYear,s=e.isShowWeek;n.month===a&&n.year===r&&t.getLastMonth(),n.month===o&&n.year===i&&t.getNextMonth(),s&&t.showWeek()})),a&&a(n)}},t.calculateCalendarOfThreeMonth=function(e,n){void 0===e&&(e=w),void 0===n&&(n=b);var a=t.state,r=a.currentChangeIsScroll,o=a.checkedDate,i=t.props.scrollChangeDate,s=t.getNearYearAndMonth(e,n),c=s.lastMonthYear,l=s.lastMonth,h=s.nextMonthYear,u=s.nextMonth;t.setState({lastMonthYear:c,lastMonth:l,nextMonthYear:h,nextMonth:u});var d=t.calculateCalendarOfMonth(c,l),f=t.calculateCalendarOfMonth(e,n),p=t.calculateCalendarOfMonth(h,u),m=[];if(m.push(d,f,p),t.setState({calendarOfMonth:m,calendarOfMonthShow:JSON.parse(JSON.stringify(m))}),i||!r){var y,g=o.day;(g>30||g>28&&1===n)&&(g=t.daysOfMonth(e)[n]),y={day:g,year:e,month:n},t.formatDisabledDate(y)||t.setState({checkedDate:{day:y.day,year:e,month:n}})}else t.setState({currentChangeIsScroll:!1})},t.calculateCalendarOfMonth=function(e,n){void 0===e&&(e=w),void 0===n&&(n=b);var a=[],r=t.state,o=r.weekStartIndex,i=r.calendarDaysTotalLength,s=t.getNearYearAndMonth(e,n),c=s.lastMonthYear,l=s.lastMonth,h=s.nextMonthYear,u=s.nextMonth,d=t.getDayOfWeek(e,n),f=t.daysOfMonth(e)[l];d2*l&&(g=2*l),0===n){var v=2-Math.round(g/l);f=T(T({},s),{hours:v})}else{var w=2-Math.round(g/l);f=T(T({},s),{minutes:w*r})}t.setState({checkedDate:f}),o&&o(f),u.style.webkitTransition="transform 300ms",u.style.webkitTransform="translate3d(0px,"+g+"px,0px)"},t.isBeSelectedTime=function(e,n){var a=t.state.checkedDate;return 0===n&&e===a.hours||1===n&&e===a.minutes},t.fillNumber=function(e){return e>9?e:"0"+e},t}return M(t,e),t.prototype.componentDidMount=function(){var e=this.props,t=e.defaultTime,n=e.timeChangeCallback,a=this.state.checkedDate;if(this.setState({hashID:["time"+Math.floor(1e6*Math.random()),"time"+Math.floor(1e6*Math.random())],hashClass:"time_item_"+Math.floor(1e6*Math.random())}),t){var r=T(T({},a),{hours:t.getHours(),minutes:t.getMinutes()});this.setState({checkedDate:r}),n&&n(r)}},t.prototype.componentDidUpdate=function(e,t){var n=this,a=e.show,r=this.props.show;r!==a&&r&&setTimeout((function(){n.initTimeArray()}))},t.prototype.render=function(){var e=this,t=this.props.show,n=this.state,a=n.timeArray,o=n.hashID,i=n.hashClass;return t?r.a.createElement("div",{className:"time_body"},r.a.createElement("div",{className:"time_group"},function(t){return t&&t.map((function(t,n){return r.a.createElement("div",{className:"time_content",id:o[n],key:n,onTouchStart:e.timeTouchStart,onTouchMove:function(t){e.timeTouchMove(t,n)},onTouchEnd:function(t){e.timeTouchEnd(t,n)}},(a=n,t.map((function(t,n){return r.a.createElement("div",{className:m()("time_item",{time_item_show:e.isBeSelectedTime(t,a)},i),key:n},e.fillNumber(t))}))));var a}))}(a))):null},t.defaultProps=x,t}(r.a.Component),E=(n(10),function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(t,n)};return function(t,n){function a(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(a.prototype=n.prototype,new a)}}()),N=function(){return(N=Object.assign||function(e){for(var t,n=1,a=arguments.length;n9?e:"0"+e},t.calendarTitleRef=function(e){if(e){var n=e.offsetHeight;t.setState({calendarTitleRefHeight:n})}},t.stopEvent=function(e){e.stopPropagation()},t.onCalendarRef=function(e){t.setState({calendarRef:e})},t.touchStart=function(e){var n=t.props.touchStartCallback;n&&n(e)},t.touchMove=function(e){var n=t.props.touchMoveCallback;n&&n(e)},t.touchEnd=function(e){var n=t.props.touchEndCallback;n&&n(e)},t.slideChange=function(e){var n=t.props.slideChangeCallback;n&&n(e)},t.dateClick=function(e){var n=t.state.checkedDate,a=t.props,r=a.dateClickCallback,o=a.format,i=a.lang,s=N(N({},n),e),c=new Date(s.year+"/"+(s.month+1)+"/"+s.day+" "+s.hours+":"+s.minutes);o&&(c=f(c,o,i)),t.setState({checkedDate:s}),r&&r(c)},t}return E(t,e),t.prototype.componentDidMount=function(){var e=this,t=this.props,n=t.model,a=t.lang,r=t.onVisibleChange;"inline"===n&&(this.setState({isShowDatetimePicker:!0}),r&&r(!0)),this.setState({language:o.default[a]}),setTimeout((function(){e.setState({isShowCalendar:!0})}))},t.prototype.componentDidUpdate=function(e){var t=e.pickerType,n=this.props,a=n.isShowAction,r=n.visible,o=this.state,i=o.isShowCalendar,s=o.isShowDatetimePicker,c=o.calendarTitleRefHeight,l=o.calendarBodyHeight,h=o.calendarTitleHeight,u=o.calendarContentHeight;i&&"time"===t&&this.showTime(),r&&!s&&this.show(),h===c&&u===c+l||(a?this.setState({calendarTitleHeight:c,calendarContentHeight:c+l}):this.setState({calendarTitleHeight:0}))},t.prototype.formatDate=function(e,t){var n=this.props.lang;return f(e,t,n)},t.prototype.render=function(){var e=this.props,t=e.model,n=e.isShowAction,a=e.disabledDate,o=e.showTodayButton,i=e.pickerType,s=e.todaySlot,c=e.actionSlot,l=e.confirmSlot,h=e.defaultDatetime,u=this.state,d=u.calendarTitleHeight,f=u.calendarContentHeight,p=u.isShowDatetimePicker,y=u.isShowCalendar,g=u.checkedDate,v=u.language,w=r.a.createElement("span",{className:m()("calendar_title_date_year",{calendar_title_date_active:y}),onClick:this.showCalendar},this.formatDate(g.year+"/"+(g.month+1)+"/"+g.day,v.DEFAULT_DATE_FORMAT)),b=r.a.createElement("span",{className:m()("calendar_title_date_time",{calendar_title_date_active:!y}),onClick:this.showTime},this.formatDate(g.year+"/"+(g.month+1)+"/"+g.day+" "+this.fillNumber(g.hours)+":"+this.fillNumber(g.minutes),v.DEFAULT_TIME_FORMAT)),k=c||r.a.createElement("div",{className:"calendar_title",ref:this.calendarTitleRef},r.a.createElement("div",{className:"calendar_title_date"},"time"!==i?w:"","date"!==i?b:""),o?r.a.createElement("div",{className:m()("calendar_confirm",{today_disable:a(new Date)}),onClick:this.today},s||v.TODAY):null,"dialog"===t?r.a.createElement("div",{className:"calendar_confirm",onClick:this.confirm},l||v.CONFIRM):null);return p?r.a.createElement("div",{className:m()("hash-calendar",{calendar_inline:"inline"===t}),style:{height:("inline"===t?f:void 0)+"px"},onClick:this.close},r.a.createElement("div",{className:"calendar_content",style:{height:f+"px"},onClick:this.stopEvent},n?k:null,r.a.createElement(C,N({onRef:this.onCalendarRef},this.props,{defaultDate:h,calendarTitleHeight:d,show:y,slideChangeCallback:this.slideChange,dateChangeCallback:this.dateChange,heightCallback:this.heightChange,touchStartCallback:this.touchStart,touchMoveCallback:this.touchMove,touchEndCallback:this.touchEnd,dateClickCallback:this.dateClick})),"date"!==i?r.a.createElement(O,N({show:!y},this.props,{defaultTime:h,timeChangeCallback:this.timeChange})):null)):null},t.defaultProps=A,t}(r.a.Component)}])})); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hash-calendar", 3 | "version": "0.1.5", 4 | "author": "HashTang", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/TangSY/react-hash-calendar.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/TangSY/react-hash-calendar/issues" 11 | }, 12 | "homepage": "https://www.hxkj.vip", 13 | "license": "MIT", 14 | "description": "react 周 月 时间选择器", 15 | "main": "lib/index.js", 16 | "keyword": "react-hash-calendar calendar date-picker datetime-picker time-picker week-picker", 17 | "private": false, 18 | "devDependencies": { 19 | "@types/classnames": "^2.2.10", 20 | "@types/node": "^12.0.0", 21 | "@types/react": "^16.0.7", 22 | "@types/react-dom": "^15.5.5", 23 | "awesome-typescript-loader": "^3.2.3", 24 | "babel-core": "^6.26.0", 25 | "babel-loader": "^7.1.2", 26 | "babel-preset-react": "^6.24.1", 27 | "chalk": "^2.1.0", 28 | "clean-webpack-plugin": "^3.0.0", 29 | "copy-webpack-plugin": "^6.3.2", 30 | "css-loader": "^0.28.7", 31 | "file-loader": "^6.2.0", 32 | "html-webpack-plugin": "^3.2.0", 33 | "ora": "^1.3.0", 34 | "source-map-loader": "^0.2.1", 35 | "style-loader": "^0.18.2", 36 | "stylus": "^0.54.8", 37 | "stylus-loader": "^3.0.2", 38 | "ts-loader": "^2.3.7", 39 | "typescript": "^3.7.2", 40 | "url-loader": "^4.1.1", 41 | "webpack": "^4.42.0", 42 | "webpack-bundle-analyzer": "^3.6.1", 43 | "webpack-cli": "^3.3.11", 44 | "webpack-dev-server": "^3.10.3", 45 | "webpack-merge": "^4.2.2" 46 | }, 47 | "dependencies": { 48 | "classnames": "^2.2.6", 49 | "react": "^16.0.0", 50 | "react-dom": "^16.0.0" 51 | }, 52 | "scripts": { 53 | "lib": "node build/build.js", 54 | "start": "webpack-dev-server --config=build/webpack.dev.conf", 55 | "build": "webpack --config=build/webpack.prod.conf" 56 | }, 57 | "browserslist": { 58 | "production": [ 59 | ">0.2%", 60 | "not dead", 61 | "not op_mini all" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 safari version" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSY/react-hash-calendar/875b1411cc4daef211f4ddecc879639f3b2ffc9c/public/demo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSY/react-hash-calendar/875b1411cc4daef211f4ddecc879639f3b2ffc9c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 19 | 28 | React App 29 | 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/calendar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import languageUtil from "../../language"; 3 | import { IDate } from "../../utils/type"; 4 | import { 5 | SCROLL_DIRECTION_LIST, 6 | WEEK_LIST, 7 | DIRECTION_LIST, 8 | } from "../../utils/constant"; 9 | import { eq } from "../../utils/eq"; 10 | import { formatDate } from "../../utils/util"; 11 | import classNames from "classnames"; 12 | import "./style.styl"; 13 | 14 | const yearNow = new Date().getFullYear(); 15 | const monthNow = new Date().getMonth(); 16 | const dayNow = new Date().getDate(); 17 | interface IObjectString { 18 | [index: string]: string; 19 | } 20 | 21 | const defaultProps = { 22 | show: false, 23 | disabledWeekView: false, // 禁用周视图 24 | isShowWeekView: false, // 是否展示周视图 25 | scrollChangeDate: true, // 滑动的时候,是否触发改变日期 26 | firstDayOfMonthClassName: "", // 每月第一天的 className 27 | todayClassName: "", // 当天日期的 className 28 | checkedDayClassName: "", // 日期被选中时的 className 29 | disabledClassName: "", // 日期被禁用时的 className 30 | notCurrentMonthDayClassName: "", // 不是当前展示月份日期的 className(例如日历前面几天与后面几天灰色部分) 31 | calendarTitleHeight: 60, // 操作栏高度 32 | defaultDate: new Date(), 33 | weekStart: "Sunday", 34 | markType: "dot", // 日期标记类型 35 | disabledDate: (date: Date) => false, // 禁用的日期 36 | disabledScroll: "", // 禁止滑动,可选值【'left', 'right', 'up', 'down', 'horizontal', 'vertical', 'all', ''】 37 | lang: "CN", // 使用的语言包 38 | }; 39 | 40 | interface ICalendarOfMonthChild { 41 | [index: number]: IDate; 42 | } 43 | 44 | interface ICalendarOfMonth { 45 | [index: number]: ICalendarOfMonthChild; 46 | } 47 | 48 | const state = { 49 | language: { WEEK: [""], MONTH: [""] }, // 使用的语言包 50 | currentChangeIsScroll: false, // 改变当前日期的方式是否为滑动事件 51 | yearOfCurrentShow: yearNow, // 当前日历展示的年份 52 | monthOfCurrentShow: monthNow, // 当前日历展示的月份 53 | weekArray: WEEK_LIST, // 星期数组 54 | calendarWeek: ["日", "一", "二", "三", "四", "五", "六"], // 日历对应的星期 55 | calendarOfMonth: [[{ year: yearNow, month: monthNow, day: dayNow }]], // 月份对应的日历表 56 | calendarOfMonthShow: [[{ year: yearNow, month: monthNow, day: dayNow }]], // 月份对应的日历表 57 | calendarDaysTotalLength: 42, // 日历表展示的总天数 6行7列 58 | lastMonthYear: 0, // 上个月的年份 59 | lastMonth: 0, // 上个月的月份 60 | nextMonthYear: 0, // 下个月的年份 61 | nextMonth: 0, // 下个月的月份 62 | checkedDate: { year: yearNow, month: monthNow, day: dayNow }, // 被选中的日期 63 | weekStartIndex: 0, // 日历第一天星期名称的index 64 | translateIndex: 0, // 用于计算上下偏移的距离 65 | transitionDuration: 0.3, // 动画持续时间 66 | touch: { 67 | x: 0, 68 | y: 0, 69 | }, // 本次touch事件,横向,纵向滑动的距离 70 | isTouching: false, // 是否正在滑动 71 | calendarGroupHeight: 0, 72 | calendarWeekTitleHeight: 0, 73 | touchStartPositionX: 0, // 开始滑动x轴的值 74 | touchStartPositionY: 0, // 开始滑动时y轴的值 75 | isShowWeek: false, // 当前日历是否以星期方式展示 76 | calendarY: 0, // 日历相对于Y轴的位置 77 | selectedDayIndex: 0, // 当前选中的日期,在这一周的第几天 78 | lastWeek: [{ year: yearNow, month: monthNow, day: dayNow }], // 上一周的数据 79 | nextWeek: [{ year: yearNow, month: monthNow, day: dayNow }], // 下一周的数据 80 | isLastWeekInCurrentMonth: false, // 上一周的数据是否在本月 81 | isNextWeekInCurrentMonth: false, // 下一周的数据是否在本月 82 | markDateColorObj: {}, // 所有被标记的日期所对应的颜色 83 | markDateTypeObj: {}, // 所有被标记的日期所对应的标记类型 84 | }; 85 | 86 | export type CalendarProps = { 87 | markDate?: any[]; 88 | disabledScroll: typeof DIRECTION_LIST[number]; 89 | lang: "CN" | "EN"; 90 | weekStart: typeof WEEK_LIST[number]; 91 | onRef?: (ref: any) => void; 92 | weekSlot?: (week: string) => React.ReactNode; 93 | heightCallback?: (height: number) => void; 94 | dateChangeCallback?: (date: IDate) => void; 95 | slideChangeCallback?: (direction: string) => void; 96 | touchStartCallback?: (e: React.TouchEvent) => void; 97 | touchMoveCallback?: (e: React.TouchEvent) => void; 98 | touchEndCallback?: (e: React.TouchEvent) => void; 99 | dateClickCallback?: (date: IDate) => void; 100 | daySlot?: ( 101 | date: IDate, 102 | extendAttr: { 103 | isMarked: boolean; 104 | isDisabledDate: boolean; 105 | isToday: boolean; 106 | isChecked: boolean; 107 | isCurrentMonthDay: boolean; 108 | isFirstDayOfMonth: boolean; 109 | } 110 | ) => React.ReactNode; 111 | } & Partial; 112 | type State = { 113 | markDateColorObj: IObjectString; 114 | markDateTypeObj: IObjectString; 115 | calendarRef?: HTMLDivElement; 116 | calendarItemRef?: HTMLDivElement; 117 | } & typeof state; 118 | 119 | class Calendar extends React.Component< 120 | CalendarProps & typeof defaultProps, 121 | State, 122 | {} 123 | > { 124 | static defaultProps = defaultProps; 125 | public state: State = state; 126 | 127 | componentDidMount() { 128 | const { 129 | lang, 130 | weekStart, 131 | onRef, 132 | defaultDate, 133 | isShowWeekView, 134 | disabledWeekView, 135 | } = this.props; 136 | const { weekArray, checkedDate, isShowWeek } = this.state; 137 | 138 | const language = languageUtil[lang]; 139 | const calendarWeek = language.WEEK; 140 | const lowerWeek = weekStart.toLowerCase(); 141 | const upperFirstCode = (lowerWeek.charAt(0).toUpperCase() + 142 | lowerWeek.slice(1)) as CalendarProps["weekStart"]; 143 | const weekStartIndex = weekArray.indexOf(upperFirstCode); 144 | const start = calendarWeek.slice(weekStartIndex); 145 | const end = calendarWeek.slice(0, weekStartIndex); 146 | 147 | onRef && onRef(this); 148 | 149 | if (isShowWeekView && disabledWeekView) { 150 | throw new Error( 151 | "'isShowWeekView' and 'disabledWeekView' can't be used at the same time" 152 | ); 153 | } 154 | 155 | if (isShowWeekView) { 156 | setTimeout(() => { 157 | this.showWeek(); 158 | }); 159 | } 160 | 161 | if (defaultDate) { 162 | this.setState( 163 | { 164 | checkedDate: { 165 | ...checkedDate, 166 | year: defaultDate.getFullYear(), 167 | month: defaultDate.getMonth(), 168 | day: defaultDate.getDate(), 169 | }, 170 | }, 171 | () => { 172 | this.calculateCalendarOfThreeMonth( 173 | defaultDate.getFullYear(), 174 | defaultDate.getMonth() 175 | ); 176 | 177 | if (isShowWeek) { 178 | this.showWeek(); 179 | } 180 | } 181 | ); 182 | } 183 | 184 | this.setState({ 185 | language: language, 186 | weekStartIndex: weekStartIndex, 187 | calendarWeek: [...start, ...end], 188 | }); 189 | } 190 | 191 | componentDidUpdate( 192 | prevProps: CalendarProps & typeof defaultProps, 193 | prevState: State 194 | ) { 195 | const { 196 | markDate: prevMarkDate, 197 | show: prevShow, 198 | isShowWeekView: prevIsShowWeekView, 199 | } = prevProps; 200 | const { 201 | markType, 202 | markDate, 203 | show, 204 | isShowWeekView, 205 | heightCallback, 206 | dateChangeCallback, 207 | } = this.props; 208 | 209 | const { 210 | weekStartIndex: prevWeekStartIndex, 211 | calendarGroupHeight: prevCalendarGroupHeight, 212 | checkedDate: prevCheckedDate, 213 | } = prevState; 214 | const { 215 | checkedDate, 216 | weekStartIndex, 217 | calendarItemRef, 218 | calendarWeekTitleHeight, 219 | markDateTypeObj, 220 | isShowWeek, 221 | } = this.state; 222 | 223 | const calendarItemHeight = 224 | (calendarItemRef && calendarItemRef.offsetHeight) || 0; 225 | const calendarGroupHeight = isShowWeek 226 | ? calendarItemHeight 227 | : calendarItemHeight * 6; 228 | 229 | if ( 230 | markDate && 231 | (!eq(prevMarkDate, markDate) || eq(markDateTypeObj, {})) && 232 | !eq(markDate, []) 233 | ) { 234 | markDate.forEach((item, index) => { 235 | if (!item.color) { 236 | let obj: { color?: string; date?: any[] } = { color: "", date: [] }; 237 | obj.color = "#1c71fb"; 238 | if (typeof item === "string" || typeof item === "number") { 239 | item = [item]; 240 | } 241 | obj.date = item || []; 242 | markDate[index] = obj; 243 | } 244 | markDate[index].type = item.type || markType || ""; 245 | 246 | markDate[index].date = this.dateFormat(markDate[index].date); 247 | }); 248 | 249 | let _markDateColorObj: IObjectString = {}; 250 | let _markDateTypeObj: IObjectString = {}; 251 | markDate.forEach((item) => { 252 | if (Array.isArray(item.date)) { 253 | item.date.forEach((date: string) => { 254 | _markDateColorObj[date] = item.color; 255 | _markDateTypeObj[date] = item.type; 256 | }); 257 | } 258 | }); 259 | this.setState({ 260 | markDateColorObj: _markDateColorObj, 261 | markDateTypeObj: _markDateTypeObj, 262 | }); 263 | } 264 | 265 | if (!eq(prevCheckedDate, checkedDate)) { 266 | dateChangeCallback && dateChangeCallback(checkedDate); 267 | } 268 | 269 | if (prevWeekStartIndex !== weekStartIndex) { 270 | this.calculateCalendarOfThreeMonth(checkedDate.year, checkedDate.month); 271 | } 272 | 273 | if (prevShow !== show) { 274 | this.calculateCalendarOfThreeMonth(checkedDate.year, checkedDate.month); 275 | this.showMonth(); 276 | } 277 | 278 | if (prevIsShowWeekView !== isShowWeekView) { 279 | if (isShowWeekView) { 280 | setTimeout(() => { 281 | this.showWeek(); 282 | }); 283 | } else { 284 | setTimeout(() => { 285 | this.showMonth(); 286 | }); 287 | } 288 | } 289 | 290 | if (prevCalendarGroupHeight !== calendarGroupHeight) { 291 | heightCallback && 292 | heightCallback(calendarGroupHeight + calendarWeekTitleHeight); 293 | this.setState({ calendarGroupHeight }); 294 | } 295 | } 296 | 297 | // 日期格式转换 298 | dateFormat(dateArr: string[]) { 299 | dateArr.forEach((date, index) => { 300 | dateArr[index] = formatDate(date, "YY/MM/DD"); 301 | }); 302 | 303 | return dateArr; 304 | } 305 | 306 | today = () => { 307 | const { checkedDate, isShowWeek, transitionDuration } = this.state; 308 | this.setState( 309 | { 310 | checkedDate: { ...checkedDate, day: dayNow }, 311 | yearOfCurrentShow: yearNow, 312 | monthOfCurrentShow: monthNow, 313 | }, 314 | () => { 315 | this.calculateCalendarOfThreeMonth(); 316 | } 317 | ); 318 | 319 | if (isShowWeek) { 320 | setTimeout(() => { 321 | this.setState({ isTouching: true }); 322 | this.showWeek(); 323 | }, transitionDuration * 1000); 324 | } 325 | }; 326 | 327 | showWeek = (checkedDate?: IDate) => { 328 | function stopScrolling(touchEvent) { 329 | touchEvent.preventDefault(); 330 | } 331 | 332 | document.addEventListener("touchstart", stopScrolling, { passive: false }); 333 | const { 334 | calendarOfMonth, 335 | checkedDate: _checkedDate, 336 | calendarItemRef, 337 | selectedDayIndex, 338 | } = this.state; 339 | checkedDate = checkedDate || _checkedDate; 340 | 341 | const calendarItemHeight = 342 | (calendarItemRef && calendarItemRef.offsetHeight) || 0; 343 | 344 | let daysArr: number[] = []; 345 | calendarOfMonth[1].forEach((item: IDate) => { 346 | daysArr.push(item.day); 347 | }); 348 | let dayIndexOfMonth = daysArr.indexOf(checkedDate.day); 349 | // 当day为月底的天数时,有可能在daysArr的前面也存在上一个月对应的日期,所以需要取lastIndexOf 350 | if (checkedDate.day > 15) { 351 | dayIndexOfMonth = daysArr.lastIndexOf(checkedDate.day); 352 | } 353 | 354 | // 计算当前日期在第几行 355 | let indexOfLine = Math.ceil((dayIndexOfMonth + 1) / 7); 356 | let lastLine = indexOfLine - 1; 357 | 358 | this.setState({ 359 | calendarY: -(calendarItemHeight * lastLine), 360 | isShowWeek: true, 361 | calendarGroupHeight: calendarItemHeight, 362 | }); 363 | 364 | let currentWeek: IDate[] = []; 365 | let sliceStart = lastLine * 7; 366 | let sliceEnd = sliceStart + 7; 367 | currentWeek = calendarOfMonth[1].slice(sliceStart, sliceEnd); 368 | 369 | let _selectedDayIndex = 0; 370 | let _isLastWeekInCurrentMonth = false; 371 | for (let i in currentWeek) { 372 | if (currentWeek[i].day === checkedDate.day) { 373 | _selectedDayIndex = parseInt(i); 374 | } 375 | } 376 | 377 | this.setState({ selectedDayIndex: _selectedDayIndex }); 378 | 379 | let firstDayOfCurrentWeek = currentWeek[0]; 380 | let lastDayOfCurrentWeek = currentWeek[6]; 381 | 382 | let lastWeek: IDate[]; 383 | if ( 384 | lastDayOfCurrentWeek.day < firstDayOfCurrentWeek.day && 385 | lastDayOfCurrentWeek.month === checkedDate.month 386 | ) { 387 | lastWeek = calendarOfMonth[0].slice(21, 28); 388 | } else { 389 | if (firstDayOfCurrentWeek.day === 1) { 390 | lastWeek = calendarOfMonth[0].slice(28, 35); 391 | } else { 392 | lastWeek = calendarOfMonth[1].slice(sliceStart - 7, sliceEnd - 7); 393 | if ( 394 | lastWeek[selectedDayIndex] && 395 | lastWeek[selectedDayIndex].month === checkedDate.month 396 | ) { 397 | _isLastWeekInCurrentMonth = true; 398 | } 399 | } 400 | } 401 | 402 | this.setState({ 403 | lastWeek, 404 | isLastWeekInCurrentMonth: _isLastWeekInCurrentMonth, 405 | }); 406 | 407 | let _isNextWeekInCurrentMonth = false; 408 | let nextWeek: IDate[]; 409 | if ( 410 | lastDayOfCurrentWeek.day < firstDayOfCurrentWeek.day && 411 | lastDayOfCurrentWeek.month !== checkedDate.month 412 | ) { 413 | nextWeek = calendarOfMonth[2].slice(7, 14); 414 | } else { 415 | if ( 416 | lastDayOfCurrentWeek.day === 417 | this.daysOfMonth(lastDayOfCurrentWeek.year)[lastDayOfCurrentWeek.month] 418 | ) { 419 | nextWeek = calendarOfMonth[2].slice(0, 7); 420 | } else { 421 | nextWeek = calendarOfMonth[1].slice(sliceStart + 7, sliceEnd + 7); 422 | if (nextWeek[selectedDayIndex].month === checkedDate.month) { 423 | _isNextWeekInCurrentMonth = true; 424 | } 425 | } 426 | } 427 | this.state.calendarOfMonthShow[0].splice(sliceStart, 7, ...lastWeek); 428 | this.state.calendarOfMonthShow[2].splice(sliceStart, 7, ...nextWeek); 429 | document.addEventListener("touchmove", stopScrolling, { passive: false }); 430 | this.setState({ 431 | nextWeek, 432 | isNextWeekInCurrentMonth: _isNextWeekInCurrentMonth, 433 | }); 434 | }; 435 | 436 | showMonth = () => { 437 | function stopScrolling(touchEvent) { 438 | touchEvent.preventDefault(); 439 | } 440 | 441 | document.addEventListener("touchstart", stopScrolling, { passive: false }); 442 | const { checkedDate, calendarItemRef } = this.state; 443 | 444 | const calendarItemHeight = 445 | (calendarItemRef && calendarItemRef.offsetHeight) || 0; 446 | 447 | this.setState({ 448 | calendarY: 0, 449 | isShowWeek: false, 450 | isLastWeekInCurrentMonth: false, 451 | isNextWeekInCurrentMonth: false, 452 | calendarGroupHeight: calendarItemHeight * 6, 453 | }); 454 | document.addEventListener("touchmove", stopScrolling, { passive: false }); 455 | this.calculateCalendarOfThreeMonth(checkedDate.year, checkedDate.month); 456 | }; 457 | 458 | weekTitleRef = (ref: HTMLDivElement): void => { 459 | if (!ref) return; 460 | 461 | this.setState({ calendarWeekTitleHeight: ref.offsetHeight }); 462 | }; 463 | 464 | calendarRef = (ref: HTMLDivElement): void => { 465 | if (!ref) return; 466 | 467 | this.setState({ calendarRef: ref }); 468 | }; 469 | 470 | calendarItemRef = (ref: HTMLDivElement): void => { 471 | if (!ref) return; 472 | 473 | this.setState({ calendarItemRef: ref }); 474 | 475 | const height = ref.offsetHeight; 476 | this.setState({ 477 | calendarGroupHeight: height * 6, 478 | }); 479 | }; 480 | 481 | touchStart = (event: React.TouchEvent) => { 482 | const { touchStartCallback } = this.props; 483 | touchStartCallback && touchStartCallback(event); 484 | 485 | this.setState({ 486 | touchStartPositionX: event.touches[0].clientX, 487 | touchStartPositionY: event.touches[0].clientY, 488 | touch: { x: 0, y: 0 }, 489 | isTouching: true, 490 | }); 491 | }; 492 | 493 | touchMove = (event: React.TouchEvent) => { 494 | event.stopPropagation(); 495 | 496 | const { touchStartPositionX, touchStartPositionY, calendarRef } = 497 | this.state; 498 | const calendarHeight = (calendarRef && calendarRef.offsetHeight) || 0; 499 | const calendarWidth = (calendarRef && calendarRef.offsetWidth) || 0; 500 | const { disabledWeekView, touchMoveCallback } = this.props; 501 | 502 | touchMoveCallback && touchMoveCallback(event); 503 | 504 | let moveX = event.touches[0].clientX - touchStartPositionX; 505 | let moveY = event.touches[0].clientY - touchStartPositionY; 506 | 507 | if (Math.abs(moveX) > Math.abs(moveY)) { 508 | if ( 509 | (moveX < 0 && !this.isCanScroll("left")) || 510 | (moveX > 0 && !this.isCanScroll("right")) 511 | ) { 512 | return; 513 | } 514 | 515 | this.setState({ touch: { x: moveX / calendarWidth, y: 0 } }); 516 | } else { 517 | // 禁用周视图(禁止上下滑动) 518 | if ( 519 | disabledWeekView || 520 | (moveY < 0 && !this.isCanScroll("up")) || 521 | (moveY > 0 && !this.isCanScroll("down")) 522 | ) { 523 | return; 524 | } 525 | 526 | this.setState({ touch: { x: 0, y: moveY / calendarHeight } }); 527 | } 528 | }; 529 | 530 | touchEnd = (event: React.TouchEvent) => { 531 | const { touch, isShowWeek, transitionDuration, calendarRef } = this.state; 532 | const calendarHeight = (calendarRef && calendarRef.offsetHeight) || 0; 533 | const { slideChangeCallback, touchEndCallback } = this.props; 534 | 535 | touchEndCallback && touchEndCallback(event); 536 | 537 | this.setState({ isTouching: false }); 538 | if (Math.abs(touch.x) > Math.abs(touch.y) && Math.abs(touch.x) > 0.2) { 539 | this.setState({ currentChangeIsScroll: true }, () => { 540 | if (touch.x > 0) { 541 | slideChangeCallback && slideChangeCallback("right"); 542 | this.getLastMonth(); 543 | if (isShowWeek) { 544 | setTimeout(() => { 545 | this.setState({ isTouching: true }); 546 | this.setState({ currentChangeIsScroll: true }, () => { 547 | this.getLastWeek(); 548 | }); 549 | }, transitionDuration * 1000); 550 | } 551 | } else if (touch.x < 0) { 552 | slideChangeCallback && slideChangeCallback("left"); 553 | 554 | this.getNextMonth(); 555 | if (isShowWeek) { 556 | setTimeout(() => { 557 | this.setState({ isTouching: true }); 558 | this.setState({ currentChangeIsScroll: true }, () => { 559 | this.getNextWeek(); 560 | }); 561 | }, transitionDuration * 1000); 562 | } 563 | } 564 | }); 565 | } 566 | 567 | if ( 568 | Math.abs(touch.y) > Math.abs(touch.x) && 569 | Math.abs(touch.y * calendarHeight) > 50 570 | ) { 571 | if (touch.y > 0 && isShowWeek) { 572 | slideChangeCallback && slideChangeCallback("down"); 573 | 574 | this.showMonth(); 575 | } else if (touch.y < 0 && !isShowWeek) { 576 | slideChangeCallback && slideChangeCallback("up"); 577 | 578 | this.showWeek(); 579 | } 580 | } else { 581 | this.setState({ touch: { x: 0, y: 0 } }); 582 | } 583 | }; 584 | 585 | // 显示上一周 586 | getLastWeek = () => { 587 | const { lastWeek, selectedDayIndex, currentChangeIsScroll } = this.state; 588 | const { scrollChangeDate } = this.props; 589 | 590 | let _checkedDate = lastWeek[selectedDayIndex]; 591 | this.showWeek(_checkedDate); 592 | 593 | if (this.formatDisabledDate(_checkedDate)) return; 594 | 595 | if (!scrollChangeDate && currentChangeIsScroll) { 596 | this.setState({ currentChangeIsScroll: false }); 597 | return; 598 | } 599 | 600 | this.setState({ checkedDate: _checkedDate }); 601 | }; 602 | 603 | // 显示下一周 604 | getNextWeek = () => { 605 | const { nextWeek, selectedDayIndex, currentChangeIsScroll } = this.state; 606 | const { scrollChangeDate } = this.props; 607 | 608 | let _checkedDate = nextWeek[selectedDayIndex]; 609 | this.showWeek(_checkedDate); 610 | 611 | if (this.formatDisabledDate(_checkedDate)) return; 612 | 613 | if (!scrollChangeDate && currentChangeIsScroll) { 614 | this.setState({ currentChangeIsScroll: false }); 615 | return; 616 | } 617 | 618 | this.setState({ checkedDate: _checkedDate }); 619 | }; 620 | 621 | // 获取上个月日历 622 | getLastMonth = () => { 623 | const { 624 | yearOfCurrentShow, 625 | monthOfCurrentShow, 626 | lastMonthYear, 627 | lastMonth, 628 | isLastWeekInCurrentMonth, 629 | isShowWeek, 630 | } = this.state; 631 | 632 | this.setState((preState) => ({ 633 | translateIndex: preState.translateIndex + 1, 634 | })); 635 | 636 | let _yearOfCurrentShow = yearOfCurrentShow; 637 | let _monthOfCurrentShow = monthOfCurrentShow; 638 | if (!isLastWeekInCurrentMonth) { 639 | _yearOfCurrentShow = lastMonthYear; 640 | _monthOfCurrentShow = lastMonth; 641 | } 642 | 643 | if (isShowWeek && isLastWeekInCurrentMonth) { 644 | return null; 645 | } 646 | 647 | this.setState({ 648 | yearOfCurrentShow: _yearOfCurrentShow, 649 | monthOfCurrentShow: _monthOfCurrentShow, 650 | }); 651 | 652 | this.calculateCalendarOfThreeMonth(_yearOfCurrentShow, _monthOfCurrentShow); 653 | }; 654 | 655 | // 获取下个月日历 656 | getNextMonth = () => { 657 | const { 658 | yearOfCurrentShow, 659 | monthOfCurrentShow, 660 | nextMonthYear, 661 | nextMonth, 662 | isNextWeekInCurrentMonth, 663 | isShowWeek, 664 | } = this.state; 665 | 666 | this.setState((preState) => ({ 667 | translateIndex: preState.translateIndex - 1, 668 | })); 669 | 670 | let _yearOfCurrentShow = yearOfCurrentShow; 671 | let _monthOfCurrentShow = monthOfCurrentShow; 672 | if (!isNextWeekInCurrentMonth) { 673 | _yearOfCurrentShow = nextMonthYear; 674 | _monthOfCurrentShow = nextMonth; 675 | } 676 | 677 | if (isShowWeek && isNextWeekInCurrentMonth) { 678 | return null; 679 | } 680 | 681 | this.setState({ 682 | yearOfCurrentShow: _yearOfCurrentShow, 683 | monthOfCurrentShow: _monthOfCurrentShow, 684 | }); 685 | 686 | this.calculateCalendarOfThreeMonth(_yearOfCurrentShow, _monthOfCurrentShow); 687 | }; 688 | 689 | // 是否可以滑动 690 | isCanScroll = (dire: typeof SCROLL_DIRECTION_LIST[number]): boolean => { 691 | const { disabledScroll } = this.props; 692 | const scrollObj = { 693 | up: ["all", "up", "vertical"], 694 | down: ["all", "down", "vertical"], 695 | left: ["all", "left", "horizontal"], 696 | right: ["all", "right", "horizontal"], 697 | }; 698 | 699 | let checkedScrollArr = scrollObj[dire]; 700 | return !checkedScrollArr.some((item) => item === disabledScroll); 701 | }; 702 | 703 | formatDisabledDate = (date: IDate): boolean => { 704 | const { disabledDate } = this.props; 705 | 706 | let fDate = new Date(`${date.year}/${date.month + 1}/${date.day}`); 707 | 708 | return disabledDate(fDate); 709 | }; 710 | 711 | isFirstDayOfMonth = (date: IDate, index: number): boolean => { 712 | return date.day === 1 && !this.isNotCurrentMonthDay(date, index); 713 | }; 714 | 715 | isNotCurrentMonthDay = (date: IDate, index: number): boolean => { 716 | const { calendarOfMonth } = this.state; 717 | 718 | if (!calendarOfMonth[index]) return false; 719 | let dateOfCurrentShow: IDate = calendarOfMonth[index][15]; // 本月中间的日期一定为本月 720 | 721 | if (!dateOfCurrentShow) return false; 722 | return ( 723 | date.year !== dateOfCurrentShow.year || 724 | date.month !== dateOfCurrentShow.month 725 | ); 726 | }; 727 | 728 | markDateColor = (date: IDate, type: string): string => { 729 | const { markDateTypeObj, markDateColorObj } = this.state; 730 | 731 | let dateString = `${date.year}/${this.fillNumber( 732 | date.month + 1 733 | )}/${this.fillNumber(date.day)}`; 734 | let markDateTypeString = markDateTypeObj[dateString] || ""; 735 | 736 | if (markDateTypeString.indexOf(type) === -1) return ""; 737 | 738 | return markDateColorObj[dateString]; 739 | }; 740 | 741 | // 小于10,在前面补0 742 | fillNumber = (val: number) => { 743 | return val > 9 ? val : "0" + val; 744 | }; 745 | 746 | isToday = (date: IDate): boolean => { 747 | return ( 748 | yearNow === date.year && monthNow === date.month && dayNow === date.day 749 | ); 750 | }; 751 | 752 | isCheckedDay = (date: IDate): boolean => { 753 | if (this.formatDisabledDate(date)) return false; 754 | 755 | const { checkedDate } = this.state; 756 | 757 | return ( 758 | checkedDate.year === date.year && 759 | checkedDate.month === date.month && 760 | checkedDate.day === date.day 761 | ); 762 | }; 763 | 764 | clickCalendarDay = (e: React.MouseEvent, date: IDate) => { 765 | if (!date) return; 766 | 767 | if (this.formatDisabledDate(date)) return; 768 | 769 | const { dateClickCallback } = this.props; 770 | this.setState( 771 | { 772 | checkedDate: { year: date.year, month: date.month, day: date.day }, 773 | }, 774 | () => { 775 | const { 776 | lastMonth, 777 | lastMonthYear, 778 | nextMonth, 779 | nextMonthYear, 780 | isShowWeek, 781 | } = this.state; 782 | 783 | if (date.month === lastMonth && date.year === lastMonthYear) { 784 | this.getLastMonth(); 785 | } 786 | if (date.month === nextMonth && date.year === nextMonthYear) { 787 | this.getNextMonth(); 788 | } 789 | 790 | if (isShowWeek) { 791 | this.showWeek(); 792 | } 793 | } 794 | ); 795 | 796 | dateClickCallback && dateClickCallback(date); 797 | }; 798 | 799 | calculateCalendarOfThreeMonth = ( 800 | year: number = yearNow, 801 | month: number = monthNow 802 | ) => { 803 | const { currentChangeIsScroll, checkedDate } = this.state; 804 | const { scrollChangeDate } = this.props; 805 | 806 | const { lastMonthYear, lastMonth, nextMonthYear, nextMonth } = 807 | this.getNearYearAndMonth(year, month); 808 | 809 | this.setState({ 810 | lastMonthYear, 811 | lastMonth, 812 | nextMonthYear, 813 | nextMonth, 814 | }); 815 | 816 | let firstMonth = this.calculateCalendarOfMonth(lastMonthYear, lastMonth); 817 | let secondMonth = this.calculateCalendarOfMonth(year, month); 818 | let thirdMonth = this.calculateCalendarOfMonth(nextMonthYear, nextMonth); 819 | 820 | let calendarOfMonth: IDate[][] = []; 821 | calendarOfMonth.push(firstMonth, secondMonth, thirdMonth); 822 | 823 | this.setState({ 824 | calendarOfMonth, 825 | calendarOfMonthShow: JSON.parse(JSON.stringify(calendarOfMonth)), 826 | }); 827 | 828 | if (!scrollChangeDate && currentChangeIsScroll) { 829 | this.setState({ 830 | currentChangeIsScroll: false, 831 | }); 832 | return; 833 | } 834 | 835 | // 改变日期选择的日期 836 | let tempDate: IDate; 837 | let day = checkedDate.day; 838 | if (day > 30 || (day > 28 && month === 1)) { 839 | day = this.daysOfMonth(year)[month]; 840 | } 841 | tempDate = { day: day, year: year, month: month }; 842 | 843 | if (this.formatDisabledDate(tempDate)) return; 844 | 845 | this.setState({ 846 | checkedDate: { day: tempDate.day, year, month }, 847 | }); 848 | }; 849 | 850 | calculateCalendarOfMonth = ( 851 | year: number = yearNow, 852 | month: number = monthNow 853 | ) => { 854 | let calendarOfCurrentMonth: IDate[] = []; 855 | 856 | const { weekStartIndex, calendarDaysTotalLength } = this.state; 857 | 858 | const { lastMonthYear, lastMonth, nextMonthYear, nextMonth } = 859 | this.getNearYearAndMonth(year, month); 860 | 861 | // 如果当月第一天不是指定的开始星期名称,则在前面补齐上个月的日期 862 | let dayOfWeek = this.getDayOfWeek(year, month); 863 | let lastMonthDays = this.daysOfMonth(year)[lastMonth]; // 上个月的总天数 864 | if (dayOfWeek < weekStartIndex) { 865 | dayOfWeek = 7 - weekStartIndex + dayOfWeek; 866 | } else { 867 | dayOfWeek -= weekStartIndex; 868 | } 869 | for (let i = 0; i < dayOfWeek; i++) { 870 | calendarOfCurrentMonth.push({ 871 | year: lastMonthYear, 872 | month: lastMonth, 873 | day: lastMonthDays - (dayOfWeek - 1 - i), 874 | }); 875 | } 876 | 877 | // 当月日期 878 | for (let i = 0; i < this.daysOfMonth(year)[month]; i++) { 879 | calendarOfCurrentMonth.push({ year, month, day: i + 1 }); 880 | } 881 | 882 | // 在日历后面填充下个月的日期,补齐6行7列 883 | let fillDays = calendarDaysTotalLength - calendarOfCurrentMonth.length; 884 | for (let i = 0; i < fillDays; i++) { 885 | calendarOfCurrentMonth.push({ 886 | year: nextMonthYear, 887 | month: nextMonth, 888 | day: i + 1, 889 | }); 890 | } 891 | 892 | return calendarOfCurrentMonth; 893 | }; 894 | 895 | // 获取月份某一天是星期几 896 | getDayOfWeek = ( 897 | year: number = yearNow, 898 | month: number = monthNow, 899 | day: number = 1 900 | ) => { 901 | let dayOfMonth = new Date(year, month, day); // 获取当月的第day天 902 | let dayOfWeek = dayOfMonth.getDay(); // 判断第day天是星期几(返回[0-6]中的一个,0代表星期天,1代表星期一) 903 | return dayOfWeek; 904 | }; 905 | 906 | daysOfMonth = (year: number) => { 907 | return [31, 28 + this.isLeap(year), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 908 | }; 909 | 910 | // 判断是否为闰年 911 | isLeap = (year: number) => { 912 | return year % 4 === 0 913 | ? year % 100 !== 0 914 | ? 1 915 | : year % 400 === 0 916 | ? 1 917 | : 0 918 | : 0; 919 | }; 920 | 921 | getNearYearAndMonth = (year: number, month: number) => { 922 | const lastMonthYear = month === 0 ? year - 1 : year; // 上个月的年份 923 | const lastMonth = month === 0 ? 11 : month - 1; // 上个月的月份 924 | const nextMonthYear = month === 11 ? year + 1 : year; // 下个月的年份 925 | const nextMonth = month === 11 ? 0 : month + 1; // 下个月的月份 926 | 927 | return { lastMonthYear, lastMonth, nextMonthYear, nextMonth }; 928 | }; 929 | 930 | render() { 931 | const { 932 | calendarTitleHeight, 933 | show, 934 | weekSlot, 935 | daySlot, 936 | disabledClassName, 937 | firstDayOfMonthClassName, 938 | todayClassName, 939 | checkedDayClassName, 940 | notCurrentMonthDayClassName, 941 | } = this.props; 942 | const { 943 | calendarWeek, 944 | calendarItemRef, 945 | calendarOfMonthShow, 946 | translateIndex, 947 | isTouching, 948 | isShowWeek, 949 | touch, 950 | calendarY, 951 | transitionDuration, 952 | } = this.state; 953 | 954 | const calendarItemHeight = 955 | (calendarItemRef && calendarItemRef.offsetHeight) || 0; 956 | const calendarGroupHeight = isShowWeek 957 | ? calendarItemHeight 958 | : calendarItemHeight * 6; 959 | 960 | const weekNode: React.ReactNode = ( 961 |
962 | {calendarWeek.map((item) => ( 963 |
964 |

965 | {(weekSlot && weekSlot(item)) || item} 966 |

967 |
968 | ))} 969 |
970 | ); 971 | 972 | const calendarItemNode = ( 973 | item: IDate[], 974 | mIndex: number 975 | ): React.ReactNode => { 976 | const { language } = this.state; 977 | return item.map((date, index) => ( 978 |
this.clickCalendarDay(e, date)} 987 | > 988 |
1005 | {(daySlot && 1006 | daySlot(date, { 1007 | isMarked: !!( 1008 | this.markDateColor(date, "circle") || 1009 | this.markDateColor(date, "dot") 1010 | ), 1011 | isDisabledDate: this.formatDisabledDate(date), 1012 | isToday: this.isToday(date), 1013 | isChecked: this.isCheckedDay(date), 1014 | isCurrentMonthDay: !this.isNotCurrentMonthDay(date, mIndex), 1015 | isFirstDayOfMonth: this.isFirstDayOfMonth(date, mIndex), 1016 | })) || 1017 | (this.isFirstDayOfMonth(date, mIndex) 1018 | ? language.MONTH && language.MONTH[date.month] 1019 | : date.day)} 1020 |
1021 |
1025 |
1026 | )); 1027 | }; 1028 | 1029 | const calendarGroupNode = ( 1030 |
    1031 | {calendarOfMonthShow.map((item, index) => ( 1032 |
  • 1042 | {calendarItemNode(item, index)} 1043 |
  • 1044 | ))} 1045 |
1046 | ); 1047 | 1048 | const calendarBodyNode = ( 1049 |
1057 | {calendarGroupNode} 1058 |
1059 | ); 1060 | 1061 | return show ? ( 1062 |
1066 | {weekNode} 1067 | {calendarBodyNode} 1068 |
1069 | ) : null; 1070 | } 1071 | } 1072 | 1073 | export default Calendar; 1074 | -------------------------------------------------------------------------------- /src/components/calendar/style.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/common.styl'; 2 | 3 | .hash-calendar { 4 | .calendar_body { 5 | position: relative; 6 | width: 100%; 7 | margin-top: px2vw(100px); 8 | } 9 | .calendar_week { 10 | position: absolute; 11 | width: 100%; 12 | left: 0; 13 | top: 0; 14 | flexAlign(); 15 | background: white; 16 | color: vice-font-color; 17 | z-index: 2; 18 | } 19 | .calendar_group { 20 | position: absolute; 21 | top: px2vw(70px); 22 | left: 0; 23 | bottom: 0; 24 | right: 0; 25 | overflow: hidden; 26 | transition: height 0.3s; 27 | -webkit-transition: height 0.3s; 28 | } 29 | .calendar_group ul { 30 | height: 100%; 31 | } 32 | .calendar_group_li { 33 | position: absolute; 34 | top: 0; 35 | left: px2vw(4px); 36 | bottom: 0; 37 | right: 0; 38 | height: 100%; 39 | width: 100%; 40 | flexAlign(); 41 | flex-wrap: wrap; 42 | background: white; 43 | will-change: transform; 44 | } 45 | .calendar_item { 46 | width: 14.13333335%; 47 | flexContent(); 48 | flex-direction: column; 49 | } 50 | .calendar_item_disable { 51 | background-color: disabled-bg-color; 52 | opacity: 1; 53 | cursor: not-allowed; 54 | color: disabled-font-color; 55 | } 56 | .calendar_day { 57 | width: px2vw(60px); 58 | height: px2vw(60px); 59 | border-radius: 50%; 60 | fontSize(28px); 61 | flexContent(); 62 | margin-bottom: px2vw(8px); 63 | } 64 | .calendar_first_today { 65 | color: main-color; 66 | } 67 | .calendar_first_today span { 68 | fontSize(20px); 69 | margin-top: px2vw(3px); 70 | } 71 | .calendar_day_today { 72 | background: bg-color; 73 | } 74 | .calendar_mark_circle { 75 | border: 1px solid main-color; 76 | } 77 | .calendar_day_not { 78 | color: disabled-font-color; 79 | } 80 | .calendar_day_checked { 81 | background: main-color; 82 | color: white; 83 | } 84 | .calendar_dot { 85 | width: 5px; 86 | height: 5px; 87 | border-radius: 50%; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/components/datetimePicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Calendar from '../calendar'; 3 | import TimePicker from '../timePicker'; 4 | import classNames from 'classnames'; 5 | import { formatDate } from '../../utils/util'; 6 | import { IDate, ITime } from '../../utils/type'; 7 | import { WEEK_LIST, DIRECTION_LIST } from '../../utils/constant'; 8 | import languageUtil from '../../language'; 9 | import './style.styl'; 10 | 11 | const defaultProps = { 12 | model: 'inline', 13 | pickerType: 'datetime', // 选择器类型 datetime:日期+时间 date:日期 time:时间 14 | format: '', // 确认选择之后,返回的日期格式 15 | visible: false, // 是否显示日历组件 16 | isShowAction: true, // 是否显示日历组件操作栏 17 | showTodayButton: true, // 是否显示返回今日按钮 18 | defaultDatetime: new Date(), // 默认时间 19 | disabledDate: (date: Date) => false, // 禁用的日期 20 | lang: 'CN', // 使用的语言包 21 | 22 | // calendar props 23 | disabledWeekView: false, // 禁用周视图 24 | isShowWeekView: false, // 是否展示周视图 25 | scrollChangeDate: true, // 滑动的时候,是否触发改变日期 26 | firstDayOfMonthClassName: '', // 每月第一天的 className 27 | todayClassName: '', // 当天日期的 className 28 | checkedDayClassName: '', // 日期被选中时的 className 29 | disabledClassName: '', // 日期被禁用时的 className 30 | notCurrentMonthDayClassName: '', // 不是当前展示月份日期的 className(例如日历前面几天与后面几天灰色部分) 31 | weekStart: 'Sunday', 32 | markType: 'dot', // 日期标记类型 33 | disabledScroll: '', // 禁止滑动,可选值【'left', 'right', 'up', 'down', 'horizontal', 'vertical', 'all', ''】 34 | 35 | // timePicker props 36 | minuteStep: 1, 37 | }; 38 | 39 | const state = { 40 | language: { 41 | CONFIRM: '', 42 | TODAY: '', 43 | WEEK: [''], 44 | MONTH: [''], 45 | DEFAULT_DATE_FORMAT: 'YY年MM月DD日', 46 | DEFAULT_TIME_FORMAT: 'hh:mm', 47 | }, 48 | checkedDate: { 49 | year: new Date().getFullYear(), 50 | month: new Date().getMonth(), 51 | day: new Date().getDate(), 52 | hours: new Date().getHours(), 53 | minutes: new Date().getMinutes(), 54 | }, // 被选中的日期 55 | isShowCalendar: false, // 是否显示日历选择控件 56 | isShowDatetimePicker: false, // 是否显示日历组件 57 | calendarBodyHeight: 0, // 日历内容的高度 58 | calendarContentHeight: 0, // 日历内容的高度 59 | calendarTitleHeight: 0, // 日历组件标题显示高度 60 | calendarTitleRefHeight: 0, // 日历组件标题实际高度 61 | firstTimes: true, // 第一次触发 62 | }; 63 | 64 | export type DateTimeProps = { 65 | weekStart: typeof WEEK_LIST[number]; 66 | disabledScroll: typeof DIRECTION_LIST[number]; 67 | markDate?: any[]; 68 | model: 'inline' | 'dialog'; 69 | lang: 'CN' | 'EN'; 70 | actionSlot?: React.ReactNode; 71 | todaySlot?: React.ReactNode; 72 | confirmSlot?: React.ReactNode; 73 | onVisibleChange?: (visible: boolean) => void; 74 | weekSlot?: (week: string) => React.ReactNode; 75 | daySlot?: ( 76 | date: IDate, 77 | extendAttr: { 78 | isMarked: boolean; 79 | isDisabledDate: boolean; 80 | isToday: boolean; 81 | isChecked: boolean; 82 | isCurrentMonthDay: boolean; 83 | isFirstDayOfMonth: boolean; 84 | } 85 | ) => React.ReactNode; 86 | slideChangeCallback?: (direction: string) => void; 87 | touchStartCallback?: (e: React.TouchEvent) => void; 88 | touchMoveCallback?: (e: React.TouchEvent) => void; 89 | touchEndCallback?: (e: React.TouchEvent) => void; 90 | dateClickCallback?: (date: Date | string) => void; 91 | dateConfirmCallback?: (date: Date | string) => void; 92 | } & Partial; 93 | type State = { calendarRef?: any } & typeof state; 94 | 95 | class ReactHashCalendar extends React.Component< 96 | DateTimeProps & typeof defaultProps, 97 | State, 98 | {} 99 | > { 100 | static defaultProps = defaultProps; 101 | 102 | public state: State = state; 103 | 104 | componentDidMount() { 105 | const { model, lang, onVisibleChange } = this.props; 106 | if (model === 'inline') { 107 | this.setState({ isShowDatetimePicker: true }); 108 | onVisibleChange && onVisibleChange(true); 109 | } 110 | 111 | this.setState({ language: languageUtil[lang] }); 112 | 113 | setTimeout(() => { 114 | this.setState({ isShowCalendar: true }); 115 | }); 116 | } 117 | 118 | componentDidUpdate(prevProps: DateTimeProps) { 119 | const { pickerType } = prevProps; 120 | const { isShowAction, visible } = this.props; 121 | const { 122 | isShowCalendar, 123 | isShowDatetimePicker, 124 | calendarTitleRefHeight, 125 | calendarBodyHeight, 126 | calendarTitleHeight, 127 | calendarContentHeight, 128 | } = this.state; 129 | 130 | if (isShowCalendar && pickerType === 'time') { 131 | this.showTime(); 132 | } 133 | 134 | if (visible && !isShowDatetimePicker) { 135 | this.show(); 136 | } 137 | 138 | if ( 139 | calendarTitleHeight !== calendarTitleRefHeight || 140 | calendarContentHeight !== calendarTitleRefHeight + calendarBodyHeight 141 | ) { 142 | if (!isShowAction) { 143 | this.setState({ calendarTitleHeight: 0 }); 144 | } else { 145 | this.setState({ 146 | calendarTitleHeight: calendarTitleRefHeight, 147 | calendarContentHeight: calendarTitleRefHeight + calendarBodyHeight, 148 | }); 149 | } 150 | } 151 | } 152 | 153 | // 显示时间选择控件 154 | showTime = () => { 155 | this.setState({ isShowCalendar: false }); 156 | }; 157 | 158 | showCalendar = () => { 159 | this.setState({ isShowCalendar: true }); 160 | }; 161 | 162 | show = () => { 163 | const { onVisibleChange } = this.props; 164 | this.setState({ isShowDatetimePicker: true }); 165 | this.setState({ isShowCalendar: true }); 166 | onVisibleChange && onVisibleChange(true); 167 | }; 168 | 169 | close = () => { 170 | const { onVisibleChange } = this.props; 171 | this.setState({ isShowDatetimePicker: false }); 172 | onVisibleChange && onVisibleChange(false); 173 | }; 174 | 175 | formatDate(time: string, format: string) { 176 | const { lang } = this.props; 177 | return formatDate(time, format, lang); 178 | } 179 | 180 | today = () => { 181 | const { disabledDate } = this.props; 182 | if (disabledDate(new Date())) return; 183 | 184 | const { calendarRef } = this.state; 185 | calendarRef && calendarRef.today(); 186 | }; 187 | 188 | confirm = () => { 189 | const { format, model, lang, dateConfirmCallback } = this.props; 190 | const { checkedDate } = this.state; 191 | let date: Date | string = new Date( 192 | `${checkedDate.year}/${checkedDate.month + 1}/${checkedDate.day} ${ 193 | checkedDate.hours 194 | }:${checkedDate.minutes}` 195 | ); 196 | if (format) { 197 | date = formatDate(date, format, lang); 198 | } 199 | dateConfirmCallback && dateConfirmCallback(date); 200 | 201 | if (model === 'dialog') { 202 | this.close(); 203 | } 204 | }; 205 | 206 | dateChange = (date: IDate) => { 207 | const { checkedDate } = this.state; 208 | this.setState({ 209 | checkedDate: { 210 | ...checkedDate, 211 | ...date, 212 | }, 213 | }); 214 | }; 215 | 216 | timeChange = (time: ITime) => { 217 | const { checkedDate } = this.state; 218 | this.setState({ 219 | checkedDate: { 220 | ...checkedDate, 221 | ...time, 222 | }, 223 | }); 224 | }; 225 | 226 | heightChange = (height: number) => { 227 | const { firstTimes, calendarTitleHeight } = this.state; 228 | const { model } = this.props; 229 | 230 | if (!firstTimes && model === 'dialog') return; 231 | 232 | this.setState({ 233 | calendarBodyHeight: height, 234 | calendarContentHeight: height + calendarTitleHeight, 235 | firstTimes: false, 236 | }); 237 | }; 238 | 239 | // 小于10,在前面补0 240 | fillNumber = (val: number) => (val > 9 ? val : '0' + val); 241 | 242 | calendarTitleRef = (ref: HTMLDivElement): void => { 243 | if (!ref) return; 244 | const height = ref.offsetHeight; 245 | 246 | this.setState({ 247 | calendarTitleRefHeight: height, 248 | }); 249 | }; 250 | 251 | stopEvent = (e: React.MouseEvent) => { 252 | e.stopPropagation(); 253 | }; 254 | 255 | onCalendarRef = (ref: any) => { 256 | this.setState({ 257 | calendarRef: ref, 258 | }); 259 | }; 260 | 261 | // 监听手指开始滑动事件 262 | touchStart = (event: React.TouchEvent) => { 263 | const { touchStartCallback } = this.props; 264 | touchStartCallback && touchStartCallback(event); 265 | }; 266 | 267 | // 监听手指开始滑动事件 268 | touchMove = (event: React.TouchEvent) => { 269 | const { touchMoveCallback } = this.props; 270 | touchMoveCallback && touchMoveCallback(event); 271 | }; 272 | 273 | // 监听手指开始滑动事件 274 | touchEnd = (event: React.TouchEvent) => { 275 | const { touchEndCallback } = this.props; 276 | touchEndCallback && touchEndCallback(event); 277 | }; 278 | 279 | // 滑动方向改变 280 | slideChange = (direction: string) => { 281 | const { slideChangeCallback } = this.props; 282 | slideChangeCallback && slideChangeCallback(direction); 283 | }; 284 | 285 | dateClick = (date: IDate) => { 286 | const { checkedDate } = this.state; 287 | const { dateClickCallback, format, lang } = this.props; 288 | let _checkedDate = { 289 | ...checkedDate, 290 | ...date, 291 | }; 292 | 293 | let fDate: Date | string = new Date( 294 | `${_checkedDate.year}/${_checkedDate.month + 1}/${_checkedDate.day} ${ 295 | _checkedDate.hours 296 | }:${_checkedDate.minutes}` 297 | ); 298 | if (format) { 299 | fDate = formatDate(fDate, format, lang); 300 | } 301 | 302 | this.setState({ 303 | checkedDate: _checkedDate, 304 | }); 305 | 306 | dateClickCallback && dateClickCallback(fDate); 307 | }; 308 | 309 | render() { 310 | const { 311 | model, 312 | isShowAction, 313 | disabledDate, 314 | showTodayButton, 315 | pickerType, 316 | todaySlot, 317 | actionSlot, 318 | confirmSlot, 319 | defaultDatetime, 320 | } = this.props; 321 | 322 | const { 323 | calendarTitleHeight, 324 | calendarContentHeight, 325 | isShowDatetimePicker, 326 | isShowCalendar, 327 | checkedDate, 328 | language, 329 | } = this.state; 330 | const dateNode: React.ReactNode = ( 331 | 337 | {this.formatDate( 338 | `${checkedDate.year}/${checkedDate.month + 1}/${checkedDate.day}`, 339 | language.DEFAULT_DATE_FORMAT 340 | )} 341 | 342 | ); 343 | 344 | const timeNode: React.ReactNode = ( 345 | 351 | {this.formatDate( 352 | `${checkedDate.year}/${checkedDate.month + 1}/${ 353 | checkedDate.day 354 | } ${this.fillNumber(checkedDate.hours)}:${this.fillNumber( 355 | checkedDate.minutes 356 | )}`, 357 | language.DEFAULT_TIME_FORMAT 358 | )} 359 | 360 | ); 361 | 362 | const actionNode: React.ReactNode = actionSlot || ( 363 |
364 |
365 | {pickerType !== 'time' ? dateNode : ''} 366 | {pickerType !== 'date' ? timeNode : ''} 367 |
368 | {showTodayButton ? ( 369 |
375 | {todaySlot || language.TODAY} 376 |
377 | ) : null} 378 | {model === 'dialog' ? ( 379 |
380 | {confirmSlot || language.CONFIRM} 381 |
382 | ) : null} 383 |
384 | ); 385 | 386 | return isShowDatetimePicker ? ( 387 |
396 |
401 | {isShowAction ? actionNode : null} 402 | 416 | {pickerType !== 'date' ? ( 417 | 423 | ) : null} 424 |
425 |
426 | ) : null; 427 | } 428 | } 429 | 430 | export default ReactHashCalendar; 431 | -------------------------------------------------------------------------------- /src/components/datetimePicker/style.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/common.styl'; 2 | 3 | .hash-calendar { 4 | position: fixed; 5 | width: 100vw; 6 | height: 100vh; 7 | top: 0; 8 | left: 0; 9 | background: rgba(0, 0, 0, 0.6); 10 | z-index: 999; 11 | &.calendar_inline { 12 | position: relative; 13 | width: 100%; 14 | height: auto; 15 | background: none; 16 | height: px2vw(710px); 17 | z-index: 1; 18 | } 19 | .calendar_content { 20 | position: absolute; 21 | width: 100%; 22 | left: 0; 23 | bottom: 0; 24 | display: flex; 25 | padding-bottom: px2vw(26px); 26 | flex-wrap: wrap; 27 | background: white; 28 | height: px2vw(710px); 29 | overflow: hidden; 30 | } 31 | .calendar_title { 32 | position: absolute; 33 | width: 100%; 34 | left: 0; 35 | top: 0; 36 | background: bg-color; 37 | borderBottom(); 38 | display: flex; 39 | align-items: center; 40 | justify-content: space-between; 41 | z-index: 1; 42 | } 43 | .calendar_title_date { 44 | color: vice-font-color; 45 | background: white; 46 | padding: px2vw(30px) px2vw(15px); 47 | } 48 | .calendar_title_date_active { 49 | color: main-font-color; 50 | font-weight: bold; 51 | } 52 | .calendar_title_date_time { 53 | margin-left: px2vw(20px); 54 | } 55 | .calendar_confirm { 56 | color: main-color; 57 | margin-right: px2vw(34px); 58 | } 59 | .today_disable { 60 | color: disabled-font-color; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as ReactHashCalendar } from './datetimePicker'; 2 | export { default as Calendar } from './calendar'; 3 | export { default as TimePicker } from './timePicker'; -------------------------------------------------------------------------------- /src/components/timePicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './style.styl'; 3 | import classNames from 'classnames'; 4 | import { checkPlatform } from '../../utils/util'; 5 | import { ITime } from '../../utils/type'; 6 | 7 | const defaultProps = { 8 | show: false, 9 | defaultTime: new Date(), 10 | minuteStep: 1, 11 | }; 12 | 13 | export type TimePickerProps = { 14 | timeChangeCallback?: (date: ITime) => void; 15 | } & Partial; 16 | 17 | const state = { 18 | hashID: [''], 19 | hashClass: '', 20 | timeRange: [''], 21 | timeOptions: { 22 | minHours: 24, 23 | minMinutes: 59, 24 | maxHours: 0, 25 | maxMinutes: 0, 26 | }, 27 | checkedDate: { 28 | hours: new Date().getHours(), 29 | minutes: new Date().getMinutes(), 30 | }, 31 | timeHeight: 0, 32 | timeStartY: 0, 33 | timeStartUp: 0, 34 | }; 35 | 36 | type State = { 37 | timeArray?: number[][]; 38 | } & typeof state; 39 | 40 | class TimePicker extends React.Component< 41 | TimePickerProps & typeof defaultProps, 42 | State, 43 | {} 44 | > { 45 | static defaultProps = defaultProps; 46 | public state: State = state; 47 | 48 | componentDidMount() { 49 | const { defaultTime, timeChangeCallback } = this.props; 50 | const { checkedDate } = this.state; 51 | 52 | this.setState({ 53 | hashID: [ 54 | `time${Math.floor(Math.random() * 1000000)}`, 55 | `time${Math.floor(Math.random() * 1000000)}`, 56 | ], 57 | hashClass: `time_item_${Math.floor(Math.random() * 1000000)}`, 58 | }); 59 | 60 | if (defaultTime) { 61 | let _checkedDate: ITime = { 62 | ...checkedDate, 63 | hours: defaultTime.getHours(), 64 | minutes: defaultTime.getMinutes(), 65 | }; 66 | this.setState({ checkedDate: _checkedDate }); 67 | 68 | timeChangeCallback && timeChangeCallback(_checkedDate); 69 | } 70 | } 71 | 72 | componentDidUpdate( 73 | prevProps: TimePickerProps & typeof defaultProps, 74 | prevState: State 75 | ) { 76 | const { show: showPrev } = prevProps; 77 | const { show } = this.props; 78 | 79 | if (show !== showPrev && show) { 80 | setTimeout(() => { 81 | this.initTimeArray(); 82 | }); 83 | } 84 | } 85 | 86 | initTimeArray = () => { 87 | const { minuteStep } = this.props; 88 | const { checkedDate, hashClass, hashID } = this.state; 89 | 90 | let hours: number[] = []; 91 | let timeArray: number[][] = []; 92 | for (let i = 0; i < 24; i++) { 93 | hours.push(i); 94 | } 95 | let minutes: number[] = []; 96 | for (let i = 0; i < 60; i++) { 97 | if (i % minuteStep === 0) { 98 | minutes.push(i); 99 | } 100 | } 101 | timeArray.push(hours, minutes); 102 | 103 | this.setState({ timeArray }); 104 | 105 | let checkHours = checkedDate.hours; 106 | let checkMinutes = checkedDate.minutes; 107 | 108 | let timeEle = document.querySelector(`.${hashClass}`); 109 | if (!timeEle) return; 110 | 111 | let _timeHeight: string = getComputedStyle(timeEle).height || ''; 112 | let timeHeight = parseFloat(_timeHeight.split('px')[0]); 113 | 114 | this.setState({ timeHeight }); 115 | 116 | let hoursUp = (2 - checkHours) * timeHeight; 117 | let hourEle = document.querySelector(`#${hashID[0]}`) as HTMLElement; 118 | let minuteEle = document.querySelector(`#${hashID[1]}`) as HTMLElement; 119 | 120 | if (!hourEle || !minuteEle) return; 121 | 122 | let minutesUp = (2 - checkMinutes / minuteStep) * timeHeight; 123 | hourEle.style.webkitTransform = 'translate3d(0px,' + hoursUp + 'px,0px)'; 124 | minuteEle.style.webkitTransform = 125 | 'translate3d(0px,' + minutesUp + 'px,0px)'; 126 | }; 127 | 128 | timeTouchStart = (e: React.TouchEvent) => { 129 | e.preventDefault(); 130 | let timeStartY = e.changedTouches[0].pageY; 131 | this.setState({ timeStartY }); 132 | 133 | let eventEl = e.currentTarget as HTMLElement; 134 | let transform = eventEl.style.webkitTransform; 135 | if (transform) { 136 | let timeStartUp = parseFloat(transform.split(' ')[1].split('px')[0]); 137 | this.setState({ timeStartUp }); 138 | } 139 | }; 140 | 141 | timeTouchMove = (e: React.TouchEvent, index: number) => { 142 | const { timeStartY, timeStartUp } = this.state; 143 | 144 | let moveEndY = e.changedTouches[0].pageY; 145 | let Y = moveEndY - timeStartY; 146 | 147 | let eventEl = e.currentTarget as HTMLElement; 148 | eventEl.style.webkitTransform = 149 | 'translate3d(0px,' + (Y + timeStartUp) + 'px,0px)'; 150 | 151 | if (checkPlatform() === '2') { 152 | this.timeTouchEnd(e, index); 153 | return false; 154 | } 155 | }; 156 | 157 | timeTouchEnd = (e: React.TouchEvent, index: number) => { 158 | const { minuteStep, timeChangeCallback } = this.props; 159 | const { checkedDate, timeStartUp, timeHeight, timeArray } = this.state; 160 | 161 | let eventEl = e.currentTarget as HTMLElement; 162 | let transform = eventEl.style.webkitTransform; 163 | let endUp = timeStartUp; 164 | if (transform) { 165 | endUp = parseFloat( 166 | eventEl.style.webkitTransform.split(' ')[1].split('px')[0] 167 | ); 168 | } 169 | 170 | let distance = Math.abs(endUp - timeStartUp); 171 | let upCount = Math.floor(distance / timeHeight) || 1; 172 | let halfWinWith = timeHeight / 2; 173 | let up = timeStartUp; 174 | 175 | if (endUp <= timeStartUp) { 176 | // 向上滑动 未过临界值 177 | if (distance <= halfWinWith) { 178 | up = timeStartUp; 179 | } else { 180 | up = timeStartUp - timeHeight * upCount; 181 | 182 | if (timeArray && up < -(timeArray[index].length - 3) * timeHeight) { 183 | up = -(timeArray[index].length - 3) * timeHeight; 184 | } 185 | } 186 | } else { 187 | // 向下滑动 未过临界值 188 | if (distance <= halfWinWith) { 189 | up = timeStartUp; 190 | } else { 191 | up = timeStartUp + timeHeight * upCount; 192 | if (up > timeHeight * 2) { 193 | up = timeHeight * 2; 194 | } 195 | } 196 | } 197 | 198 | let _checkedDate: ITime; 199 | if (index === 0) { 200 | let hours = 2 - Math.round(up / timeHeight); 201 | _checkedDate = { ...checkedDate, hours }; 202 | } else { 203 | let minute = 2 - Math.round(up / timeHeight); 204 | _checkedDate = { ...checkedDate, minutes: minute * minuteStep }; 205 | } 206 | this.setState({ checkedDate: _checkedDate }); 207 | timeChangeCallback && timeChangeCallback(_checkedDate); 208 | 209 | eventEl.style.webkitTransition = 'transform 300ms'; 210 | eventEl.style.webkitTransform = 'translate3d(0px,' + up + 'px,0px)'; 211 | }; 212 | 213 | isBeSelectedTime = (time: number, index: number) => { 214 | // 是否为当前选中的时间 215 | const { checkedDate } = this.state; 216 | 217 | return ( 218 | (index === 0 && time === checkedDate.hours) || 219 | (index === 1 && time === checkedDate.minutes) 220 | ); 221 | }; 222 | 223 | // 小于10,在前面补0 224 | fillNumber = (val: number) => { 225 | return val > 9 ? val : '0' + val; 226 | }; 227 | 228 | render() { 229 | const { show } = this.props; 230 | const { timeArray, hashID, hashClass } = this.state; 231 | 232 | const timeItemNode = ( 233 | timeArr: number[], 234 | parentIndex: number 235 | ): React.ReactNode => { 236 | return timeArr.map((time: number, index: number) => ( 237 |
245 | {this.fillNumber(time)} 246 |
247 | )); 248 | }; 249 | 250 | const timeContentNode = (timeArray?: number[][]): React.ReactNode => { 251 | return ( 252 | timeArray && 253 | timeArray.map((item: number[], index: number) => ( 254 |
{ 260 | this.timeTouchMove(event, index); 261 | }} 262 | onTouchEnd={(event) => { 263 | this.timeTouchEnd(event, index); 264 | }} 265 | > 266 | {timeItemNode(item, index)} 267 |
268 | )) 269 | ); 270 | }; 271 | 272 | return show ? ( 273 |
274 |
{timeContentNode(timeArray)}
275 |
276 | ) : null; 277 | } 278 | } 279 | 280 | export default TimePicker; 281 | -------------------------------------------------------------------------------- /src/components/timePicker/style.styl: -------------------------------------------------------------------------------- 1 | @import '../../style/common.styl'; 2 | 3 | .hash-calendar { 4 | .time_body { 5 | width: 100%; 6 | margin-top: px2vw(100px); 7 | } 8 | .time_group { 9 | width: 100%; 10 | display: flex; 11 | align-items: flex-start; 12 | justify-content: center; 13 | height: px2vw(360px); 14 | margin-top: px2vw(100px); 15 | -webkit-overflow-scrolling: touch; 16 | overflow: hidden; 17 | } 18 | .time_content { 19 | touch-action: none; 20 | padding: 0 px2vw(40px); 21 | -webkit-overflow-scrolling: touch; 22 | } 23 | .time_item { 24 | padding: px2vw(20px) 0; 25 | color: vice-font-color; 26 | } 27 | .time_item_show { 28 | color: main-font-color; 29 | } 30 | .time_disabled { 31 | color: red; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/examples/index.tsx: -------------------------------------------------------------------------------- 1 | // import { ReactHashCalendar } from '../components'; 2 | import React from 'react'; 3 | 4 | const ReactHashCalendar = require('../components').ReactHashCalendar; 5 | 6 | const state = { 7 | defaultDatetime: new Date(), 8 | isShowCalendar: false, 9 | markDate: [ 10 | '2020/11/24', 11 | '2020/11/22', 12 | { 13 | color: 'red', 14 | type: 'dot', 15 | date: [ 16 | '0', 17 | '2020/02/25', 18 | '2020/03/25', 19 | '2020/04/01', 20 | '2020/05/25', 21 | '2020/06/25', 22 | '2020/07/25', 23 | '2020/08/25', 24 | '2020/09/25', 25 | '2020/10/25', 26 | '2020/11/25', 27 | '2020/12/25', 28 | ], 29 | }, 30 | { 31 | color: 'blue', 32 | type: 'circle', 33 | date: [ 34 | '2020/01/20', 35 | '2020/02/20', 36 | '2020/03/20', 37 | '2020/04/20', 38 | '2020/05/20', 39 | '2020/06/20', 40 | '2020/07/20', 41 | '2020/08/20', 42 | '2020/09/20', 43 | '2020/10/20', 44 | '2020/11/20', 45 | '2020/12/20', 46 | ], 47 | }, 48 | { 49 | color: 'pink', 50 | date: [ 51 | '2020/01/12', 52 | '2020/02/12', 53 | '2020/03/12', 54 | '2020/04/12', 55 | '2020/05/12', 56 | '2020/06/12', 57 | '2020/07/12', 58 | '2020/08/12', 59 | '2020/09/12', 60 | '2020/10/12', 61 | '2020/11/12', 62 | '2020/12/12', 63 | ], 64 | }, 65 | { 66 | color: '#000000', 67 | date: [ 68 | '2020/01/29', 69 | '2020/02/29', 70 | '2020/03/29', 71 | '2020/04/29', 72 | '2020/05/29', 73 | '2020/06/29', 74 | '2020/07/29', 75 | '2020/08/29', 76 | '2020/09/29', 77 | '2020/10/29', 78 | '2020/11/29', 79 | '2020/12/29', 80 | ], 81 | }, 82 | ], 83 | }; 84 | 85 | type State = typeof state; 86 | 87 | class Examples extends React.Component<{}, State, {}> { 88 | public state: State = state; 89 | 90 | handleVisibleChange = (isShowCalendar: boolean) => { 91 | this.setState({ isShowCalendar }); 92 | }; 93 | 94 | showCalendar = () => { 95 | this.setState({ isShowCalendar: true }); 96 | }; 97 | 98 | dateClick = (date?: string | Date) => { 99 | console.log('Examples -> dateClick -> date', date); 100 | }; 101 | 102 | dateConfirm = (date?: string | Date) => { 103 | console.log('Examples -> dateConfirm -> date', date); 104 | }; 105 | 106 | disabledDate = (date: Date): boolean => { 107 | let timestamp = date.getTime(); 108 | let oneDay = 24 * 60 * 60 * 1000; 109 | 110 | if (timestamp < new Date().getTime() - oneDay) { 111 | return true; 112 | } 113 | return false; 114 | }; 115 | 116 | render() { 117 | const { isShowCalendar, markDate, defaultDatetime } = this.state; 118 | return ( 119 |
120 | 121 | 143 |
144 | ); 145 | } 146 | } 147 | 148 | export default Examples; 149 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Examples from './examples/index'; 4 | import './style/reset.css'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/language/cn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description:中文 3 | * @Author: TSY 4 | * @Date: 2020-09-08 23:12:02 5 | * @LastEditTime: 2020-09-09 22:22:01 6 | */ 7 | 8 | export default { 9 | CONFIRM: '确定', 10 | TODAY: '今天', 11 | WEEK: ['日', '一', '二', '三', '四', '五', '六'], 12 | MONTH: [ 13 | '1月', 14 | '2月', 15 | '3月', 16 | '4月', 17 | '5月', 18 | '6月', 19 | '7月', 20 | '8月', 21 | '9月', 22 | '10月', 23 | '11月', 24 | '12月', 25 | ], 26 | DEFAULT_DATE_FORMAT: 'YY年MM月DD日', 27 | DEFAULT_TIME_FORMAT: 'hh:mm', 28 | }; 29 | -------------------------------------------------------------------------------- /src/language/en.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Description: 英文 3 | * @Author: TSY 4 | * @CreateDate: 2020/3/22 21:59 5 | */ 6 | 7 | export default { 8 | CONFIRM: 'CONFIRM', 9 | TODAY: 'TODAY', 10 | WEEK: ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'], 11 | MONTH: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'], 12 | DEFAULT_DATE_FORMAT: 'MM DD,YY', 13 | DEFAULT_TIME_FORMAT: 'at hh:mm F' 14 | } 15 | -------------------------------------------------------------------------------- /src/language/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @Description: 统一导出所有语言文件 3 | * @Author: TSY 4 | * @CreateDate: 2020/3/22 22:01 5 | */ 6 | 7 | import CN from './cn'; 8 | import EN from './en'; 9 | 10 | export default { 11 | CN, 12 | EN, 13 | }; 14 | -------------------------------------------------------------------------------- /src/style/common.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @Description: 3 | * @Author: TSY 4 | * @CreateDate: 2018/6/9 13:28 5 | */ 6 | .click_item:active { 7 | background: #eee; 8 | } 9 | .mask { 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | width: 100%; 14 | height: 100%; 15 | background: rgba(0,0,0,0.5); 16 | z-index: 999; 17 | } 18 | .pulldown-wrapper { 19 | top: -150px; 20 | } 21 | .iconfont { 22 | font-size: 34px; 23 | font-size: 4.533333333333333vw; 24 | } 25 | -------------------------------------------------------------------------------- /src/style/common.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * @Description: 3 | * @Author: TSY 4 | * @CreateDate: 2018/6/9 13:28 5 | */ 6 | 7 | //设计稿尺寸 8 | designSize = 750px 9 | // 页面主色 10 | main-color = #1c71fb 11 | //页面背景色 12 | bg-color = #f4f4f4 13 | //主文字颜色 14 | main-font-color = #4c4c4c 15 | //副文字颜色 16 | vice-font-color = #898989 17 | //禁用背景颜色 18 | disabled-bg-color = #f5f7fa 19 | //禁用文字颜色 20 | disabled-font-color = #c0c4cc 21 | //绿色 22 | green-color = #00cb69 23 | //红色 24 | red-color = #f80f30 25 | //橙色 26 | orange-color = #FF7800 27 | // 1px 实线边框 28 | solidBorder(color = bg-color) { 29 | border 1px solid color 30 | } 31 | 32 | borderBottom(color = bg-color) { 33 | border-bottom 1px solid color 34 | } 35 | 36 | borderTop(color = bg-color) { 37 | border-top 1px solid color 38 | } 39 | 40 | borderLeft(color = bg-color) { 41 | border-left 1px solid color 42 | } 43 | 44 | borderRight(color = bg-color) { 45 | border-right 1px solid color 46 | } 47 | 48 | bottomLine(color = bg-color) { 49 | border-bottom px2vw(16px) solid color 50 | } 51 | 52 | topLine(color = bg-color) { 53 | border-top px2vw(16px) solid color 54 | } 55 | 56 | //浏览器私有前缀属性扩展 57 | vendor(prop, args) { 58 | -webkit-{prop} args 59 | -moz-{prop} args 60 | {prop} args 61 | } 62 | 63 | //文字溢出显示省略号 64 | textOverflow() { 65 | overflow hidden 66 | text-overflow ellipsis 67 | white-space nowrap 68 | } 69 | 70 | //px转vw 71 | px2vw(size) { 72 | return (size /designSize * 100) vw 73 | } 74 | 75 | fontSize(size, isMoblie = true) 76 | if isMobile 77 | font-size size 78 | font-size px2vw(size) 79 | else 80 | font-size size 81 | 82 | paddingAround() { 83 | padding px2vw(30px) px2vw(34px) 84 | } 85 | 86 | paddingSmall() { 87 | padding px2vw(16px) px2vw(34px) 88 | } 89 | 90 | fixedTop() { 91 | position: fixed 92 | width 100% 93 | top 0 94 | left 0 95 | z-index 2 96 | } 97 | 98 | fixedBottom() { 99 | position: fixed 100 | width 100% 101 | bottom 0 102 | left 0 103 | z-index 2 104 | } 105 | 106 | flexAlign(align = center) { 107 | display flex 108 | align-items align 109 | } 110 | 111 | flexContent(align = center, justify = center) { 112 | display flex 113 | align-items align 114 | justify-content justify 115 | } 116 | 117 | flexBetween(align = center) { 118 | display flex 119 | align-items align 120 | justify-content space-between 121 | } 122 | 123 | flexAround(align = center) { 124 | display flex 125 | align-items align 126 | justify-content space-around 127 | } 128 | 129 | .click_item:active { 130 | background: #eee; 131 | } 132 | 133 | .mask { 134 | position fixed 135 | top 0 136 | left 0 137 | width 100% 138 | height 100% 139 | background rgba(0,0,0,0.5) 140 | z-index 999 141 | } 142 | 143 | .pulldown-wrapper { 144 | top: -150px; 145 | } 146 | 147 | .iconfont { 148 | fontSize(34px) 149 | } -------------------------------------------------------------------------------- /src/style/reset.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @Description: 3 | * @Author: TSY 4 | * @CreateDate: 2018/6/9 13:28 5 | */ 6 | html, 7 | body { 8 | width: 100%; 9 | border: 0; 10 | font-family: 'Helvetica-Neue', 'Helvetica', Arial, sans-serif; 11 | margin: 0; 12 | padding: 0; 13 | overflow-x: hidden; 14 | font-size: 4vw; 15 | background: #fff; 16 | color: #191919; 17 | } 18 | div, 19 | span, 20 | object, 21 | iframe, 22 | img, 23 | table, 24 | caption, 25 | thead, 26 | tbody, 27 | tfoot, 28 | tr, 29 | td, 30 | article, 31 | aside, 32 | canvas, 33 | details, 34 | figure, 35 | hgroup, 36 | menu, 37 | nav, 38 | footer, 39 | header, 40 | section, 41 | summary, 42 | mark, 43 | audio, 44 | video { 45 | border: 0; 46 | margin: 0; 47 | padding: 0; 48 | } 49 | h1, 50 | h2, 51 | h3, 52 | h4, 53 | h5, 54 | h6, 55 | p, 56 | blockquote, 57 | pre, 58 | a, 59 | abbr, 60 | address, 61 | cit, 62 | code, 63 | del, 64 | dfn, 65 | em, 66 | ins, 67 | q, 68 | samp, 69 | small, 70 | strong, 71 | sub, 72 | sup, 73 | b, 74 | i, 75 | hr, 76 | dl, 77 | dt, 78 | dd, 79 | ol, 80 | ul, 81 | li, 82 | fieldset, 83 | legend, 84 | label { 85 | border: 0; 86 | vertical-align: baseline; 87 | margin: 0; 88 | padding: 0; 89 | } 90 | article, 91 | aside, 92 | canvas, 93 | figure, 94 | figure img, 95 | figcaption, 96 | hgroup, 97 | footer, 98 | header, 99 | nav, 100 | section, 101 | audio, 102 | video { 103 | display: inline-block; 104 | } 105 | table { 106 | border-collapse: separate; 107 | border-spacing: 0; 108 | } 109 | table caption, 110 | table th, 111 | table td { 112 | text-align: left; 113 | vertical-align: middle; 114 | } 115 | li { 116 | list-style: none; 117 | } 118 | a { 119 | text-decoration: none; 120 | outline: none; 121 | display: inline-block; 122 | } 123 | a img { 124 | border: 0; 125 | } 126 | img { 127 | width: 100%; 128 | } 129 | :focus { 130 | outline: 0; 131 | } 132 | * { 133 | box-sizing: border-box; 134 | } 135 | textarea { 136 | resize: none; 137 | appearance: none; 138 | font-size: 4vw; 139 | } 140 | input { 141 | appearance: none; 142 | -webkit-appearance: none; 143 | font-size: 4vw; 144 | } 145 | select { 146 | appearance: none; 147 | background-color: #fff; 148 | font-size: 4vw; 149 | } 150 | button { 151 | border: none; 152 | font-size: 4vw; 153 | } 154 | p, 155 | span { 156 | letter-spacing: 1px; 157 | } 158 | .mescroll-totop-self { 159 | bottom: 50px !important; 160 | } 161 | .mint-indicator-wrapper { 162 | z-index: 99; 163 | } 164 | -------------------------------------------------------------------------------- /src/style/reset.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * @Description: 3 | * @Author: TSY 4 | * @CreateDate: 2018/6/9 13:28 5 | */ 6 | html, body { 7 | .hash-calendar { 8 | width: 100%; 9 | border: 0; 10 | font-family: "Helvetica-Neue", "Helvetica", Arial, sans-serif; 11 | margin: 0; 12 | padding: 0; 13 | overflow-x: hidden; 14 | font-size: 4vw; 15 | background: #fff; 16 | color: #191919 17 | } 18 | } 19 | 20 | .hash-calendar { 21 | div, span, object, iframe, img, table, caption, thead, tbody, 22 | tfoot, tr, tr, td, article, aside, canvas, details, figure, hgroup, menu, 23 | nav, footer, header, section, summary, mark, audio, video { 24 | border: 0; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, address, cit, code, 30 | del, dfn, em, ins, q, samp, small, strong, sub, sup, b, i, hr, dl, dt, dd, 31 | ol, ul, li, fieldset, legend, label { 32 | border: 0; 33 | vertical-align: baseline; 34 | margin: 0; 35 | padding: 0; 36 | } 37 | 38 | li { 39 | list-style: none; 40 | } 41 | 42 | * { 43 | box-sizing: border-box; 44 | 45 | &:focus { 46 | outline: 0; 47 | } 48 | } 49 | 50 | button { 51 | border: none; 52 | font-size: 4vw; 53 | } 54 | 55 | p, span { 56 | letter-spacing: 1px; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: 3 | * @Author: TSY 4 | * @Date: 2020-10-28 23:32:59 5 | * @LastEditTime: 2020-10-28 23:35:13 6 | */ 7 | import { tuple } from './type'; 8 | 9 | export const SCROLL_DIRECTION_LIST = tuple('left', 'right', 'up', 'down'); 10 | 11 | export const WEEK_LIST = tuple( 12 | 'Sunday', 13 | 'Monday', 14 | 'Tuesday', 15 | 'Wednesday', 16 | 'Thursday', 17 | 'Friday', 18 | 'Saturday' 19 | ); 20 | 21 | export const DIRECTION_LIST = tuple( 22 | '', 23 | 'all', 24 | 'left', 25 | 'right', 26 | 'up', 27 | 'down', 28 | 'horizontal', 29 | 'vertical' 30 | ); 31 | -------------------------------------------------------------------------------- /src/utils/eq.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | 3 | function isFunction(obj: any) { 4 | return toString.call(obj) === '[object Function]'; 5 | } 6 | 7 | export const eq = (a: any, b: any, aStack?: any, bStack?: any): boolean => { 8 | // === 结果为 true 的区别出 +0 和 -0 9 | if (a === b) return a !== 0 || 1 / a === 1 / b; 10 | 11 | // typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数 12 | if (a == null || b == null) return false; 13 | 14 | // 判断 NaN 15 | // eslint-disable-next-line 16 | if (a !== a) return b !== b; 17 | 18 | // 判断参数 a 类型,如果是基本类型,在这里可以直接返回 false 19 | var type = typeof a; 20 | if (type !== 'function' && type !== 'object' && typeof b != 'object') 21 | return false; 22 | 23 | // 更复杂的对象使用 deepEq 函数进行深度比较 24 | return deepEq(a, b, aStack, bStack); 25 | }; 26 | 27 | function deepEq(a: any, b: any, aStack: any, bStack: any) { 28 | // a 和 b 的内部属性 [[class]] 相同时 返回 true 29 | var className = toString.call(a); 30 | if (className !== toString.call(b)) return false; 31 | 32 | switch (className) { 33 | case '[object RegExp]': 34 | case '[object String]': 35 | return '' + a === '' + b; 36 | case '[object Number]': 37 | // eslint-disable-next-line 38 | if (+a !== +a) return +b !== +b; 39 | return +a === 0 ? 1 / +a === 1 / b : +a === +b; 40 | case '[object Date]': 41 | case '[object Boolean]': 42 | return +a === +b; 43 | } 44 | 45 | var areArrays = className === '[object Array]'; 46 | // 不是数组 47 | if (!areArrays) { 48 | // 过滤掉两个函数的情况 49 | if (typeof a != 'object' || typeof b != 'object') return false; 50 | 51 | var aCtor = a.constructor, 52 | bCtor = b.constructor; 53 | // aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦 54 | if ( 55 | aCtor === bCtor && 56 | !( 57 | isFunction(aCtor) && 58 | aCtor instanceof aCtor && 59 | isFunction(bCtor) && 60 | bCtor instanceof bCtor 61 | ) && 62 | 'constructor' in a && 63 | 'constructor' in b 64 | ) { 65 | return false; 66 | } 67 | } 68 | 69 | aStack = aStack || []; 70 | bStack = bStack || []; 71 | var length = aStack.length; 72 | 73 | // 检查是否有循环引用的部分 74 | while (length--) { 75 | if (aStack[length] === a) { 76 | return bStack[length] === b; 77 | } 78 | } 79 | 80 | aStack.push(a); 81 | bStack.push(b); 82 | 83 | // 数组判断 84 | if (areArrays) { 85 | length = a.length; 86 | if (length !== b.length) return false; 87 | 88 | while (length--) { 89 | if (!eq(a[length], b[length], aStack, bStack)) return false; 90 | } 91 | } 92 | // 对象判断 93 | else { 94 | var keys = Object.keys(a), 95 | key; 96 | length = keys.length; 97 | 98 | if (Object.keys(b).length !== length) return false; 99 | while (length--) { 100 | key = keys[length]; 101 | if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack))) 102 | return false; 103 | } 104 | } 105 | 106 | aStack.pop(); 107 | bStack.pop(); 108 | return true; 109 | } 110 | -------------------------------------------------------------------------------- /src/utils/type.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: 3 | * @Author: TSY 4 | * @Date: 2020-09-18 22:15:06 5 | * @LastEditTime: 2020-10-10 22:58:29 6 | */ 7 | export type Omit = Pick>; 8 | // https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead 9 | export const tuple = (...args: T) => args; 10 | 11 | export const tupleNum = (...args: T) => args; 12 | 13 | /** 14 | * https://stackoverflow.com/a/59187769 15 | * Extract the type of an element of an array/tuple without performing indexing 16 | */ 17 | export type ElementOf = T extends (infer E)[] 18 | ? E 19 | : T extends readonly (infer E)[] 20 | ? E 21 | : never; 22 | 23 | /** 24 | * https://github.com/Microsoft/TypeScript/issues/29729 25 | */ 26 | export type LiteralUnion = T | (U & {}); 27 | 28 | export interface IDate { 29 | year: number; 30 | month: number; 31 | day: number; 32 | } 33 | 34 | export interface ITime { 35 | hours: number; 36 | minutes: number; 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: 各种工具类 3 | * @Author: TSY 4 | * @Date: 2020-09-08 23:13:30 5 | * @LastEditTime: 2020-10-10 22:18:15 6 | */ 7 | 8 | /** 9 | * 判断安卓与IOS平台 10 | * @returns {string} 11 | */ 12 | export const checkPlatform = function () { 13 | if (/android/i.test(navigator.userAgent)) { 14 | return '1'; 15 | } 16 | if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) { 17 | return '2'; 18 | } 19 | }; 20 | 21 | /** 22 | * 日期格式化 23 | * @param time {string} 24 | * @param format {string} 25 | * @returns {string} 26 | */ 27 | export let formatDate = ( 28 | time: string | Date, 29 | format?: string, 30 | lang = 'CN' 31 | ): string => { 32 | lang = lang.toUpperCase(); 33 | let language = require('../language').default[lang] || {}; 34 | format = 35 | format || `${language.DEFAULT_DATE_FORMAT} ${language.DEFAULT_TIME_FORMAT}`; 36 | let date = time ? new Date(time) : new Date(); 37 | let year = date.getFullYear(); 38 | let month = date.getMonth() + 1; // 月份是从0开始的 39 | let day = date.getDate(); 40 | let hour = date.getHours(); 41 | let min = date.getMinutes(); 42 | let sec = date.getSeconds(); 43 | let preArr = Array.apply(null, Array(10)).map(function (elem, index) { 44 | return '0' + index; 45 | }); /// /开个长度为10的数组 格式为 00 01 02 03 46 | 47 | let newTime = format 48 | .replace(/YY/g, year + '') 49 | .replace(/F/g, hour >= 12 ? 'pm' : 'am') 50 | .replace(/ss/g, preArr[sec] || sec + '') 51 | .replace(/mm/g, preArr[min] || min + '') 52 | .replace( 53 | /hh/g, 54 | hour > 12 && format.includes('F') 55 | ? hour - 12 + '' 56 | : format.includes('F') 57 | ? hour + '' 58 | : preArr[hour] || hour + '' 59 | ) 60 | .replace(/DD/g, preArr[day] || day + '') 61 | .replace( 62 | /MM/g, 63 | lang === 'EN' ? language.MONTH[month - 1] : preArr[month] || month 64 | ); 65 | 66 | return newTime; 67 | }; 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react", 22 | "sourceMap": true, 23 | "noImplicitAny": false, 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } -------------------------------------------------------------------------------- /type.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ReactHashCalendar'; 2 | declare module 'Calendar'; 3 | declare module 'TimePicker'; 4 | --------------------------------------------------------------------------------