├── src ├── app.scss ├── pages │ └── index │ │ ├── index.scss │ │ ├── index.config.js │ │ ├── picker.scss │ │ ├── index.jsx │ │ ├── format.js │ │ └── picker.js ├── app.config.js ├── app.js └── index.html ├── .eslintrc ├── .gitignore ├── config ├── dev.js ├── prod.js └── index.js ├── .editorconfig ├── babel.config.js ├── .npmrc ├── project.config.json ├── package.json └── README.md /src/app.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/index/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["taro/react", "eslint:recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/index/index.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | navigationBarTitleText: '首页' 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | deploy_versions/ 3 | .temp/ 4 | .rn_temp/ 5 | node_modules/ 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /config/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"development"' 4 | }, 5 | defineConstants: { 6 | }, 7 | mini: {}, 8 | h5: {} 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel-preset-taro 更多选项和默认值: 2 | // https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md 3 | module.exports = { 4 | presets: [ 5 | ['taro', { 6 | framework: 'react', 7 | ts: false 8 | }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/app.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | pages: [ 3 | 'pages/index/index' 4 | ], 5 | window: { 6 | backgroundTextStyle: 'light', 7 | navigationBarBackgroundColor: '#fff', 8 | navigationBarTitleText: 'WeChat', 9 | navigationBarTextStyle: 'black' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import './app.scss' 3 | 4 | class App extends Component { 5 | 6 | componentDidMount () {} 7 | 8 | componentDidShow () {} 9 | 10 | componentDidHide () {} 11 | 12 | componentDidCatchError () {} 13 | 14 | // this.props.children 是将要会渲染的页面 15 | render () { 16 | return this.props.children 17 | } 18 | } 19 | 20 | export default App 21 | -------------------------------------------------------------------------------- /config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"production"' 4 | }, 5 | defineConstants: { 6 | }, 7 | mini: {}, 8 | h5: { 9 | /** 10 | * 如果h5端编译后体积过大,可以使用webpack-bundle-analyzer插件对打包体积进行分析。 11 | * 参考代码如下: 12 | * webpackChain (chain) { 13 | * chain.plugin('analyzer') 14 | * .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) 15 | * } 16 | */ 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org 2 | disturl=https://npm.taobao.org/dist 3 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ 4 | phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/ 5 | electron_mirror=https://npm.taobao.org/mirrors/electron/ 6 | chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver 7 | operadriver_cdnurl=https://npm.taobao.org/mirrors/operadriver 8 | selenium_cdnurl=https://npm.taobao.org/mirrors/selenium 9 | node_inspector_cdnurl=https://npm.taobao.org/mirrors/node-inspector 10 | fsevents_binary_host_mirror=http://npm.taobao.org/mirrors/fsevents/ 11 | -------------------------------------------------------------------------------- /src/pages/index/picker.scss: -------------------------------------------------------------------------------- 1 | .index{ 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | .section { 6 | display: flex; 7 | width: 100%; 8 | justify-content: center; 9 | picker-view { 10 | width: 100%; 11 | height: 560px; 12 | picker-view-column { 13 | text-align: center; 14 | font-size: 26px; 15 | view { 16 | line-height: 100px; 17 | } 18 | } 19 | } 20 | } 21 | .handle { 22 | display: flex; 23 | flex-direction: row; 24 | justify-content: center; 25 | align-items: center; 26 | height: 100px; 27 | button { 28 | height: 70px; 29 | line-height: 70px; 30 | font-size: 32px; 31 | padding: 0 80px; 32 | margin: 0 20px; 33 | } 34 | .confirm { 35 | background-color: #07C160; 36 | } 37 | .cancel { 38 | background-color: #F2F2F2; 39 | color: #1AAD19; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "dist/", 3 | "projectname": "picker", 4 | "description": "picker for taro", 5 | "appid": "wx54f7ddca043d7b7a", 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": false, 9 | "enhance": false, 10 | "postcss": false, 11 | "preloadBackgroundData": false, 12 | "minified": false, 13 | "newFeature": false, 14 | "coverView": true, 15 | "nodeModules": false, 16 | "autoAudits": false, 17 | "showShadowRootInWxmlPanel": true, 18 | "scopeDataCheck": false, 19 | "uglifyFileName": false, 20 | "checkInvalidKey": true, 21 | "checkSiteMap": true, 22 | "uploadWithSourceMap": true, 23 | "compileHotReLoad": false, 24 | "useMultiFrameRuntime": false, 25 | "useApiHook": true, 26 | "babelSetting": { 27 | "ignore": [], 28 | "disablePlugins": [], 29 | "outputPath": "" 30 | }, 31 | "enableEngineNative": false, 32 | "bundle": false, 33 | "useIsolateContext": true, 34 | "useCompilerModule": true, 35 | "userConfirmedUseCompilerModuleSwitch": false, 36 | "userConfirmedBundleSwitch": false, 37 | "packNpmManually": false, 38 | "packNpmRelationList": [], 39 | "minifyWXSS": true 40 | }, 41 | "compileType": "miniprogram", 42 | "simulatorType": "wechat", 43 | "simulatorPluginLibVersion": {}, 44 | "condition": {} 45 | } -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | projectName: 'picker', 3 | date: '2020-12-4', 4 | designWidth: 750, 5 | deviceRatio: { 6 | 640: 2.34 / 2, 7 | 750: 1, 8 | 828: 1.81 / 2 9 | }, 10 | sourceRoot: 'src', 11 | outputRoot: 'dist', 12 | plugins: [], 13 | defineConstants: { 14 | }, 15 | copy: { 16 | patterns: [ 17 | ], 18 | options: { 19 | } 20 | }, 21 | framework: 'react', 22 | mini: { 23 | postcss: { 24 | pxtransform: { 25 | enable: true, 26 | config: { 27 | 28 | } 29 | }, 30 | url: { 31 | enable: true, 32 | config: { 33 | limit: 1024 // 设定转换尺寸上限 34 | } 35 | }, 36 | cssModules: { 37 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 38 | config: { 39 | namingPattern: 'module', // 转换模式,取值为 global/module 40 | generateScopedName: '[name]__[local]___[hash:base64:5]' 41 | } 42 | } 43 | } 44 | }, 45 | h5: { 46 | publicPath: '/', 47 | staticDirectory: 'static', 48 | postcss: { 49 | autoprefixer: { 50 | enable: true, 51 | config: { 52 | } 53 | }, 54 | cssModules: { 55 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 56 | config: { 57 | namingPattern: 'module', // 转换模式,取值为 global/module 58 | generateScopedName: '[name]__[local]___[hash:base64:5]' 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | module.exports = function (merge) { 66 | if (process.env.NODE_ENV === 'development') { 67 | return merge({}, config, require('./dev')) 68 | } 69 | return merge({}, config, require('./prod')) 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "picker", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "picker for taro", 6 | "templateInfo": { 7 | "name": "default", 8 | "typescript": false, 9 | "css": "sass" 10 | }, 11 | "scripts": { 12 | "build:weapp": "taro build --type weapp", 13 | "build:swan": "taro build --type swan", 14 | "build:alipay": "taro build --type alipay", 15 | "build:tt": "taro build --type tt", 16 | "build:h5": "taro build --type h5", 17 | "build:rn": "taro build --type rn", 18 | "build:qq": "taro build --type qq", 19 | "build:jd": "taro build --type jd", 20 | "build:quickapp": "taro build --type quickapp", 21 | "dev:weapp": "npm run build:weapp -- --watch", 22 | "dev:swan": "npm run build:swan -- --watch", 23 | "dev:alipay": "npm run build:alipay -- --watch", 24 | "dev:tt": "npm run build:tt -- --watch", 25 | "dev:h5": "npm run build:h5 -- --watch", 26 | "dev:rn": "npm run build:rn -- --watch", 27 | "dev:qq": "npm run build:qq -- --watch", 28 | "dev:jd": "npm run build:jd -- --watch", 29 | "dev:quickapp": "npm run build:quickapp -- --watch" 30 | }, 31 | "browserslist": [ 32 | "last 3 versions", 33 | "Android >= 4.1", 34 | "ios >= 8" 35 | ], 36 | "author": "", 37 | "dependencies": { 38 | "@babel/runtime": "^7.7.7", 39 | "@tarojs/components": "3.0.18", 40 | "@tarojs/react": "3.0.18", 41 | "@tarojs/runtime": "3.0.18", 42 | "@tarojs/taro": "3.0.18", 43 | "dayjs": "^1.9.6", 44 | "react": "^16.10.0", 45 | "react-dom": "^16.10.0" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.8.0", 49 | "@tarojs/mini-runner": "3.0.18", 50 | "@tarojs/webpack-runner": "3.0.18", 51 | "@types/react": "^16.0.0", 52 | "@types/webpack-env": "^1.13.6", 53 | "babel-preset-taro": "3.0.18", 54 | "eslint": "^6.8.0", 55 | "eslint-config-taro": "3.0.18", 56 | "eslint-plugin-import": "^2.12.0", 57 | "eslint-plugin-react": "^7.8.2", 58 | "eslint-plugin-react-hooks": "^1.6.1", 59 | "stylelint": "9.3.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于 Taro 实现的时间选择器 2 | 3 | 本项目基于 pickerView、pickerViewColumn 来自定义了一个选择器用于日期和时间在用一个组件中选择完成。项目使用 taro 框架,也可实现类似微信提醒的时间选择器 4 | 5 | 6 | 7 | 8 | ## 简介 9 | 10 | 本项目所实现的组件拥有灵活的配置项可轻松实现如下的时间选择功能: 11 | 12 | - 年月日时分秒全部支持 13 | - 类似微信提醒中的今天、x 月 x 日 明天、x 月 x 日 后天、x 月 x 日 周一...等时间选择 14 | - 一个周期内的某几个时间点选择 (如一天内的 8 点、12 点、16 点) 15 | - 一个周期内的固定间隔时间 (如 10 分、20 分、30 分、40 分)等 16 | - 配合时间偏移属性(offset)支持默认时间选取下个整点或整十分等 17 | - 模仿了微信默认的确认取消按钮,和微信的选择器保持体验一致,并通过 props 和父组件交互 18 | 19 | 最新的版本组件利用了[day.js](https://day.js.org/zh-CN/)来代替 moment-mini,得益于 day.js,将原来的 75KB 大小的日期处理库降为了 2KB 的大小。 20 | ~~组件利用了[moment](http://momentjs.cn/)这个日期处理类库的部分特性来让代码更加简洁和稳定,为了保持小程序包的体积尽量小,组件 import 了 moment-mini 这个 npm 包,~~ 21 | 当你调用 onInitial 和 onConfirm 时,可通过指定 mode 来获得返回时间的数据格式 22 | 23 | ## 如何使用 24 | 25 | 可参考 src/pages/index/index.jsx 的使用方法 26 | 27 | #### dateTime 的使用: 28 | 29 | dataTime 参数是多个对象元素的数组,每个对象可支持如下参数选择: 30 | 31 | - mode: ['year', 'month', 'day', 'hour', 'minute', 'second'] 32 | - unit: 界面中的日期标记,例如年,月,日,时,分,秒或小时的:00 33 | - duration: 显示当前模式的最大数量,例如 10 年或 30 天(只可用于年和日) 34 | - fields: 可指定当前模式中间隔固定时间间断进行显示,例如只显示 10 秒、20 秒、30 秒(只可用于月、小时、分钟、秒钟),不能和 selected 同时指定 35 | - humanity:可显示类似微信提醒的时间选择列表,如今天,XX 月 XX 日 明天,XX 月 XX 日 周 X。需搭配 format: 'M 月 D 日'使用,目前仅适用于 mode 为 day 时 36 | - format: 用于解决组合时间的格式化问题,正常情况不需要指定,只有当遇到组合(如 X 月 X 日)或者指定 unit(例如:00)时使用 37 | - selected: 可指定当前模式下只选择有效范围内的哪几个元素,例如 24 小时内,只选择 8 点、12 点、16 点,,不能和 fields 同时指定 38 | - selectedDefault: 指定 selected 的默认值(而不用当前时间去匹配) 39 | - offset: 设定当前时间点偏移量(常用于设定下一小时或者下一天等) 40 | 41 | dateTime 举例: 42 | 43 | - {mode: 'year', unit: '年' }, 44 | - {mode: 'month', unit: '月'}, 45 | - { mode: 'day', duration: 90, unit: '日', humanity: true, format: 'M月D日' }, 46 | - { mode: 'hour', unit: ':00', format: 'H:s', selected: [8, 12, 16] }, 47 | - { mode: 'hour', unit: '时' }, 48 | - {mode: 'minute', fields: 10, unit: '分'}, 49 | - {mode: 'second', fields: 30, unit: '秒'}, 50 | 51 | ## 新版更新(2020-12-08): 52 | 53 | 将 master branch 升级到 taro3,之前的版本在 taro2 的 branch 54 | 支持了多列选择,可以轻松实现请假开始日期和请假结束日期在一个组件里面选择 55 | 修复了已有的一些 bug,同时将代码的注释完全写清楚(尤其核心 format.js) 56 | 57 | ## 新版更新(2020-03-30): 58 | 59 | 将组件和 format 逻辑分开,同时支持更多的参数选择。 60 | 61 | ## 组件的使用 62 | 63 | #### dateTime 64 | 65 | 传入需要显示的时间模式,具体请参考 dateTime 的使用 66 | 67 | #### onConfirm() 68 | 69 | 点击确认时触发,传递参数为选择器当前选择的时间(dayjs 格式) 70 | 71 | #### onCancel() 72 | 73 | 点击取消时触发,可被父组件用于触发弹框隐藏之类的操作 74 | 75 | #### onInitial() 76 | 77 | 组件加载时触发,通知父组件初始化时本组件的时间选择结果(由于初始化为当前时间可能为下一个时间周期例如使用 fields、selected 等参数,故本操作可把初始化时间直接传给父组件用于展示) 78 | 79 | #### mode 80 | 81 | 指定 onInitial 和 onConfirm 时返回的日期格式,支持[format](https://day.js.org/docs/zh-CN/display/format)、[unix](https://day.js.org/docs/zh-CN/display/unix-timestamp)、[toString](https://day.js.org/docs/zh-CN/display/as-string)、[fromNow](https://day.js.org/docs/zh-CN/display/from-now)等等,具体请参考 dayjs 文档。 82 | -------------------------------------------------------------------------------- /src/pages/index/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View } from '@tarojs/components' 3 | // import dayjs from 'dayjs' 4 | import Picker from './picker' 5 | import './index.scss' 6 | 7 | export default class Index extends Component { 8 | componentWillMount() {} 9 | 10 | componentDidMount() {} 11 | 12 | componentWillUnmount() {} 13 | 14 | componentDidShow() {} 15 | 16 | componentDidHide() {} 17 | 18 | handleInitial = (value, index) => { 19 | console.log('initial value: ', value, ', selected index: ', index) 20 | } 21 | 22 | handleConfirm = (value, index) => { 23 | console.log('confirm value: ', value, ', selected index: ', index) 24 | } 25 | 26 | handleCancel = () => { 27 | console.log('cancel action') 28 | } 29 | 30 | render() { 31 | /*支持参数: 32 | mode: ['year', 'month', 'day', 'hour', 'minute', 'second'] 33 | unit: 界面中的单位标记,例如年,月,日,时,分,秒或小时的:00 34 | duration: 显示当前模式的最大数量,例如10年或30天(只可用于年和日) 35 | fields: 可指定当前模式中间隔固定时间间断进行显示,例如只显示10秒、20秒、30秒(只可用于月、小时、分钟、秒钟),不能和selected同时指定 36 | humanity:可显示类似微信提醒的时间选择列表,如今天,XX月XX日 明天,XX月XX日 周X。需搭配format: 'M月D日'使用,目前仅适用于mode为day时 37 | format: 用于解决组合时间的格式化问题,正常情况不需要指定,只有当遇到组合(如X月X日)或者指定unit(例如:00)时使用 38 | selected: 可指定当前模式下只选择有效范围内的哪几个元素,例如24小时内,只选择8点、12点、16点,,不能和fields同时指定 39 | selectedDefault: 指定selected的默认值(而不用当前时间去匹配) 40 | offset: 设定当前时间点偏移量(常用于设定下一小时或者下一天等) 41 | */ 42 | const dateTime = [ 43 | { mode: 'year', unit: '年' }, 44 | { mode: 'month', unit: '月' }, 45 | { mode: 'day', duration: 30, unit: '日' }, 46 | 47 | // { mode: 'hour', unit: ':00', format: 'H:s', selected: [9, 12] }, 48 | // { mode: 'hour', unit: '时' }, 49 | // {mode: 'year', unit: '年' }, 50 | // {mode: 'month', unit: '月'}, 51 | // { mode: 'day', duration: 30, unit: '日', humanity: true, format: 'M月D日' }, 52 | // {mode: 'day', duration: 30, unit: '日' }, 53 | // { mode: 'hour', unit: ':00', format: 'H:s', selected: [8, 12, 16] }, 54 | // { mode: 'minute', fields: 10, unit: '分' }, 55 | // {mode: 'second', fields: 30, unit: '秒'}, 56 | ] 57 | 58 | // const dayModel = { mode: 'day', duration: 30, unit: '日', humanity: true, format: 'M月D日', offset: 0 } 59 | // const hourModel = { mode: 'hour', unit: ':00', format: 'H:s', selected: [8, 9, 10, 11, 12, 13, 14, 15, 16] } 60 | // const nowHour = dayjs().hour() 61 | // const dividePoint = [0, 8, 12] 62 | // const dateIndex = dividePoint.reduce((result, current, index, arr) => { 63 | // if (nowHour < current) { 64 | // arr.splice(1) 65 | // return index - 1 66 | // } else { 67 | // return dividePoint.length - 1 68 | // } 69 | // }, 0) 70 | // const dateTime = [ 71 | // [ 72 | // [dayModel, { ...hourModel, ...{ selectedDefault: 0 } }], 73 | // [dayModel, { ...hourModel, ...{ selectedDefault: 1 } }], 74 | // ], 75 | // [ 76 | // [dayModel, { ...hourModel, ...{ selectedDefault: 4 } }], 77 | // [dayModel, { ...hourModel, ...{ selectedDefault: 5 } }], 78 | // ], 79 | // [ 80 | // [dayModel, { ...hourModel, ...{ selectedDefault: 0 } }], 81 | // [dayModel, { ...hourModel, ...{ selectedDefault: 1 } }], 82 | // ], 83 | // ][dateIndex] 84 | 85 | return ( 86 | 87 | 102 | 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pages/index/format.js: -------------------------------------------------------------------------------- 1 | // 输出的结果包含item为显示的可选项,value为默认显示值 2 | // res为返回的结果,time为返回的默认时间 3 | // 整个代码流程为: 4 | // 1.如果指定了offset,则根据offset属性将time做偏移作为time(为了实现当前8:30,需要默认选择9点这样的需求) 5 | // 2.如果指定了selected,则根据第一步的结果寻找大于或者等于time的可选时间点作为新的time, 6 | // 如果无法匹配到合适的selected(findIndex返回-1),则设定time为下个周期的开始时间 7 | // 如果同时指定了selectedDefault,则设定time为selected的selectedDefault对应的值 8 | // 3.根据mode来区分开年/日---月/时/分/秒---range,对三种mode分别进行单独的处理 9 | 10 | const format = (dateTime, dayjs) => { 11 | // res为返回的结果,time为返回的默认时间 12 | let res = { value: [], item: [] }, 13 | time = dayjs.clone() 14 | // 注释掉这里是因为这里判断fields的同时只做了增加到下一个时间点的操作,完全可以用fields和offset搭配实现select下一个时间点 15 | // dateTime 16 | // .filter((key) => key.fields > 1) 17 | // .map((item) => { 18 | // time = time.add(item.fields, item.mode) 19 | // }) 20 | 21 | // offset: 设定time偏移量(常用于设定下一小时或者下一天等) 22 | dateTime 23 | .filter((key) => key.offset) 24 | .map((item) => { 25 | time = time.add(item.offset, item.mode) 26 | }) 27 | 28 | // selected: 可指定当前模式下只选择有效范围内的哪几个元素,例如24小时内,只选择8点、12点、16点 29 | // 寻找大于或者等于time的最近的可选择时间点 30 | dateTime 31 | .filter((key) => key.selected) 32 | .map((item) => { 33 | const { selected, mode, selectedDefault } = item 34 | const index = selected.findIndex((value) => value >= time.get(mode)) 35 | if (index === -1) { 36 | // 如果未找到可选时间点,就设定time为下个周期(如第二天)的开始时间 37 | time = time.add(getInverval(mode) - time.get(mode), mode) 38 | } else { 39 | // 如果找到可选时间点,就设定time为这个时间点 40 | time = time.set(mode, selected[index]) 41 | } 42 | // 如果设定了selectedDefault,则将time设定为default对应的时间点 43 | if (selectedDefault !== undefined) { 44 | time = time.set(mode, selected[selectedDefault]) 45 | } 46 | }) 47 | // 此处针对offset、selected和selectedDefault属性处理完成 48 | 49 | dateTime.map((item) => { 50 | const { mode, selected, selectedDefault, unit = '', fields = 1 } = item 51 | // 此处区分年、天有别于其他周期在于年和天都不是标准周期(每个月不同天,年一直在增长) 52 | if (['month', 'hour', 'minute', 'second'].includes(mode)) { 53 | // 设定显示的可选项 54 | // [...Array(getInverval('hour')).keys()]获取24个小时的数字的数组:[0,1,...,23] 55 | res.item.push( 56 | [...Array(getInverval(mode)).keys()] 57 | // 按照fields来平均分割这些数字 58 | .filter((i) => i % fields === 0) 59 | // 如果设置了selected属性,只返回特定的数字 60 | .filter((i) => { 61 | if (selected) return selected.includes(i) 62 | else return true 63 | }) 64 | // 在所有的数字后面添加单位(unit);由于月份为0-11,需要转换为1-12 65 | .map((i) => (mode === 'month' ? i + 1 : i) + unit) 66 | ) 67 | // 根据time的值和selected的匹配来设定默认值为数组中第几个,当未指定selected或者定位到下个周期时,设定index为-1 68 | const index = selected && selected.findIndex((value) => value === time.get(mode)) 69 | // 设定显示的默认选择项 70 | res.value.push(selected ? (index < 0 ? 0 : index) : ~~(time.get(mode) / fields)) 71 | } else if (['year', 'day'].includes(mode)) { 72 | // 根据 73 | res.item.push(timeFormat(item, dayjs)) 74 | // 寻找time和dayjs时间点的差值作为默认选择值 75 | res.value.push(selectedDefault || time.startOf(mode).diff(dayjs.startOf(mode), mode)) 76 | } else { 77 | return 78 | } 79 | }) 80 | return res 81 | } 82 | 83 | // 对于年和日,获取显示的可选项 84 | const timeFormat = (time, dayjs) => { 85 | const { mode, duration = 30, unit = '', humanity = false } = time, 86 | res = [] 87 | for (let i = 0; i < duration; i++) { 88 | let timeItem 89 | // 利用dayjs[convertDay(mode)]来动态实现dayjs.date()或dayjs.year()的方法,用computedTime来实现可选择项的生成 90 | const computedTime = dayjs.add(i, mode) 91 | if (humanity && mode === 'day') { 92 | timeItem = `${computedTime.month() + 1}月${computedTime.date()}日` 93 | if (i < 3) { 94 | // 今天明天后天这三天明确显示出来 95 | timeItem = (i === 0 ? '' : timeItem + ' ') + convertDate(i) 96 | } else { 97 | // 在日期后添加星期几,更人性化 98 | timeItem += ` ${convertWeek(computedTime.day())}` 99 | } 100 | } else { 101 | timeItem = computedTime.get(convertDay(mode)) + unit 102 | } 103 | res.push(timeItem) 104 | } 105 | return res 106 | } 107 | 108 | const convertWeek = (item) => { 109 | return { 0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六' }[item] 110 | } 111 | 112 | const convertDate = (item) => { 113 | return { 0: '今天', 1: '明天', 2: '后天' }[item] 114 | } 115 | 116 | // dayjs的get分为day of week和date of month,故需要将day转换为date使用 117 | const convertDay = (item) => { 118 | return { day: 'date' }[item] || item 119 | } 120 | 121 | const getInverval = (item) => { 122 | return { hour: 24, minute: 60, second: 60, month: 12 }[item] 123 | } 124 | 125 | export default format 126 | -------------------------------------------------------------------------------- /src/pages/index/picker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { View, PickerView, PickerViewColumn, Button } from '@tarojs/components' 3 | import dayjs from 'dayjs' 4 | import 'dayjs/locale/zh-cn' 5 | import customParseFormat from 'dayjs/plugin/customParseFormat' 6 | import format from './format' 7 | import './picker.scss' 8 | 9 | dayjs.extend(customParseFormat) 10 | export default class Index extends Component { 11 | constructor() { 12 | super(...arguments) 13 | this.state = { 14 | value: [], 15 | source: [], 16 | markMultiDateTime: false, 17 | } 18 | } 19 | componentWillMount() { 20 | const { dateTime, start, value: _value } = this.props 21 | let markMultiDateTime = false 22 | if (dateTime && Array.isArray(dateTime)) { 23 | dateTime.map((dateTimeItem) => { 24 | //判断一维数组还是二维数组,分别对应单组选择和两组选择 25 | if (Array.isArray(dateTimeItem)) { 26 | markMultiDateTime = true 27 | // 取得格式化计算之后的结果 28 | const source = dateTimeItem && format(dateTimeItem, dayjs(start)) 29 | // 后续需要source而不需要item的原因是source可能是多纬数组,每个纬度里面包含自己的item和value 30 | this.setState((state) => ({ 31 | ...state, 32 | source: [...state.source, source], 33 | value: [...state.value, source.value], 34 | })) 35 | // this.setState((state) => ({ ...state, value: [...state.value, source.value] })) 36 | } 37 | }) 38 | if (!markMultiDateTime) { 39 | const source = dateTime && format(dateTime, dayjs(start)) 40 | this.setState((state) => ({ 41 | ...state, 42 | source: [...state.source, source], 43 | value: [...state.value, source.value], 44 | })) 45 | // this.setState((state) => ({ ...state, source: [...state.source, source] })) 46 | // this.setState((state) => ({ ...state, value: [...state.value, source.value] })) 47 | } 48 | this.setState((state) => ({ ...state, markMultiDateTime })) 49 | } 50 | _value && this.setState((state) => ({ ...state, ...{ value: markMultiDateTime ? _value : [_value] } })) 51 | } 52 | 53 | componentDidMount() { 54 | this.onInitial() 55 | } 56 | 57 | componentWillUnmount() {} 58 | 59 | componentDidShow() {} 60 | 61 | componentDidHide() {} 62 | 63 | onChange = (e, index) => { 64 | const _value = [...this.state.value] 65 | _value[index] = e.detail.value 66 | this.setState({ value: _value }) 67 | } 68 | 69 | onInitial = () => { 70 | const { value, markMultiDateTime } = this.state 71 | const { onInitial, mode } = this.props 72 | // 根据返回格式(mode)来格式化选中的时间 73 | onInitial && onInitial(this.getDayjs(mode), markMultiDateTime ? value : value[0]) 74 | } 75 | 76 | onConfirm = () => { 77 | const { value, markMultiDateTime } = this.state 78 | const { onConfirm, mode } = this.props 79 | onConfirm && onConfirm(this.getDayjs(mode), markMultiDateTime ? value : value[0]) 80 | } 81 | 82 | // 根据可选项和当前选择索引返回已选中的时间 83 | getDayjs = (mode = 'unix') => { 84 | let { source, value, markMultiDateTime } = this.state 85 | const { dateTime } = this.props 86 | const res = [] 87 | // 此处遍历dateTime和遍历source的区别在于一维数组还是二维数组 88 | for (let i = 0; i < source.length; i++) { 89 | let time = '', 90 | token = '' 91 | // source[i].item.length为可选项的列数 92 | for (let j = 0; j < source[i].item.length; j++) { 93 | // source[i].item[j]为每一列的数据组成的数组,value[i][j]为对应这列数组的选中值 94 | const select = source[i].item[j][value[i][j]] 95 | // 对'今天'这个值进行特殊处理,其他直接返回当前的选择字符串 96 | time += (select === '今天' ? dayjs().format('M月D日') : select) + '-' 97 | // 对于二维数组取i、j;对于一维数组取j 98 | const item = markMultiDateTime ? dateTime[i][j] : dateTime[j] 99 | token += (item.format || this.getToken(item.mode)) + '-' 100 | } 101 | res.push(dayjs(time, token)[mode]()) 102 | } 103 | return markMultiDateTime ? res : res[0] 104 | } 105 | 106 | onCancel = () => { 107 | const { onCancel } = this.props 108 | onCancel && onCancel() 109 | } 110 | 111 | // 标准格式化选择器 112 | getToken = (mode) => { 113 | return { 114 | year: 'YYYY年', 115 | month: 'M月', 116 | day: 'D日', 117 | hour: 'H时', 118 | minute: 'm分', 119 | second: 's秒', 120 | }[mode] 121 | } 122 | 123 | render() { 124 | const { source, value } = this.state 125 | const { dateTime = [] } = this.props 126 | return ( 127 | acc.concat(val), []).length < 4 ? '80%' : '100%' }} 130 | > 131 | 132 | {source.map((element, index) => ( 133 | this.onChange(e, index)} 138 | // 使用acc.concat将多维数组打平成一维数组再求数组长度 139 | > 140 | {element.item.map((item, elementIndex) => ( 141 | 142 | {item.map((time) => ( 143 | {time} 144 | ))} 145 | 146 | ))} 147 | 148 | ))} 149 | 150 | 151 | 154 | 157 | 158 | 159 | ) 160 | } 161 | } 162 | --------------------------------------------------------------------------------