mirror of
https://github.com/tenrok/vue2-datepicker.git
synced 2026-05-17 04:39:40 +03:00
refactor: 2.0
This commit is contained in:
@@ -7,5 +7,21 @@
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"transform-vue-jsx",
|
||||
"jsx-v-model"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [
|
||||
["env", { "target": { "node": "current" }}],
|
||||
"stage-3"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-vue-jsx",
|
||||
"jsx-v-model"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/build/
|
||||
/config/
|
||||
/dist/
|
||||
/*.js
|
||||
/__test__/
|
||||
/test/
|
||||
@@ -0,0 +1,43 @@
|
||||
// https://eslint.org/docs/user-guide/configuring
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
extends: [
|
||||
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
|
||||
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
|
||||
'plugin:vue/essential',
|
||||
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
|
||||
'standard'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow async-await
|
||||
'generator-star-spacing': 'off',
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'camelcase': ['off', { properties: 'never' }],
|
||||
// "vue/max-attributes-per-line": [2, {
|
||||
// "singleline": 1,
|
||||
// "multiline": {
|
||||
// "max": 1,
|
||||
// "allowFirstLine": true
|
||||
// }
|
||||
// }],
|
||||
"vue/html-indent": ["error", 2, {
|
||||
"attribute": 1,
|
||||
"closeBracket": 0,
|
||||
"alignAttributesVertically": false,
|
||||
"ignores": []
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
lib
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
coverage
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
- "8"
|
||||
script: "npm run test:push"
|
||||
@@ -1,11 +1,21 @@
|
||||
# vue2-datepicker
|
||||
|
||||
[中文版](https://github.com/mengxiong10/vue2-datepicker/blob/master/README_CN.md)
|
||||
[中文版](https://github.com/mengxiong10/vue2-datepicker/blob/master/README.zh-CN.md)
|
||||
|
||||
> A Datepicker Component For Vue2
|
||||
|
||||
<a href="https://travis-ci.org/mengxiong10/vue2-datepicker">
|
||||
<img src="https://travis-ci.org/mengxiong10/vue2-datepicker.svg?branch=master" alt="build:passed">
|
||||
</a>
|
||||
<a href="https://coveralls.io/github/mengxiong10/vue2-datepicker">
|
||||
<img src="https://coveralls.io/repos/github/mengxiong10/vue2-datepicker/badge.svg?branch=master" alt="Badge">
|
||||
</a>
|
||||
<a href="LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-yellow.svg">
|
||||
</a>
|
||||
|
||||
## Demo
|
||||
<https://mengxiong10.github.io/vue2-datepicker/>
|
||||
<https://mengxiong10.github.io/vue2-datepicker/demo>
|
||||
|
||||

|
||||
|
||||
@@ -27,13 +37,19 @@ export default {
|
||||
return {
|
||||
time1: '',
|
||||
time2: '',
|
||||
time3: '',
|
||||
shortcuts: [
|
||||
{
|
||||
text: 'Today',
|
||||
start: new Date(),
|
||||
end: new Date()
|
||||
}
|
||||
]
|
||||
],
|
||||
timePickerOptions:{
|
||||
start: '00:00',
|
||||
step: '00:30',
|
||||
end: '23:30'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,50 +58,66 @@ export default {
|
||||
<template>
|
||||
<div>
|
||||
<date-picker v-model="time1" :first-day-of-week="1"></date-picker>
|
||||
<date-picker v-model="time2" range :shortcuts="shortcuts"></date-picker>
|
||||
<date-picker v-model="time2" type="datetime" :time-picker-options="timePickerOptions"></date-picker>
|
||||
<date-picker v-model="time3" range :shortcuts="shortcuts"></date-picker>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
### Attributes
|
||||
### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|---------------------|---------------|-------------|-----------------------------------------------------|
|
||||
| type | String | 'date' | select datepicker or datetimepicker(date/datetime) |
|
||||
| range | Boolean | false | if true, the type is daterange or datetimerange |
|
||||
| format | String | yyyy-MM-dd | Date formatting string |
|
||||
| custom-formatter | function | null | custom Date display |
|
||||
| format | String | YYYY-MM-DD | The parsing tokens are similar to the moment.js |
|
||||
| lang | String/Object | zh | Translation (en/zh/es/pt-br/fr/ru/de/it/cs)(custom) |
|
||||
| clearable | Boolean | true | if false, don't show the clear icon |
|
||||
| confirm | Boolean | false | if true, need click the button to change the value |
|
||||
| editable | Boolean | true | if false, user cann't type it |
|
||||
| disabled | Boolean | false | Disable the component |
|
||||
| editable | Boolean | false | if true, user can type it(only the range is false) |
|
||||
| placeholder | String | | input placeholder text |
|
||||
| width | String/Number | 210 | input size |
|
||||
| disabled-days | Array | [] | Days in YYYY-MM-DD format to disable |
|
||||
| not-before | String/Date | '' | Disable all dates before new Date(not-before) |
|
||||
| not-after | String/Date | '' | Disable all dates after new Date(not-after) |
|
||||
| disabled-days | Array/function| [] | Disable Days |
|
||||
| shortcuts | Boolean/Array | true | the shortcuts for the range picker |
|
||||
| time-picker-options | Object | {} | set timePickerOptions(start, step, end) |
|
||||
| minute-step | Number | 0 | if > 0 don't show the second picker(0 - 60) |
|
||||
| first-day-of-week | Number | 7 | set the first day of week (1-7) |
|
||||
| input-class | String | 'mx-input' | the input class name |
|
||||
| input-name | String | 'date' | the input name attr |
|
||||
| confirm-text | String | 'OK' | the default text to display on confirm button |
|
||||
| range-separator | String | '~' | the range separator text |
|
||||
|
||||
|
||||
#### lang
|
||||
* String (en/zh/es/pt-br/fr/ru/de/it/cs)
|
||||
* Object
|
||||
* Object (custom)
|
||||
|
||||
```JavaScript
|
||||
{
|
||||
days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
pickers: ['next 7 days', 'next 30 days', 'previous 7 days', 'previous 30 days'],
|
||||
placeholder: {
|
||||
date: 'Select Date',
|
||||
dateRange: 'Select Date Range'
|
||||
```html
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
lang: {
|
||||
days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
pickers: ['next 7 days', 'next 30 days', 'previous 7 days', 'previous 30 days'],
|
||||
placeholder: {
|
||||
date: 'Select Date',
|
||||
dateRange: 'Select Date Range'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<date-picker v-model="value" :lang="lang"></date-picker>
|
||||
</template>
|
||||
|
||||
```
|
||||
|
||||
#### shortcuts
|
||||
@@ -112,8 +144,10 @@ export default {
|
||||
### Events
|
||||
| Name | Description | Callback Arguments |
|
||||
|-----------------|------------------------------|------------------------|
|
||||
| change | When user select date | the currentValue |
|
||||
| change | When the value change | the currentValue |
|
||||
| input | When the value change | the currentValue |
|
||||
| confirm | When user click 'OK' button | the currentValue |
|
||||
| input-error | When user type a invalid Date| the input value |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
# vue2-datepicker
|
||||
|
||||
[English Version](https://github.com/mengxiong10/vue2-datepicker/blob/master/README_CN.md)
|
||||
|
||||
> 一个基于Vue2.x的日期时间选择组件
|
||||
|
||||
<a href="https://travis-ci.org/mengxiong10/vue2-datepicker">
|
||||
<img src="https://travis-ci.org/mengxiong10/vue2-datepicker.svg?branch=master" alt="build:passed">
|
||||
</a>
|
||||
<a href="https://coveralls.io/github/mengxiong10/vue2-datepicker">
|
||||
<img src="https://coveralls.io/repos/github/mengxiong10/vue2-datepicker/badge.svg?branch=master" alt="Badge">
|
||||
</a>
|
||||
<a href="LICENSE">
|
||||
<img src="https://img.shields.io/badge/License-MIT-yellow.svg">
|
||||
</a>
|
||||
|
||||
## 线上Demo
|
||||
<https://mengxiong10.github.io/vue2-datepicker/demo>
|
||||
|
||||

|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
$ npm install vue2-datepicker --save
|
||||
```
|
||||
|
||||
## 用法
|
||||
|
||||
```html
|
||||
<script>
|
||||
import DatePicker from 'vue2-datepicker'
|
||||
|
||||
export default {
|
||||
components: { DatePicker },
|
||||
data() {
|
||||
return {
|
||||
time1: '',
|
||||
time2: '',
|
||||
time3: '',
|
||||
shortcuts: [
|
||||
{
|
||||
text: 'Today',
|
||||
start: new Date(),
|
||||
end: new Date()
|
||||
}
|
||||
],
|
||||
timePickerOptions:{
|
||||
start: '00:00',
|
||||
step: '00:30',
|
||||
end: '23:30'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<date-picker v-model="time1" :first-day-of-week="1"></date-picker>
|
||||
<date-picker v-model="time2" type="datetime" :time-picker-options="timePickerOptions"></date-picker>
|
||||
<date-picker v-model="time3" range :shortcuts="shortcuts"></date-picker>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
### Props
|
||||
|
||||
| Prop | Type | Default | Description
|
||||
|---------------------|---------------|-------------|-----------------------------------------------------
|
||||
| type | String | 'date' | 选择日期或日期时间(可选:date,datetime)
|
||||
| range | Boolean | false | 如果是true, 显示日历范围选择
|
||||
| format | String | YYYY-MM-DD | 格式化显示日期 api类似moment.js
|
||||
| lang | String/Object | zh | 选择语言或自定义 (en/zh/es/pt-br/fr/ru/de/it/cs)(custom)
|
||||
| clearable | Boolean | true | 如果设置false, 不显示清除图标
|
||||
| confirm | Boolean | false | 如果是true, 显示确认按钮且需要确认才更新时间
|
||||
| editable | Boolean | true | 如果是false, 用户不能手动输入更新日期
|
||||
| disabled | Boolean | false | 禁用组件
|
||||
| placeholder | String | | 输入框placeholder
|
||||
| width | String/Number | 210 | 设置宽度
|
||||
| not-before | String/Date | '' | 禁止选择这个时间之前的时间
|
||||
| not-after | String/Date | '' | 禁止选择这个时间之前=后的时间
|
||||
| disabled-days | Array/function| [] | 自定义禁止的日期
|
||||
| shortcuts | Boolean/Array | true | 自定义范围选择的时候快捷选项(见下表)
|
||||
| time-picker-options | Object | {} | 自定义时间选择的开始,结束,步进(见下表)
|
||||
| minute-step | Number | 0 | 设置分钟的步进, 设置后不显示秒的选择
|
||||
| first-day-of-week | Number | 7 | 设置日历星期几开头(1-7)
|
||||
| input-class | String | 'mx-input' | 自定义输入框的类名
|
||||
| input-name | String | 'date' | 自定义input 的 name 属性
|
||||
| confirm-text | String | 'OK' | 确认按钮的名称
|
||||
| range-separator | String | '~' | range 分隔符
|
||||
|
||||
|
||||
#### shortcuts
|
||||
* true - 显示默认快捷选择
|
||||
* false - 隐藏快捷选择
|
||||
* Object[] - 自定义快捷选择, 格式:[{text, start, end}]
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
|-----------------|---------------|----------------|
|
||||
| text | String | 显示文字 |
|
||||
| start | Date | 开始日期 |
|
||||
| end | Date | 结束日期 |
|
||||
|
||||
#### time-picker-options
|
||||
* Object[] - 自定义时间选择, 格式:[{start, step, end}]
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
|-----------------|---------------|-----------------------|
|
||||
| start | String | 开始时间 (eg '00:00') |
|
||||
| step | String | 步进时间 (eg '00:30') |
|
||||
| end | String | 结束时间 (eg '23:30') |
|
||||
|
||||
|
||||
### Events
|
||||
| Name | 说明 | 回调参数 |
|
||||
|-----------------|----------------------------- |----------------|
|
||||
| change | 日期改变的时候触发 | 选择的日期 |
|
||||
| input | 日期改变的时候触发 | 选择的日期 |
|
||||
| confirm | 点击确认按钮触发的事件 | 选择的日期 |
|
||||
| input-error | 当用户输入的值无效时候触发 | 用户输入的字符串 |
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](https://github.com/mengxiong10/vue2-datepicker/blob/master/LICENSE)
|
||||
|
||||
Copyright (c) 2017-present xiemengxiong
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
# vue2-datepicker
|
||||
|
||||
[English Version](https://github.com/mengxiong10/vue2-datepicker/blob/master/README_CN.md)
|
||||
|
||||
> 一个基于Vue2.x的日期时间选择组件
|
||||
|
||||
## 线上Demo
|
||||
<https://mengxiong10.github.io/vue2-datepicker/>
|
||||
|
||||

|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
$ npm install vue2-datepicker --save
|
||||
```
|
||||
|
||||
## 用法
|
||||
|
||||
```html
|
||||
<script>
|
||||
import DatePicker from 'vue2-datepicker'
|
||||
|
||||
export default {
|
||||
components: { DatePicker },
|
||||
data() {
|
||||
return {
|
||||
time1: '',
|
||||
time2: '',
|
||||
shortcuts: [
|
||||
{
|
||||
text: 'Today',
|
||||
start: new Date(),
|
||||
end: new Date()
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<date-picker v-model="time1" :first-day-of-week="1"></date-picker>
|
||||
<date-picker v-model="time2" range :shortcuts="shortcuts"></date-picker>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
### Props
|
||||
|
||||
| 名称 | 类型 | 默认 | 说明
|
||||
|---------------------|---------------|-------------|-------------------------------------------
|
||||
| type | String | 'date' | 选择日期或日期时间(可选:date,datetime)
|
||||
| range | Boolean | false | 如果是true, 显示日历范围选择
|
||||
| confirm | Boolean | false | 如果是true, 显示确认按钮且需要确认才更新时间
|
||||
| format | String | yyyy-MM-dd | 自定义显示在输入框上的格式(yyyy-MM-dd HH:mm:ss)
|
||||
| lang | String | zh | 选择语言 (en/zh/es/pt-br/fr/ru/de/it/cs)
|
||||
| placeholder | String | | placeholder的值
|
||||
| width | String/Number | 210 | 输入框的width
|
||||
| disabled-days | Array | [] | 禁止选择的日期 (['2018-1-1'])
|
||||
| not-before | String/Date | '' | 禁止选择这个时间之前的时间
|
||||
| not-after | String/Date | '' | 禁止选择这个时间之后的时间
|
||||
| shortcuts | Boolean/Array | true | 自定义范围选择的时候快捷选项(见下表)
|
||||
| time-picker-options | Object | {} | 自定义时间选择的开始,结束,步进(见下表)
|
||||
| minute-step | Number | 0 | 分钟的步进,设置time-picker-options,这项无效
|
||||
| first-day-of-week | Number | 7 | 设置日历星期几开头(1-7)
|
||||
| input-class | String | 'mx-input' | 自定义输入框的类名
|
||||
| confirm-text | String | 'OK' | 确认按钮的名称
|
||||
| disabled | Boolean | false | 禁用组件
|
||||
| editable | Boolean | false | 如果是true, 用户可以手动输入 (仅在range === false)
|
||||
|
||||
#### shortcuts
|
||||
* true - 显示默认快捷选择
|
||||
* false - 隐藏快捷选择
|
||||
* Object[] - 自定义快捷选择, 格式:[{text, start, end}]
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
|-----------------|---------------|----------------|
|
||||
| text | String | 显示文字 |
|
||||
| start | Date | 开始日期 |
|
||||
| end | Date | 结束日期 |
|
||||
|
||||
#### time-picker-options
|
||||
* Object[] - 自定义时间选择, 格式:[{start, step, end}]
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
|-----------------|---------------|-----------------------|
|
||||
| start | String | 开始时间 (eg '00:00') |
|
||||
| step | String | 步进时间 (eg '00:30') |
|
||||
| end | String | 结束时间 (eg '23:30') |
|
||||
|
||||
|
||||
### Events
|
||||
| Name | 说明 | 回调参数 |
|
||||
|-----------------|------------------------------|-------------|
|
||||
| change | 选择的时候触发 | 选择的日期 |
|
||||
| confirm | 点击确认按钮触发的事件 | 选择的日期 |
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](https://github.com/mengxiong10/vue2-datepicker/blob/master/LICENSE)
|
||||
|
||||
Copyright (c) 2017-present xiemengxiong
|
||||
@@ -0,0 +1,48 @@
|
||||
const path = require('path')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
|
||||
module.exports = {
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
'@': path.join(__dirname, '..', 'src')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test:/\.scss$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ['babel-loader'],
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin()
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
const merge = require('webpack-merge')
|
||||
const devWebpackConfig = require('./webpack.dev.config.js')
|
||||
|
||||
const webpackConfig = merge(devWebpackConfig, {
|
||||
devtool: 'source-map',
|
||||
mode: 'production'
|
||||
externals: {
|
||||
'vue': 'Vue',
|
||||
'@/index': 'DatePicker'
|
||||
},
|
||||
})
|
||||
|
||||
module.exports = webpackConfig
|
||||
@@ -0,0 +1,50 @@
|
||||
const path = require('path')
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.base.config.js')
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
|
||||
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
mode: 'production',
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../lib'),
|
||||
filename: 'index.js',
|
||||
library: "DatePicker",
|
||||
libraryTarget: "umd"
|
||||
},
|
||||
plugins: [
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: { safe: true }
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
const webpackConfigExtractCss = merge(baseWebpackConfig, {
|
||||
mode: 'production',
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../lib'),
|
||||
filename: 'datepicker.js',
|
||||
library: "DatePicker",
|
||||
libraryTarget: "umd"
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test:/\.scss$/,
|
||||
use: [ MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader']
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'datepicker.css'
|
||||
}),
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: { safe: true }
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
module.exports = [ webpackConfig, webpackConfigExtractCss]
|
||||
@@ -0,0 +1,30 @@
|
||||
const path = require('path')
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.base.config.js')
|
||||
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
mode: 'development',
|
||||
entry: './demo/index.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../demo'),
|
||||
filename: 'build.js'
|
||||
},
|
||||
externals: {
|
||||
'vue': 'Vue'
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: {
|
||||
index: './demo/index.html'
|
||||
},
|
||||
noInfo: true,
|
||||
port: 9000
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
devtool: 'cheap-module-eval-source-map'
|
||||
})
|
||||
|
||||
module.exports = webpackConfig
|
||||
@@ -1,676 +0,0 @@
|
||||
<template>
|
||||
<div class="mx-calendar">
|
||||
<div class="mx-calendar-header" v-if="currentPanel === 'time'">
|
||||
<a @click="currentPanel = 'date'">{{now.toLocaleDateString()}}</a>
|
||||
</div>
|
||||
<div class="mx-calendar-header" v-else>
|
||||
<a class="mx-calendar__prev-icon" @click="changeYear(-1)">«</a>
|
||||
<a v-show="currentPanel === 'date'" class="mx-calendar__prev-icon" @click="changeMonth(-1)">‹</a>
|
||||
<a class="mx-calendar__next-icon" @click="changeYear(1)">»</a>
|
||||
<a v-show="currentPanel === 'date'" class="mx-calendar__next-icon" @click="changeMonth(1)">›</a>
|
||||
<a @click="showMonths">{{months[currentMonth]}}</a>
|
||||
<a @click="showYears">{{currentYear}}</a>
|
||||
</div>
|
||||
<div class="mx-calendar-content">
|
||||
<table class="mx-calendar-table" v-show="currentPanel === 'date'">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(day, index) in days" :key="index">{{day}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in dates">
|
||||
<td v-for="cell in row" :title="cell.title" :class="getDateClasses(cell)" @click="selectDate(cell)">{{cell.day}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mx-calendar-year" v-show="currentPanel === 'years'">
|
||||
<a v-for="year in years"
|
||||
@click="selectYear(year)"
|
||||
:class="{'current': currentYear === year, 'disabled': isDisabledYear(year)}">{{year}}</a>
|
||||
</div>
|
||||
<div class="mx-calendar-month" v-show="currentPanel === 'months'">
|
||||
<a v-for="(month, index) in months"
|
||||
@click="selectMonth(index)"
|
||||
:class="{'current': currentMonth === index, 'disabled': isDisabledMonth(index)}">{{month}}</a>
|
||||
</div>
|
||||
<div class="mx-calendar-time"
|
||||
v-show="currentPanel === 'time'" >
|
||||
<div v-if="timeSelectOptions.length" class="mx-time-list-wrapper">
|
||||
<ul class="mx-time-list">
|
||||
<li class="mx-time-item mx-time-picker-item"
|
||||
:class="getTimeClasses(item.value.hours * 60 + item.value.minutes, -1)"
|
||||
@click="pickTime(item.value)"
|
||||
v-for="item in timeSelectOptions">
|
||||
{{item.label}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else class="mx-time-list-wrapper"
|
||||
:style="{width: 100 / times.length + '%' }"
|
||||
v-for="(time, index) in times"
|
||||
:key="index">
|
||||
<ul class="mx-time-list">
|
||||
<li class="mx-time-item"
|
||||
v-for="num in time"
|
||||
:class="getTimeClasses(num, index)"
|
||||
:key="num"
|
||||
@click="selectTime(num, index)"
|
||||
>{{num | timeText}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const getTimeArray = function (len, step = 1) {
|
||||
const length = parseInt(len / step)
|
||||
return Array.apply(null, { length }).map((v, i) => i * step)
|
||||
}
|
||||
|
||||
const parseTime = function(time) {
|
||||
const values = (time || '').split(':');
|
||||
if (values.length >= 2) {
|
||||
const hours = parseInt(values[0], 10);
|
||||
const minutes = parseInt(values[1], 10);
|
||||
return {
|
||||
hours,
|
||||
minutes
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatTime = function(time, type="24") {
|
||||
let hours = time.hours
|
||||
hours = (type === '24') ? hours : (hours % 12 || 12)
|
||||
hours = hours < 10 ? '0' + hours : hours
|
||||
let minutes = time.minutes < 10 ? '0' + time.minutes : time.minutes
|
||||
let result = hours + ':' + minutes
|
||||
if (type === '12') {
|
||||
result += time.hours >= 12 ? ' pm' : ' am'
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
startAt: null,
|
||||
endAt: null,
|
||||
value: null,
|
||||
show: Boolean
|
||||
},
|
||||
data() {
|
||||
const translation = this.$parent.translation
|
||||
const minuteStep = this.$parent.minuteStep
|
||||
const times = [getTimeArray(24, 1), getTimeArray(60, minuteStep || 1)]
|
||||
if (minuteStep === 0) {
|
||||
times.push(getTimeArray(60, 1))
|
||||
}
|
||||
return {
|
||||
months: translation.months,
|
||||
dates: [], // 日历面板
|
||||
years: [], // 年代面板
|
||||
now: new Date(), // calendar-header 显示的时间, 用于切换日历
|
||||
currentPanel: 'date',
|
||||
times: times
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 日历显示头
|
||||
days() {
|
||||
const days = this.$parent.translation.days
|
||||
const firstday = +this.$parent.firstDayOfWeek
|
||||
return days.concat(days).slice(firstday, firstday + 7)
|
||||
},
|
||||
timeType () {
|
||||
return /h+/.test(this.$parent.format) ? '12' : '24'
|
||||
},
|
||||
timeSelectOptions () {
|
||||
const result = []
|
||||
const options = this.$parent.timePickerOptions
|
||||
if (!options) {
|
||||
return []
|
||||
}
|
||||
if (typeof options === 'function') {
|
||||
return options() || []
|
||||
}
|
||||
const start = parseTime(options.start)
|
||||
const end = parseTime(options.end)
|
||||
const step = parseTime(options.step)
|
||||
if (start && end && step) {
|
||||
const startMinutes = start.minutes + start.hours * 60;
|
||||
const endMinutes = end.minutes + end.hours * 60;
|
||||
const stepMinutes = step.minutes + step.hours * 60
|
||||
const len = Math.floor((endMinutes - startMinutes) / stepMinutes)
|
||||
for (let i = 0; i <= len; i++) {
|
||||
let timeMinutes = startMinutes + i * stepMinutes
|
||||
let hours = Math.floor(timeMinutes/60)
|
||||
let minutes = timeMinutes % 60
|
||||
let value = {
|
||||
hours, minutes
|
||||
}
|
||||
result.push({
|
||||
value,
|
||||
label: formatTime(value, this.timeType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
currentYear() {
|
||||
return this.now.getFullYear()
|
||||
},
|
||||
currentMonth() {
|
||||
return this.now.getMonth()
|
||||
},
|
||||
curHour() {
|
||||
return this.now.getHours()
|
||||
},
|
||||
curMinute() {
|
||||
return this.now.getMinutes()
|
||||
},
|
||||
curSecond() {
|
||||
return this.now.getSeconds()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateCalendar()
|
||||
},
|
||||
watch: {
|
||||
show(val) {
|
||||
if (val) {
|
||||
this.currentPanel = 'date'
|
||||
this.updateNow()
|
||||
}
|
||||
},
|
||||
value: {
|
||||
handler: 'updateNow',
|
||||
immediate: true
|
||||
},
|
||||
now: 'updateCalendar'
|
||||
},
|
||||
filters: {
|
||||
timeText(value) {
|
||||
return ('00' + value).slice(String(value).length)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateNow() {
|
||||
this.now = this.value ? new Date(this.value) : new Date()
|
||||
},
|
||||
// 更新面板选择时间
|
||||
updateCalendar() {
|
||||
function getCalendar(time, firstday, length, classes) {
|
||||
return Array.apply(null, { length }).map((v, i) => {
|
||||
// eslint-disable-line
|
||||
let day = firstday + i
|
||||
const date = new Date(
|
||||
time.getFullYear(),
|
||||
time.getMonth(),
|
||||
day,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
date.setDate(day)
|
||||
return {
|
||||
title: date.toLocaleDateString(),
|
||||
date,
|
||||
day,
|
||||
classes
|
||||
}
|
||||
})
|
||||
}
|
||||
const firstDayOfWeek = this.$parent.firstDayOfWeek
|
||||
const time = new Date(this.now)
|
||||
time.setDate(0) // 把时间切换到上个月最后一天
|
||||
const lastMonthLength = (time.getDay() + 7 - firstDayOfWeek) % 7 + 1 // time.getDay() 0是星期天, 1是星期一 ...
|
||||
const lastMonthfirst = time.getDate() - (lastMonthLength - 1)
|
||||
const lastMonth = getCalendar(
|
||||
time,
|
||||
lastMonthfirst,
|
||||
lastMonthLength,
|
||||
'lastMonth'
|
||||
)
|
||||
|
||||
time.setMonth(time.getMonth() + 2, 0) // 切换到这个月最后一天
|
||||
const curMonthLength = time.getDate()
|
||||
const curMonth = getCalendar(time, 1, curMonthLength, 'curMonth')
|
||||
|
||||
time.setMonth(time.getMonth() + 1, 1)
|
||||
const nextMonthLength = 42 - (lastMonthLength + curMonthLength)
|
||||
const nextMonth = getCalendar(time, 1, nextMonthLength, 'nextMonth')
|
||||
|
||||
// 分割数组
|
||||
let index = 0
|
||||
let resIndex = 0
|
||||
const arr = lastMonth.concat(curMonth, nextMonth)
|
||||
const result = new Array(6)
|
||||
while (index < 42) {
|
||||
result[resIndex++] = arr.slice(index, (index += 7))
|
||||
}
|
||||
this.dates = result
|
||||
},
|
||||
isDisabled (date) {
|
||||
const now = new Date(date).getTime()
|
||||
if (
|
||||
this.$parent.disabledDays.some(v => new Date(v).setHours(0, 0, 0, 0) === now) ||
|
||||
this.$parent.notBefore !== '' && now < new Date(this.$parent.notBefore).setHours(0, 0, 0, 0) ||
|
||||
this.$parent.notAfter !== '' && now > new Date(this.$parent.notAfter).setHours(0, 0, 0, 0) ||
|
||||
this.startAt && now < new Date(this.startAt).setHours(0, 0, 0, 0) ||
|
||||
this.endAt && now > new Date(this.endAt).setHours(0, 0, 0, 0)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
getDateClasses(cell) {
|
||||
const classes = []
|
||||
const cellTime = new Date(cell.date).setHours(0, 0, 0, 0)
|
||||
const cellEndTime = new Date(cell.date).setHours(23, 59, 59, 999)
|
||||
const curTime = this.value ? new Date(this.value).setHours(0, 0, 0, 0) : 0
|
||||
const startTime = this.startAt
|
||||
? new Date(this.startAt).setHours(0, 0, 0, 0)
|
||||
: 0
|
||||
const endTime = this.endAt ? new Date(this.endAt).setHours(0, 0, 0, 0) : 0
|
||||
const today = new Date().setHours(0, 0, 0, 0)
|
||||
if (this.isDisabled(cellTime)) {
|
||||
return 'disabled'
|
||||
}
|
||||
classes.push(cell.classes)
|
||||
|
||||
if (cellTime === today) {
|
||||
classes.push('today')
|
||||
}
|
||||
|
||||
// range classes
|
||||
if (curTime) {
|
||||
if (cellTime === curTime) {
|
||||
classes.push('current')
|
||||
} else if (startTime && cellTime <= curTime) {
|
||||
classes.push('inrange')
|
||||
} else if (endTime && cellTime >= curTime) {
|
||||
classes.push('inrange')
|
||||
}
|
||||
}
|
||||
return classes.join(' ')
|
||||
},
|
||||
getTimeClasses(value, index) {
|
||||
let curValue
|
||||
let cellTime
|
||||
const startTime = this.startAt ? new Date(this.startAt) : 0
|
||||
const endTime = this.endAt ? new Date(this.endAt) : 0
|
||||
const classes = []
|
||||
switch (index) {
|
||||
case -1:
|
||||
curValue = this.curHour * 60 + this.curMinute
|
||||
cellTime = new Date(this.now).setHours(Math.floor(value / 60), value % 60, 0)
|
||||
break
|
||||
case 0:
|
||||
curValue = this.curHour
|
||||
cellTime = new Date(this.now).setHours(value)
|
||||
break
|
||||
case 1:
|
||||
curValue = this.curMinute
|
||||
cellTime = new Date(this.now).setMinutes(value)
|
||||
break
|
||||
case 2:
|
||||
curValue = this.curSecond
|
||||
cellTime = new Date(this.now).setSeconds(value)
|
||||
break
|
||||
}
|
||||
if (
|
||||
(this.$parent.notBefore !== '' &&
|
||||
cellTime < new Date(this.$parent.notBefore).getTime()) ||
|
||||
(this.$parent.notAfter !== '' &&
|
||||
cellTime > new Date(this.$parent.notAfter).getTime())
|
||||
) {
|
||||
return 'disabled'
|
||||
}
|
||||
|
||||
if (value === curValue) {
|
||||
classes.push('cur-time')
|
||||
} else if (startTime) {
|
||||
if (cellTime < startTime) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
} else if (endTime) {
|
||||
if (cellTime > endTime) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
}
|
||||
return classes.join(' ')
|
||||
},
|
||||
showMonths() {
|
||||
if (this.currentPanel === 'months') {
|
||||
this.currentPanel = 'date'
|
||||
} else {
|
||||
this.currentPanel = 'months'
|
||||
}
|
||||
},
|
||||
showYears() {
|
||||
// 当前年代
|
||||
if (this.currentPanel === 'years') {
|
||||
this.currentPanel = 'date'
|
||||
} else {
|
||||
let firstYear = Math.floor(this.now.getFullYear() / 10) * 10
|
||||
let years = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
years.push(firstYear + i)
|
||||
}
|
||||
this.years = years
|
||||
this.currentPanel = 'years'
|
||||
}
|
||||
},
|
||||
// 前进或后退一年
|
||||
changeYear(flag) {
|
||||
if (this.currentPanel === 'years') {
|
||||
this.years = this.years.map(v => v + flag * 10)
|
||||
} else {
|
||||
const now = new Date(this.now)
|
||||
now.setFullYear(now.getFullYear() + flag, now.getMonth(), 1)
|
||||
this.now = now
|
||||
}
|
||||
},
|
||||
changeMonth(flag) {
|
||||
const now = new Date(this.now)
|
||||
now.setMonth(now.getMonth() + flag, 1)
|
||||
this.now = now
|
||||
},
|
||||
scrollIntoView(container, selected) {
|
||||
if (!selected) {
|
||||
container.scrollTop = 0
|
||||
return
|
||||
}
|
||||
const top = selected.offsetTop
|
||||
const bottom = selected.offsetTop + selected.offsetHeight
|
||||
const viewRectTop = container.scrollTop
|
||||
const viewRectBottom = viewRectTop + container.clientHeight
|
||||
if (top < viewRectTop) {
|
||||
container.scrollTop = top
|
||||
} else if (bottom > viewRectBottom) {
|
||||
container.scrollTop = bottom - container.clientHeight
|
||||
}
|
||||
},
|
||||
selectDate(cell) {
|
||||
const classes = this.getDateClasses(cell)
|
||||
if (classes.indexOf('disabled') !== -1) {
|
||||
return
|
||||
}
|
||||
let date = new Date(cell.date)
|
||||
// datetime 跳转到 timepicker
|
||||
if (this.$parent.type === 'datetime') {
|
||||
// 保留时分秒
|
||||
if (this.value instanceof Date) {
|
||||
date.setHours(
|
||||
this.value.getHours(),
|
||||
this.value.getMinutes(),
|
||||
this.value.getSeconds()
|
||||
)
|
||||
}
|
||||
if (this.startAt && date.getTime() < new Date(this.startAt).getTime()) {
|
||||
date = new Date(this.startAt)
|
||||
} else if (
|
||||
this.endAt &&
|
||||
date.getTime() > new Date(this.endAt).getTime()
|
||||
) {
|
||||
date = new Date(this.endAt)
|
||||
}
|
||||
this.currentPanel = 'time'
|
||||
this.$nextTick(() => {
|
||||
Array.prototype.forEach.call(
|
||||
this.$el.querySelectorAll('.mx-time-list-wrapper'),
|
||||
(el) => {
|
||||
this.scrollIntoView(el, el.querySelector('.cur-time'))
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
this.now = date
|
||||
this.$emit('input', date)
|
||||
this.$emit('select')
|
||||
},
|
||||
isDisabledYear (year) {
|
||||
if (this.value) {
|
||||
const now = new Date(this.now).setFullYear(year)
|
||||
return this.isDisabled(now)
|
||||
}
|
||||
return false
|
||||
},
|
||||
isDisabledMonth (month) {
|
||||
if (this.value) {
|
||||
const now = new Date(this.now).setMonth(month)
|
||||
return this.isDisabled(now)
|
||||
}
|
||||
return false
|
||||
},
|
||||
selectYear(year) {
|
||||
if (this.isDisabledYear(year)) {
|
||||
return
|
||||
}
|
||||
const now = new Date(this.now)
|
||||
now.setFullYear(year)
|
||||
this.now = now
|
||||
if (this.value) {
|
||||
this.$emit('input', now)
|
||||
this.$emit('select', true)
|
||||
}
|
||||
this.currentPanel = 'months'
|
||||
},
|
||||
selectMonth(month) {
|
||||
if (this.isDisabledMonth(month)) {
|
||||
return
|
||||
}
|
||||
const now = new Date(this.now)
|
||||
now.setMonth(month)
|
||||
this.now = now
|
||||
if (this.value) {
|
||||
this.$emit('input', now)
|
||||
this.$emit('select', true)
|
||||
}
|
||||
this.currentPanel = 'date'
|
||||
},
|
||||
selectTime(value, index) {
|
||||
const classes = this.getTimeClasses(value, index)
|
||||
if (classes.indexOf('disabled') !== -1) {
|
||||
return
|
||||
}
|
||||
const date = new Date(this.now)
|
||||
if (index === 0) {
|
||||
date.setHours(value)
|
||||
} else if (index === 1) {
|
||||
date.setMinutes(value)
|
||||
} else if (index === 2) {
|
||||
date.setSeconds(value)
|
||||
}
|
||||
this.now = date
|
||||
this.$emit('input', date)
|
||||
this.$emit('select')
|
||||
},
|
||||
pickTime (value) {
|
||||
const classes = this.getTimeClasses(value.hours * 60 + value.minutes, -1)
|
||||
if (classes.indexOf('disabled') !== -1) {
|
||||
return
|
||||
}
|
||||
const date = new Date(this.now)
|
||||
date.setHours(value.hours, value.minutes, 0)
|
||||
this.now = date
|
||||
this.$emit('input', date)
|
||||
this.$emit('select')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.mx-calendar {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.mx-calendar-header {
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
& > a:hover {
|
||||
color: #1284e7;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-calendar__next-icon,
|
||||
.mx-calendar__prev-icon {
|
||||
font-size: 20px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.mx-calendar__prev-icon {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.mx-calendar__next-icon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.mx-calendar-content {
|
||||
height: 224px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx-calendar-table {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
td {
|
||||
cursor: pointer;
|
||||
}
|
||||
.today {
|
||||
color: #20a0ff;
|
||||
}
|
||||
.lastMonth,
|
||||
.nextMonth {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-calendar-table td,
|
||||
.mx-calendar-table th {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx-calendar-table td {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx-calendar-table td.inrange,
|
||||
.mx-calendar-table td:hover,
|
||||
.mx-calendar-year > a:hover,
|
||||
.mx-calendar-month > a:hover {
|
||||
background-color: #eaf8fe;
|
||||
}
|
||||
|
||||
.mx-calendar-table td.current,
|
||||
.mx-calendar-year > a.current,
|
||||
.mx-calendar-month > a.current {
|
||||
color: #fff;
|
||||
background-color: #1284e7;
|
||||
}
|
||||
|
||||
.mx-calendar-table td.disabled,
|
||||
.mx-time-item.disabled,
|
||||
.mx-calendar-year a.disabled,
|
||||
.mx-calendar-month a.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #ccc;
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.mx-calendar-year,
|
||||
.mx-calendar-month,
|
||||
.mx-calendar-time {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 7px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx-calendar-year > a {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
margin: 1px 5%;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.mx-calendar-month > a {
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
line-height: 40px;
|
||||
margin: 8px 1.5%;
|
||||
}
|
||||
|
||||
.mx-time-list-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mx-time-list-wrapper::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
.mx-time-list-wrapper::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 10px;
|
||||
box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mx-time-list-wrapper:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.mx-time-list-wrapper:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
.mx-time-picker-item {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.mx-time-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.mx-time-item {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mx-time-item:hover {
|
||||
background-color: #eaf8fe;
|
||||
}
|
||||
.mx-time-item.cur-time {
|
||||
color: #fff;
|
||||
background-color: #1284e7;
|
||||
}
|
||||
</style>
|
||||
@@ -1,583 +0,0 @@
|
||||
<template>
|
||||
<div class="mx-datepicker"
|
||||
:class="{'disabled': disabled}"
|
||||
:style="{'width': computedWidth,'min-width':range ? (type === 'datetime' ? '320px' : '210px') : '140px'}"
|
||||
v-clickoutside="closePopup">
|
||||
<input :name="inputName"
|
||||
:disabled="disabled"
|
||||
:class="inputClass"
|
||||
:value="text"
|
||||
:readonly="!editable || range"
|
||||
:placeholder="innerPlaceholder"
|
||||
ref="input"
|
||||
@mouseenter="hoverIcon"
|
||||
@mouseleave="hoverIcon"
|
||||
@click="togglePopup"
|
||||
@input="handleInput"
|
||||
@change="handleChange"
|
||||
@mousedown="$event.preventDefault()">
|
||||
<i class="mx-input-icon"
|
||||
:class="showCloseIcon ? 'mx-input-icon__close' : 'mx-input-icon__calendar'"
|
||||
@mouseenter="hoverIcon"
|
||||
@mouseleave="hoverIcon"
|
||||
@click="clickIcon"></i>
|
||||
<div class="mx-datepicker-popup"
|
||||
:class="{'range':range}"
|
||||
:style="position"
|
||||
ref="calendar"
|
||||
v-show="showPopup">
|
||||
<calendar-panel v-if="!range"
|
||||
v-model="currentValue"
|
||||
@select="selectDate"
|
||||
:show="showPopup"></calendar-panel>
|
||||
<div v-else
|
||||
style="overflow:hidden">
|
||||
<div class="mx-datepicker-top"
|
||||
v-if="ranges.length">
|
||||
<span v-for="range in ranges"
|
||||
@click="selectRange(range)">{{range.text}}</span>
|
||||
</div>
|
||||
<calendar-panel style="width:50%;box-shadow:1px 0 rgba(0, 0, 0, .1)"
|
||||
v-model="currentValue[0]"
|
||||
:end-at="currentValue[1]"
|
||||
@select="selectDate"
|
||||
:show="showPopup"></calendar-panel>
|
||||
<calendar-panel style="width:50%;"
|
||||
v-model="currentValue[1]"
|
||||
:start-at="currentValue[0]"
|
||||
@select="selectDate"
|
||||
:show="showPopup"></calendar-panel>
|
||||
</div>
|
||||
<div class="mx-datepicker-footer"
|
||||
v-if="confirm">
|
||||
<button type="button"
|
||||
class="mx-datepicker-btn mx-datepicker-btn-confirm"
|
||||
@click="confirmDate"> {{ confirmText }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CalendarPanel from './calendar-panel.vue'
|
||||
import Languages from './languages.js'
|
||||
|
||||
const isObject = function (obj) {
|
||||
return obj !== null && typeof obj === 'object'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'DatePicker',
|
||||
components: { CalendarPanel },
|
||||
props: {
|
||||
value: null,
|
||||
format: {
|
||||
type: String,
|
||||
default: 'yyyy-MM-dd'
|
||||
},
|
||||
customFormatter: {
|
||||
type: Function
|
||||
},
|
||||
range: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'date' // ['date', 'datetime']
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 210
|
||||
},
|
||||
placeholder: String,
|
||||
lang: {
|
||||
type: [String, Object],
|
||||
default: 'zh'
|
||||
},
|
||||
shortcuts: {
|
||||
type: [Boolean, Array],
|
||||
default: true
|
||||
},
|
||||
disabledDays: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
notBefore: {
|
||||
default: ''
|
||||
},
|
||||
notAfter: {
|
||||
default: ''
|
||||
},
|
||||
firstDayOfWeek: {
|
||||
default: 7,
|
||||
type: Number,
|
||||
validator: val => val >= 1 && val <= 7
|
||||
},
|
||||
minuteStep: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
validator: val => val >= 0 && val <= 60
|
||||
},
|
||||
timePickerOptions: {
|
||||
type: [Object, Function],
|
||||
default () {
|
||||
return null
|
||||
}
|
||||
},
|
||||
confirm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inputClass: {
|
||||
type: String,
|
||||
default: 'mx-input'
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: 'OK'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rangeSeparator: {
|
||||
type: String,
|
||||
default: '~'
|
||||
},
|
||||
inputName:{
|
||||
type: String,
|
||||
default: 'date'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showPopup: false,
|
||||
showCloseIcon: false,
|
||||
currentValue: this.value,
|
||||
position: null,
|
||||
userInput: null,
|
||||
ranges: [] // 快捷选项
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler (val) {
|
||||
if (!this.range) {
|
||||
this.currentValue = this.isValidDate(val) ? val : undefined
|
||||
} else {
|
||||
this.currentValue = this.isValidRange(val)
|
||||
? val.slice(0, 2)
|
||||
: [undefined, undefined]
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
showPopup (val) {
|
||||
if (val) {
|
||||
this.$nextTick(this.displayPopup)
|
||||
} else {
|
||||
this.userInput = null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
translation () {
|
||||
if (isObject(this.lang)) {
|
||||
return { ...Languages['en'], ...this.lang }
|
||||
}
|
||||
return Languages[this.lang] || Languages['en']
|
||||
},
|
||||
innerPlaceholder () {
|
||||
return (
|
||||
this.placeholder ||
|
||||
(this.range
|
||||
? this.translation.placeholder.dateRange
|
||||
: this.translation.placeholder.date)
|
||||
)
|
||||
},
|
||||
text () {
|
||||
if (!this.range && this.isValidDate(this.value)) {
|
||||
return this.userInput !== null ? this.userInput : this.stringify(this.value)
|
||||
}
|
||||
if (this.range && this.isValidRange(this.value)) {
|
||||
return (
|
||||
this.stringify(this.value[0]) + ` ${this.rangeSeparator} ` + this.stringify(this.value[1])
|
||||
)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
computedWidth () {
|
||||
if ((typeof this.width === 'string' && /^\d+$/.test(this.width)) || typeof this.width === 'number') {
|
||||
return this.width + 'px'
|
||||
}
|
||||
return this.width
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInput (event) {
|
||||
this.userInput = event.target.value
|
||||
},
|
||||
handleChange (event) {
|
||||
const value = event.target.value
|
||||
const date = this.parseDate(value, this.format)
|
||||
if (date && this.editable && !this.range) {
|
||||
if (this.notBefore && date < new Date(this.notBefore)) {
|
||||
return
|
||||
}
|
||||
if (this.notAfter && date > new Date(this.notAfter)) {
|
||||
return
|
||||
}
|
||||
for (let i = 0, len = this.disabledDays.length; i < len; i++) {
|
||||
if (date.getTime() === new Date(this.disabledDays[i]).getTime()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$emit('input', date)
|
||||
this.$emit('change', date)
|
||||
this.closePopup()
|
||||
}
|
||||
},
|
||||
updateDate () {
|
||||
const val = this.currentValue
|
||||
if ((!this.range && val) || (this.range && val[0] && val[1])) {
|
||||
this.$emit('input', val)
|
||||
this.$emit('change', val)
|
||||
}
|
||||
},
|
||||
confirmDate () {
|
||||
this.updateDate()
|
||||
this.closePopup()
|
||||
this.$emit('confirm', this.currentValue)
|
||||
},
|
||||
selectDate (show = false) {
|
||||
if (!this.confirm && !this.disabled) {
|
||||
this.updateDate()
|
||||
if (!show && this.type === 'date' && !this.range) {
|
||||
this.closePopup()
|
||||
}
|
||||
}
|
||||
},
|
||||
closePopup () {
|
||||
this.showPopup = false
|
||||
},
|
||||
togglePopup () {
|
||||
if (this.showPopup) {
|
||||
this.$refs.input.blur()
|
||||
this.showPopup = false
|
||||
} else {
|
||||
this.$refs.input.focus()
|
||||
this.showPopup = true
|
||||
}
|
||||
},
|
||||
hoverIcon (e) {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
if (e.type === 'mouseenter' && this.text) {
|
||||
this.showCloseIcon = true
|
||||
}
|
||||
if (e.type === 'mouseleave') {
|
||||
this.showCloseIcon = false
|
||||
}
|
||||
},
|
||||
clickIcon () {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
if (this.showCloseIcon) {
|
||||
this.$emit('input', '')
|
||||
this.$emit('change', '')
|
||||
} else {
|
||||
this.togglePopup()
|
||||
}
|
||||
},
|
||||
parseDate (str, fmt = 'yyyy-MM-dd') {
|
||||
let isValid = true
|
||||
const obj = { y: 0, M: 1, d: 0, H: 0, h: 0, m: 0, s: 0 }
|
||||
fmt.replace(/([^yMdHhms]*?)(([yMdHhms])\3*)([^yMdHhms]*?)/g, function (m, $1, $2, $3, $4, idx, old) {
|
||||
const rgs = new RegExp($1 + '(\\d{' + $2.length + '})' + $4)
|
||||
const index = str.search(rgs)
|
||||
if (index === -1) {
|
||||
isValid = false
|
||||
} else {
|
||||
str = str.replace(rgs, function (_m, _$1) {
|
||||
obj[$3] = parseInt(_$1);
|
||||
return ''
|
||||
});
|
||||
}
|
||||
return ''
|
||||
});
|
||||
if (!isValid) {
|
||||
return false
|
||||
}
|
||||
obj.M--
|
||||
return new Date(obj.y, obj.M, obj.d, obj.H || obj.h, obj.m, obj.s)
|
||||
},
|
||||
formatDate (date, fmt = 'yyyy-MM-dd HH:mm:ss') {
|
||||
const hour = date.getHours()
|
||||
const map = {
|
||||
'M+': date.getMonth() + 1, // 月份
|
||||
'[Dd]+': date.getDate(), // 日
|
||||
'H+': hour, // 小时
|
||||
'h+': (hour % 12) || 12, // 小时
|
||||
'm+': date.getMinutes(), // 分
|
||||
's+': date.getSeconds(), // 秒
|
||||
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
|
||||
S: date.getMilliseconds(), // 毫秒
|
||||
'a': hour >= 12 ? 'pm' : 'am',
|
||||
'A': hour >= 12 ? 'PM' : 'AM'
|
||||
}
|
||||
let str = fmt.replace(/[Yy]+/g, function (str) {
|
||||
return ('' + date.getFullYear()).slice(4 - str.length)
|
||||
})
|
||||
Object.keys(map).forEach(key => {
|
||||
str = str.replace(new RegExp(key), function (str) {
|
||||
const value = '' + map[key]
|
||||
return str.length === 1 ? value : ('00' + value).slice(value.length)
|
||||
})
|
||||
})
|
||||
return str
|
||||
},
|
||||
stringify (date) {
|
||||
if (typeof this.customFormatter === 'function') {
|
||||
return this.customFormatter(new Date(date))
|
||||
}
|
||||
return this.formatDate(new Date(date), this.format)
|
||||
},
|
||||
isValidDate (date) {
|
||||
return !!new Date(date).getTime()
|
||||
},
|
||||
isValidRange (date) {
|
||||
return (
|
||||
Array.isArray(date) &&
|
||||
date.length === 2 &&
|
||||
this.isValidDate(date[0]) &&
|
||||
this.isValidDate(date[1])
|
||||
)
|
||||
},
|
||||
selectRange (range) {
|
||||
this.$emit('input', [range.start, range.end])
|
||||
this.$emit('change', [range.start, range.end])
|
||||
},
|
||||
initRanges () {
|
||||
if (Array.isArray(this.shortcuts)) {
|
||||
this.ranges = this.shortcuts
|
||||
} else if (this.shortcuts) {
|
||||
this.ranges = [
|
||||
{
|
||||
text: '未来7天',
|
||||
start: new Date(),
|
||||
end: new Date(Date.now() + 3600 * 1000 * 24 * 7)
|
||||
},
|
||||
{
|
||||
text: '未来30天',
|
||||
start: new Date(),
|
||||
end: new Date(Date.now() + 3600 * 1000 * 24 * 30)
|
||||
},
|
||||
{
|
||||
text: '最近7天',
|
||||
start: new Date(Date.now() - 3600 * 1000 * 24 * 7),
|
||||
end: new Date()
|
||||
},
|
||||
{
|
||||
text: '最近30天',
|
||||
start: new Date(Date.now() - 3600 * 1000 * 24 * 30),
|
||||
end: new Date()
|
||||
}
|
||||
]
|
||||
this.ranges.forEach((v, i) => {
|
||||
v.text = this.translation.pickers[i]
|
||||
})
|
||||
} else {
|
||||
this.ranges = []
|
||||
}
|
||||
},
|
||||
displayPopup () {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
const dw = document.documentElement.clientWidth
|
||||
const dh = document.documentElement.clientHeight
|
||||
const InputRect = this.$el.getBoundingClientRect()
|
||||
const PopupRect = this.$refs.calendar.getBoundingClientRect()
|
||||
this.position = {}
|
||||
if (
|
||||
dw - InputRect.left < PopupRect.width &&
|
||||
InputRect.right < PopupRect.width
|
||||
) {
|
||||
this.position.left = 1 - InputRect.left + 'px'
|
||||
} else if (InputRect.left + InputRect.width / 2 <= dw / 2) {
|
||||
this.position.left = 0
|
||||
} else {
|
||||
this.position.right = 0
|
||||
}
|
||||
if (
|
||||
InputRect.top <= PopupRect.height + 1 &&
|
||||
dh - InputRect.bottom <= PopupRect.height + 1
|
||||
) {
|
||||
this.position.top = dh - InputRect.top - PopupRect.height - 1 + 'px'
|
||||
} else if (InputRect.top + InputRect.height / 2 <= dh / 2) {
|
||||
this.position.top = '100%'
|
||||
} else {
|
||||
this.position.bottom = '100%'
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.initRanges()
|
||||
},
|
||||
directives: {
|
||||
clickoutside: {
|
||||
bind (el, binding, vnode) {
|
||||
el['@clickoutside'] = e => {
|
||||
if (
|
||||
!el.contains(e.target) &&
|
||||
binding.expression &&
|
||||
vnode.context[binding.expression]
|
||||
) {
|
||||
binding.value()
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', el['@clickoutside'], true)
|
||||
},
|
||||
unbind (el) {
|
||||
document.removeEventListener('click', el['@clickoutside'], true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.mx-datepicker {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: #73879c;
|
||||
font: 14px/1.5 'Helvetica Neue', Helvetica, Arial, 'Microsoft Yahei',
|
||||
sans-serif;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.mx-datepicker-popup {
|
||||
position: absolute;
|
||||
width: 250px;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
z-index: 1000;
|
||||
&.range {
|
||||
width: 496px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-input {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 30px 6px 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-input-icon {
|
||||
top: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-input-icon__calendar {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA00lEQVQ4T72SzQ2CQBCF54UGKIES6EAswQq0BS/A3PQ0hAt0oKVQgiVYAkcuZMwSMOyCyRKNe9uf+d6b2Qf6csGtL8sy7vu+Zebn/E5EoiAIwjRNH/PzBUBEGiJqmPniAMw+YeZkFSAiJwA3j45aVT0wsxGitwOjDGDnASBVvU4OLQARRURk9e4CAcSqWn8CLHp3Ae6MXAe/B4yzUeMkz/P9ZgdFUQzFIwD/B4yKgwMTos0OtvzCHcDRJ0gAzlmW1VYSq6oKu66LfQBTjC2AT+Hamxcml5IRpPq3VQAAAABJRU5ErkJggg==);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.mx-input-icon__close::before {
|
||||
content: '\2716';
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx-datepicker-top {
|
||||
text-align: left;
|
||||
padding: 0 12px;
|
||||
line-height: 34px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
& > span {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #1284e7;
|
||||
}
|
||||
&:after {
|
||||
content: '|';
|
||||
margin: 0 10px;
|
||||
color: #48576a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker-footer {
|
||||
padding: 4px;
|
||||
clear: both;
|
||||
text-align: right;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.mx-datepicker-btn {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
padding: 7px 15px;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.mx-datepicker-btn-confirm {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: #73879c;
|
||||
&:hover {
|
||||
color: #1284e7;
|
||||
border-color: #1284e7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="example">
|
||||
<section class="demo">
|
||||
<span class="label">default:</span>
|
||||
<date-picker v-model="value1" lang="en" editable></date-picker>
|
||||
</section>
|
||||
<section class="demo">
|
||||
<span class="label">range:</span>
|
||||
<date-picker v-model="value2" range lang="en"></date-picker>
|
||||
</section>
|
||||
<pre class="pre">{{demo1}}</pre>
|
||||
</div>
|
||||
<div class="example">
|
||||
<section class="demo">
|
||||
<span class="label">datetime:</span>
|
||||
<date-picker v-model="value3" lang="en" type="datetime" format="yyyy-MM-dd HH:mm:ss"></date-picker>
|
||||
</section>
|
||||
<section class="demo">
|
||||
<span class="label">datetime with minute-step picker:</span>
|
||||
<date-picker v-model="value4" lang="en" type="datetime" format="yyyy-MM-dd hh:mm:ss a"
|
||||
:time-picker-options="{
|
||||
start: '00:00',
|
||||
step: '00:30',
|
||||
end: '23:30'
|
||||
}"></date-picker>
|
||||
</section>
|
||||
<section class="demo">
|
||||
<span class="label">datetime range:</span>
|
||||
<date-picker v-model="value5" range type="datetime" lang="en" format="yyyy-MM-dd HH:mm:ss"></date-picker>
|
||||
</section>
|
||||
<blockquote class="tips">
|
||||
if you use the datetime, you should set the format which default is 'yyyy-MM-dd'
|
||||
</blockquote>
|
||||
<pre class="pre">{{demo2}}</pre>
|
||||
</div>
|
||||
<div class="example">
|
||||
<section class="demo">
|
||||
<span class="label">with confirm:</span>
|
||||
<date-picker
|
||||
v-model="value6"
|
||||
format="yyyy-MM-dd"
|
||||
lang="en"
|
||||
confirm></date-picker>
|
||||
</section>
|
||||
<section class="demo">
|
||||
<span class="label">datetime with confirm:</span>
|
||||
<date-picker
|
||||
v-model="value7"
|
||||
type="datetime"
|
||||
lang="en"
|
||||
format="yyyy-MM-dd hh:mm:ss"
|
||||
confirm></date-picker>
|
||||
</section>
|
||||
<section class="demo">
|
||||
<span class="label">range with confirm:</span>
|
||||
<date-picker
|
||||
v-model="value8"
|
||||
range
|
||||
lang="en"
|
||||
format="yyyy-MM-dd"
|
||||
@confirm="confirm"
|
||||
confirm></date-picker>
|
||||
</section>
|
||||
<blockquote class="tips">
|
||||
Recommend to use the confirm option when the type is 'datetime' or range is true
|
||||
</blockquote>
|
||||
<pre class="pre">{{demo3}}</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatePicker from '../index'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: { DatePicker },
|
||||
data () {
|
||||
return {
|
||||
value1: new Date(),
|
||||
value2: '',
|
||||
value3: new Date(),
|
||||
value4: '',
|
||||
value5: '',
|
||||
value6: '',
|
||||
value7: '',
|
||||
value8: '',
|
||||
demo1: '<date-picker v-model="value1" editable lang="en"></date-picker>\n<date-picker v-model="value3" range lang="en"></date-picker>',
|
||||
demo2: `<date-picker v-model="value3" type="datetime" format="yyyy-MM-dd HH:mm:ss" lang="en"></date-picker>\n<date-picker v-model="value4" type="datetime" format="yyyy-MM-dd hh:mm:ss a" :time-picker-options="{start: '00:00',step: '00:30',end: '23:30'}" lang="en"></date-picker>\n<date-picker v-model="value4" range type="datetime" format="yyyy-MM-dd HH:mm:ss" lang="en"></date-picker>`,
|
||||
demo3: '<date-picker v-model="value6" format="yyyy-MM-dd" lang="en" confirm></date-picker>\n<date-picker v-model="value7" lang="en" type="datetime" format="yyyy-MM-dd hh:mm:ss" confirm></date-picker>\n<date-picker v-model="value8" lang="en" range format="yyyy-MM-dd" confirm></date-picker>'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
confirm (val) {
|
||||
console.log(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.demo {
|
||||
margin:20px;
|
||||
}
|
||||
.label{
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.example {
|
||||
padding: 45px;
|
||||
word-wrap: break-word;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
.example > .demo {
|
||||
display: inline-block;
|
||||
}
|
||||
.tips {
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>vue2-datepicker</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
.example {
|
||||
padding: 20px;
|
||||
word-wrap: break-word;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
.source {
|
||||
display: inline-block;
|
||||
margin:20px;
|
||||
}
|
||||
.label{
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.pre {
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.4;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.tips {
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/vue2-datepicker/dist/build.js"></script>
|
||||
<script src="./build.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
|
||||
<script>window.hljs.initHighlightingOnLoad();</script>
|
||||
</body>
|
||||
</html>
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import Vue from 'vue'
|
||||
import DatePicker from '@/index'
|
||||
|
||||
Vue.use(DatePicker)
|
||||
|
||||
new Vue({ // eslint-disable-line
|
||||
el: '#app',
|
||||
data () {
|
||||
return {
|
||||
value1: new Date(),
|
||||
value2: '',
|
||||
value3: new Date(),
|
||||
value4: '',
|
||||
value5: '',
|
||||
value6: '',
|
||||
value7: '',
|
||||
value8: '',
|
||||
value9: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSource (obj) {
|
||||
return Object.keys(obj).map(key => {
|
||||
const value = obj[key]
|
||||
return (
|
||||
<section class="source">
|
||||
<label class="label">{key} : </label>
|
||||
{Vue.compile(value).render.call(this)}
|
||||
</section>
|
||||
)
|
||||
})
|
||||
},
|
||||
getPre (obj) {
|
||||
return Object.keys(obj).map(key => {
|
||||
const value = obj[key].replace(/\n/g, '').replace(/\s+/g, ' ')
|
||||
return (
|
||||
<pre class="pre">
|
||||
<code class="language-html">{value}</code>
|
||||
</pre>
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
const example1 = {
|
||||
'base': '<date-picker v-model="value1" lang="en" :not-before="new Date()"></date-picker>',
|
||||
'range': '<date-picker v-model="value2" range lang="en"></date-picker>'
|
||||
}
|
||||
const example2 = {
|
||||
'datetime': `
|
||||
<date-picker
|
||||
v-model="value3"
|
||||
lang="en"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm:ss"></date-picker>`,
|
||||
'datetime with time-picker-options': `
|
||||
<date-picker
|
||||
v-model="value4"
|
||||
lang="en"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD hh:mm:ss a"
|
||||
:time-picker-options="{
|
||||
start: '00:00',
|
||||
step: '00:30',
|
||||
end: '23:30'
|
||||
}"></date-picker>`,
|
||||
'datetime with minute-step': `
|
||||
<date-picker
|
||||
v-model="value9"
|
||||
lang="en"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD hh:mm:ss a"
|
||||
:minute-step="10"
|
||||
></date-picker>`,
|
||||
'datetime range': `
|
||||
<date-picker
|
||||
v-model="value5"
|
||||
range
|
||||
type="datetime"
|
||||
lang="en"
|
||||
format="YYYY-MM-DD HH:mm:ss"></date-picker>`
|
||||
}
|
||||
const example3 = {
|
||||
'with confirm': `
|
||||
<date-picker
|
||||
v-model="value6"
|
||||
format="YYYY-MM-DD"
|
||||
lang="en"
|
||||
confirm></date-picker>`,
|
||||
'datetime with confirm': `
|
||||
<date-picker
|
||||
v-model="value7"
|
||||
type="datetime"
|
||||
lang="en"
|
||||
format="YYYY-MM-DD hh:mm:ss"
|
||||
confirm></date-picker>`,
|
||||
'range width confirm': `
|
||||
<date-picker
|
||||
v-model="value8"
|
||||
range
|
||||
lang="en"
|
||||
format="YYYY-MM-DD"
|
||||
confirm></date-picker>`
|
||||
}
|
||||
const arr = [
|
||||
{
|
||||
exam: example1
|
||||
},
|
||||
{
|
||||
exam: example2,
|
||||
tips: 'if you use the datetime, you should set the format to "YYYY-MM-DD HH:mm:ss" which default is "YYY-MM-DD'
|
||||
},
|
||||
{
|
||||
exam: example3,
|
||||
tips: 'Recommend to use the confirm option when the type is "datetime" or "range" is true'
|
||||
}
|
||||
]
|
||||
return (
|
||||
<div id="app">
|
||||
{arr.map(obj => (
|
||||
<div class="example">
|
||||
{this.getSource(obj.exam)}
|
||||
{
|
||||
obj.tips
|
||||
? <blockquote class="tips">{obj.tips}</blockquote>
|
||||
: ''
|
||||
}
|
||||
{this.getPre(obj.exam)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
new Vue({ // eslint-disable-line
|
||||
el: '#app',
|
||||
render: h => h(App)
|
||||
})
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
-11
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>vue2-datepicker</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="demo/build.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+8313
-1299
File diff suppressed because it is too large
Load Diff
+67
-28
@@ -1,12 +1,31 @@
|
||||
{
|
||||
"name": "vue2-datepicker",
|
||||
"description": "A Datepicker Component For Vue2",
|
||||
"main": "dist/build.js",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"version": "1.9.8",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
|
||||
"demo": "cross-env NODE_ENV=production webpack --progress --hide-modules --config webpack.demo.config.js",
|
||||
"deploy": "cross-env NODE_ENV=production webpack --progress --hide-modules --config webpack.deploy.config.js"
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot --open --config build/webpack.dev.config.js",
|
||||
"demo": "cross-env NODE_ENV=production webpack --progress --hide-modules --config build/webpack.demo.config.js",
|
||||
"deploy": "cross-env NODE_ENV=production webpack --progress --hide-modules --config build/webpack.deploy.config.js",
|
||||
"test:push": "jest --coverage --coverageReporters=text-lcov | coveralls",
|
||||
"test": "jest"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"vue"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
|
||||
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"^@/(.*)$": "<rootDir>/src/$1"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -23,34 +42,54 @@
|
||||
},
|
||||
"homepage": "https://github.com/mengxiong10/vue2-datepicker#readme",
|
||||
"dependencies": {
|
||||
"vue": "^2.2.1"
|
||||
"fecha": "^2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.6",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-eslint": "^8.0.2",
|
||||
"babel-loader": "^7.0.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.18",
|
||||
"autoprefixer": "^7.2.6",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-helper-vue-jsx-merge-props": "^2.0.3",
|
||||
"babel-jest": "^23.0.1",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-jsx-v-model": "^2.0.3",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"babel-plugin-transform-vue-jsx": "^3.7.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-stage-3": "^6.24.1",
|
||||
"cross-env": "^5.0.0",
|
||||
"coveralls": "^3.0.1",
|
||||
"cross-env": "^5.1.6",
|
||||
"css-loader": "^0.25.0",
|
||||
"eslint": "^4.12.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"cz-conventional-changelog": "^2.1.0",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-standard": "^11.0.0",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-loader": "^1.9.0",
|
||||
"eslint-plugin-html": "^4.0.1",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^5.2.1",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"file-loader": "^0.9.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"postcss-loader": "^2.0.9",
|
||||
"sass-loader": "^6.0.6",
|
||||
"vue-loader": "^13.0.5",
|
||||
"vue-template-compiler": "^2.2.1",
|
||||
"webpack": "^2.2.0",
|
||||
"webpack-dev-server": "^2.2.0",
|
||||
"webpack-merge": "^4.1.1"
|
||||
"eslint-loader": "^2.0.0",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"eslint-plugin-node": "^6.0.1",
|
||||
"eslint-plugin-promise": "^3.8.0",
|
||||
"eslint-plugin-standard": "^3.1.0",
|
||||
"eslint-plugin-vue": "^4.5.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"highlight.js": "^9.12.0",
|
||||
"jest": "^23.0.1",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"node-sass": "^4.9.0",
|
||||
"optimize-css-assets-webpack-plugin": "^4.0.2",
|
||||
"postcss-loader": "^2.1.5",
|
||||
"sass-loader": "^6.0.7",
|
||||
"vue": "^2.5.16",
|
||||
"vue-jest": "^2.6.0",
|
||||
"vue-loader": "^15.2.1",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"webpack": "^4.9.1",
|
||||
"webpack-cli": "^2.1.4",
|
||||
"webpack-dev-server": "^3.1.4",
|
||||
"webpack-merge": "^4.1.2"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<div class="mx-calendar">
|
||||
<div class="mx-calendar-header">
|
||||
<a
|
||||
v-show="panel !== 'TIME'"
|
||||
class="mx-icon-last-year"
|
||||
@click="handleIconYear(-1)">«</a>
|
||||
<a
|
||||
v-show="panel === 'DATE'"
|
||||
class="mx-icon-last-month"
|
||||
@click="handleIconMonth(-1)">‹</a>
|
||||
<a
|
||||
v-show="panel !== 'TIME'"
|
||||
class="mx-icon-next-year"
|
||||
@click="handleIconYear(1)">»</a>
|
||||
<a
|
||||
v-show="panel === 'DATE'"
|
||||
class="mx-icon-next-month"
|
||||
@click="handleIconMonth(1)">›</a>
|
||||
<a
|
||||
v-show="panel !== 'TIME'"
|
||||
class="mx-current-month"
|
||||
@click="handleBtnMonth">{{months[calendarMonth]}}</a>
|
||||
<a
|
||||
v-show="panel !== 'TIME'"
|
||||
class="mx-current-year"
|
||||
@click="handleBtnYear">{{calendarYear}}</a>
|
||||
<a
|
||||
v-show="panel === 'TIME'"
|
||||
class="mx-time-header"
|
||||
@click="showPanelDate">{{timeHeader}}</a>
|
||||
</div>
|
||||
<div class="mx-calendar-content">
|
||||
<panel-date
|
||||
v-show="panel === 'DATE'"
|
||||
:value="value"
|
||||
:calendar-month="calendarMonth"
|
||||
:calendar-year="calendarYear"
|
||||
:start-at="startAt"
|
||||
:end-at="endAt"
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
:disabled-date="isDisabledDate"
|
||||
@select="selectDate"/>
|
||||
<panel-year
|
||||
v-show="panel === 'YEAR'"
|
||||
:value="value"
|
||||
:first-year="firstYear"
|
||||
@select="selectYear" />
|
||||
<panel-month
|
||||
v-show="panel === 'MONTH'"
|
||||
:value="value"
|
||||
:calendar-year="calendarYear"
|
||||
@select="selectMonth" />
|
||||
<panel-time
|
||||
v-show="panel === 'TIME'"
|
||||
:minute-step="minuteStep"
|
||||
:time-picker-options="timePickerOptions"
|
||||
:value="value"
|
||||
:disabled-time="isDisabledTime"
|
||||
@select="selectTime" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isValidDate, isDateObejct } from '@/utils/index'
|
||||
import { t } from '@/locale/index'
|
||||
import scrollIntoView from '@/utils/scroll-into-view'
|
||||
import PanelDate from '@/panel/date'
|
||||
import PanelYear from '@/panel/year'
|
||||
import PanelMonth from '@/panel/month'
|
||||
import PanelTime from '@/panel/time'
|
||||
|
||||
export default {
|
||||
name: 'CalendarPanel',
|
||||
components: { PanelDate, PanelYear, PanelMonth, PanelTime },
|
||||
props: {
|
||||
value: {
|
||||
default: null,
|
||||
validator: function (val) {
|
||||
return val === null || isValidDate(val)
|
||||
}
|
||||
},
|
||||
startAt: null,
|
||||
endAt: null,
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// below user set
|
||||
type: {
|
||||
type: String,
|
||||
default: 'date' // ['date', 'datetime']
|
||||
},
|
||||
firstDayOfWeek: {
|
||||
default: 7,
|
||||
type: Number,
|
||||
validator: val => val >= 1 && val <= 7
|
||||
},
|
||||
notBefore: {
|
||||
default: null,
|
||||
validator: function (val) {
|
||||
return !val || isValidDate(val)
|
||||
}
|
||||
},
|
||||
notAfter: {
|
||||
default: null,
|
||||
validator: function (val) {
|
||||
return !val || isValidDate(val)
|
||||
}
|
||||
},
|
||||
disabledDays: {
|
||||
type: [Array, Function],
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
minuteStep: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
validator: val => val >= 0 && val <= 60
|
||||
},
|
||||
timePickerOptions: {
|
||||
type: [Object, Function],
|
||||
default () {
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const now = new Date()
|
||||
const calendarYear = now.getFullYear()
|
||||
const calendarMonth = now.getMonth()
|
||||
const firstYear = Math.floor(calendarYear / 10) * 10
|
||||
const months = t('months')
|
||||
return {
|
||||
panel: 'DATE',
|
||||
dates: [],
|
||||
months,
|
||||
calendarMonth,
|
||||
calendarYear,
|
||||
firstYear
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
now: {
|
||||
get () {
|
||||
return new Date(this.calendarYear, this.calendarMonth).getTime()
|
||||
},
|
||||
set (val) {
|
||||
const now = new Date(val)
|
||||
this.calendarYear = now.getFullYear()
|
||||
this.calendarMonth = now.getMonth()
|
||||
}
|
||||
},
|
||||
timeHeader () {
|
||||
return this.value && new Date(this.value).toLocaleDateString()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler: 'updateNow'
|
||||
},
|
||||
visible: {
|
||||
immediate: true,
|
||||
handler: 'init'
|
||||
},
|
||||
panel: {
|
||||
immediate: true,
|
||||
handler: 'handelPanelChange'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handelPanelChange (panel) {
|
||||
if (panel === 'YEAR') {
|
||||
this.firstYear = Math.floor(this.calendarYear / 10) * 10
|
||||
} else if (panel === 'TIME') {
|
||||
this.$nextTick(() => {
|
||||
[...this.$el.querySelectorAll('.mx-panel-time .mx-time-list')].forEach(el => {
|
||||
scrollIntoView(el, el.querySelector('.actived'))
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
init () {
|
||||
this.panel = 'DATE'
|
||||
this.updateNow(this.value)
|
||||
},
|
||||
// 根据value更新日历
|
||||
updateNow (value) {
|
||||
this.now = value ? new Date(value) : new Date()
|
||||
},
|
||||
isDisabledTime (date, startAt, endAt) {
|
||||
const time = new Date(date).getTime()
|
||||
const notBefore = this.notBefore && (time < new Date(this.notBefore))
|
||||
const notAfter = this.notAfter && (time > new Date(this.notAfter))
|
||||
startAt = startAt === undefined ? this.startAt : startAt
|
||||
startAt = startAt && (time < new Date(startAt))
|
||||
endAt = endAt === undefined ? this.endAt : endAt
|
||||
endAt = endAt && (time > new Date(endAt))
|
||||
return notBefore || notAfter || startAt || endAt
|
||||
},
|
||||
isDisabledDate (date, startAt, endAt) {
|
||||
const time = new Date(date).getTime()
|
||||
const notBefore = this.notBefore && (time < new Date(this.notBefore).setHours(0, 0, 0, 0))
|
||||
const notAfter = this.notAfter && (time > new Date(this.notAfter).setHours(0, 0, 0, 0))
|
||||
startAt = startAt === undefined ? this.startAt : startAt
|
||||
startAt = startAt && (time < new Date(startAt).setHours(0, 0, 0, 0))
|
||||
endAt = endAt === undefined ? this.endAt : endAt
|
||||
endAt = endAt && (time > new Date(endAt).setHours(0, 0, 0, 0))
|
||||
let disabledDays = false
|
||||
if (Array.isArray(this.disabledDays)) {
|
||||
disabledDays = this.disabledDays.some(v => new Date(v).setHours(0, 0, 0, 0) === time)
|
||||
} else if (typeof this.disabledDays === 'function') {
|
||||
disabledDays = this.disabledDays(new Date(date))
|
||||
}
|
||||
|
||||
return notBefore || notAfter || disabledDays || startAt || endAt
|
||||
},
|
||||
selectDate (date) {
|
||||
if (this.type === 'datetime') {
|
||||
let time = new Date(date)
|
||||
if (isDateObejct(this.value)) {
|
||||
time.setHours(
|
||||
this.value.getHours(),
|
||||
this.value.getMinutes(),
|
||||
this.value.getSeconds()
|
||||
)
|
||||
}
|
||||
if (this.isDisabledTime(time)) {
|
||||
time.setHours(0, 0, 0, 0)
|
||||
if (this.notBefore && time.getTime() < new Date(this.notBefore).getTime()) {
|
||||
time = new Date(this.notBefore)
|
||||
}
|
||||
if (this.startAt && time.getTime() < new Date(this.startAt).getTime()) {
|
||||
time = new Date(this.startAt)
|
||||
}
|
||||
}
|
||||
this.$emit('select-time', time)
|
||||
this.panel = 'TIME'
|
||||
return
|
||||
}
|
||||
this.$emit('select-date', date)
|
||||
},
|
||||
selectYear (year) {
|
||||
this.changeCalendarYear(year)
|
||||
this.showPanelMonth()
|
||||
},
|
||||
selectMonth (month) {
|
||||
this.changeCalendarMonth(month)
|
||||
this.showPanelDate()
|
||||
},
|
||||
selectTime (time) {
|
||||
this.$emit('select-time', time)
|
||||
},
|
||||
changeCalendarYear (year) {
|
||||
this.now = new Date(year, this.calendarMonth)
|
||||
},
|
||||
changeCalendarMonth (month) {
|
||||
this.now = new Date(this.calendarYear, month)
|
||||
},
|
||||
handleIconMonth (flag) {
|
||||
this.changeCalendarMonth(this.calendarMonth + flag)
|
||||
},
|
||||
handleIconYear (flag) {
|
||||
if (this.panel === 'YEAR') {
|
||||
this.changePanelYears(flag)
|
||||
} else {
|
||||
this.changeCalendarYear(this.calendarYear + flag)
|
||||
}
|
||||
},
|
||||
handleBtnYear () {
|
||||
if (this.panel === 'YEAR') {
|
||||
this.showPanelDate()
|
||||
} else {
|
||||
this.showPanelYear()
|
||||
}
|
||||
},
|
||||
handleBtnMonth () {
|
||||
if (this.panel === 'MONTH') {
|
||||
this.showPanelDate()
|
||||
} else {
|
||||
this.showPanelMonth()
|
||||
}
|
||||
},
|
||||
changePanelYears (flag) {
|
||||
this.firstYear = this.firstYear + flag * 10
|
||||
},
|
||||
showPanelDate () {
|
||||
this.panel = 'DATE'
|
||||
},
|
||||
showPanelYear () {
|
||||
this.panel = 'YEAR'
|
||||
},
|
||||
showPanelMonth () {
|
||||
this.panel = 'MONTH'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
export default {
|
||||
bind (el, binding, vnode) {
|
||||
el['@clickoutside'] = e => {
|
||||
if (
|
||||
!el.contains(e.target) &&
|
||||
binding.expression &&
|
||||
vnode.context[binding.expression]
|
||||
) {
|
||||
binding.value()
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', el['@clickoutside'], true)
|
||||
},
|
||||
unbind (el) {
|
||||
document.removeEventListener('click', el['@clickoutside'], true)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import DatePicker from './datepicker/index.vue'
|
||||
import DatePicker from './index.vue'
|
||||
import './index.scss'
|
||||
|
||||
DatePicker.install = function (Vue) {
|
||||
Vue.component(DatePicker.name, DatePicker)
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
$default-color: #73879c;
|
||||
$primary-color: #1284e7;
|
||||
|
||||
.mx-datepicker {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 210px;
|
||||
color: $default-color;
|
||||
font: 14px/1.5 'Helvetica Neue', Helvetica, Arial, 'Microsoft Yahei', sans-serif;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.mx-datepicker-range {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.mx-datepicker-popup {
|
||||
position: absolute;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.mx-input-wrapper {
|
||||
position: relative;
|
||||
.mx-clear-wrapper {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
.mx-clear-wrapper {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx-input {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 30px;
|
||||
padding-left: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-input-append {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.mx-input-icon {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-style: normal;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx-calendar-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #555;
|
||||
stroke-width: 8px;
|
||||
stroke: currentColor;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.mx-clear-icon {
|
||||
&::before {
|
||||
display: inline-block;
|
||||
content: '\2716';
|
||||
vertical-align: middle;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-range-wrapper {
|
||||
width: 248px * 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx-shortcuts-wrapper {
|
||||
text-align: left;
|
||||
padding: 0 12px;
|
||||
line-height: 34px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
.mx-shortcuts {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: mix(#fff, $primary-color, 20%);
|
||||
}
|
||||
&:after {
|
||||
content: '|';
|
||||
margin: 0 10px;
|
||||
color: #48576a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx-datepicker-footer {
|
||||
padding: 4px;
|
||||
clear: both;
|
||||
text-align: right;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.mx-datepicker-btn {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
padding: 7px 15px;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.mx-datepicker-btn-confirm {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: #73879c;
|
||||
&:hover {
|
||||
color: #1284e7;
|
||||
border-color: #1284e7;
|
||||
}
|
||||
}
|
||||
|
||||
/* 日历组件 */
|
||||
.mx-calendar {
|
||||
float: left;
|
||||
color: $default-color;
|
||||
padding: 6px 12px;
|
||||
font: 14px/1.5 Helvetica Neue,Helvetica,Arial,Microsoft Yahei,sans-serif;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-calendar-header {
|
||||
padding: 0 4px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
> a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: mix(#fff, $primary-color, 20%);
|
||||
}
|
||||
}
|
||||
@at-root {
|
||||
.mx-icon-last-month,
|
||||
.mx-icon-next-month {
|
||||
padding: 0 6px;
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.mx-icon-last-month {
|
||||
float: left;
|
||||
}
|
||||
.mx-icon-next-month {
|
||||
float: right;
|
||||
}
|
||||
.mx-icon-last-year {
|
||||
@extend .mx-icon-last-month
|
||||
}
|
||||
.mx-icon-next-year {
|
||||
@extend .mx-icon-next-month
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx-calendar-content {
|
||||
width: 32px * 7;
|
||||
height: 32px * 7;
|
||||
.cell {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #eaf8fe;
|
||||
}
|
||||
&.actived {
|
||||
color: #fff;
|
||||
background-color: $primary-color;
|
||||
}
|
||||
&.inrange {
|
||||
background-color: #eaf8fe;
|
||||
}
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #ccc;
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx-panel-date {
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
td, th {
|
||||
font-size: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
td {
|
||||
&.today {
|
||||
color: mix(#fff, $primary-color, 10%);
|
||||
}
|
||||
&.last-month,
|
||||
&.next-month {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx-panel-year {
|
||||
padding: 7px 0;
|
||||
.cell {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
margin: 1px 5%;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-panel-month {
|
||||
.cell {
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
line-height: 40px;
|
||||
margin: 8px 1.5%;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-time-list {
|
||||
position: relative; // 定位 offsetParent
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-top: 1px solid rgba(0,0,0,.05);
|
||||
border-left: 1px solid rgba(0,0,0,.05);
|
||||
overflow-y: auto;
|
||||
.mx-time-picker-item {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
&:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
.cell {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
/* 滚动条滑块 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 10px;
|
||||
box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<div
|
||||
class="mx-datepicker"
|
||||
:class="{
|
||||
'mx-datepicker-range': range,
|
||||
'disabled': disabled
|
||||
}"
|
||||
:style="{
|
||||
'width': computedWidth
|
||||
}"
|
||||
v-clickoutside="closePopup">
|
||||
<div class="mx-input-wrapper"
|
||||
@click="showPopup">
|
||||
<input
|
||||
:class="inputClass"
|
||||
ref="input"
|
||||
type="text"
|
||||
:name="inputName"
|
||||
:disabled="disabled"
|
||||
:readonly="!editable"
|
||||
:value="text"
|
||||
:placeholder="innerPlaceholder"
|
||||
@input="handleInput"
|
||||
@change="handleChange">
|
||||
<span class="mx-input-append">
|
||||
<slot name="calendar-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 200 200" class="mx-calendar-icon">
|
||||
<rect x="13" y="29" rx="14" ry="14" width="174" height="158" fill="transparent" />
|
||||
<line x1="46" x2="46" y1="8" y2="50" />
|
||||
<line x1="154" x2="154" y1="8" y2="50" />
|
||||
<line x1="13" x2="187" y1="70" y2="70" />
|
||||
<text x="50%" y="135" font-size="90" stroke-width="1" text-anchor="middle" dominant-baseline="middle">{{new Date().getDate()}}</text>
|
||||
</svg>
|
||||
</slot>
|
||||
</span>
|
||||
<span
|
||||
v-if="showClearIcon"
|
||||
class="mx-input-append mx-clear-wrapper"
|
||||
@click.stop="clearDate">
|
||||
<slot name="mx-clear-icon">
|
||||
<i class="mx-input-icon mx-clear-icon"></i>
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mx-datepicker-popup"
|
||||
:style="position"
|
||||
v-show="popupVisible"
|
||||
ref="calendar">
|
||||
<slot name="header">
|
||||
<div class="mx-shortcuts-wrapper"
|
||||
v-if="range && innnerShortcuts.length">
|
||||
<span
|
||||
class="mx-shortcuts"
|
||||
v-for="(range, index) in innnerShortcuts"
|
||||
:key="index"
|
||||
@click="selectRange(range)">{{range.text}}</span>
|
||||
</div>
|
||||
</slot>
|
||||
<calendar-panel
|
||||
v-if="!range"
|
||||
v-bind="$attrs"
|
||||
:value="currentValue"
|
||||
:visible="popupVisible"
|
||||
@select-date="selectDate"
|
||||
@select-time="selectTime"></calendar-panel>
|
||||
<div class="mx-range-wrapper"
|
||||
v-else>
|
||||
<calendar-panel
|
||||
style="box-shadow:1px 0 rgba(0, 0, 0, .1)"
|
||||
v-bind="$attrs"
|
||||
:value="currentValue[0]"
|
||||
:end-at="currentValue[1]"
|
||||
:start-at="null"
|
||||
:visible="popupVisible"
|
||||
@select-date="selectStartDate"
|
||||
@select-time="selectStartTime"></calendar-panel>
|
||||
<calendar-panel
|
||||
v-bind="$attrs"
|
||||
:value="currentValue[1]"
|
||||
:start-at="currentValue[0]"
|
||||
:end-at="null"
|
||||
:visible="popupVisible"
|
||||
@select-date="selectEndDate"
|
||||
@select-time="selectEndTime"></calendar-panel>
|
||||
</div>
|
||||
<slot name="footer">
|
||||
<div class="mx-datepicker-footer"
|
||||
v-if="confirm">
|
||||
<button type="button"
|
||||
class="mx-datepicker-btn mx-datepicker-btn-confirm"
|
||||
@click="confirmDate">{{ confirmText }}</button>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fecha from 'fecha'
|
||||
import clickoutside from '@/directives/clickoutside'
|
||||
import { isValidDate, isValidRange, isDateObejct } from '@/utils/index'
|
||||
import { use, t } from '@/locale/index'
|
||||
import CalendarPanel from './calendar.vue'
|
||||
|
||||
export default {
|
||||
name: 'DatePicker',
|
||||
components: { CalendarPanel },
|
||||
directives: {
|
||||
clickoutside
|
||||
},
|
||||
props: {
|
||||
value: null,
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
lang: {
|
||||
type: [String, Object],
|
||||
default: 'zh'
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'YYYY-MM-DD'
|
||||
},
|
||||
range: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rangeSeparator: {
|
||||
type: String,
|
||||
default: '~'
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: 'OK'
|
||||
},
|
||||
confirm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
shortcuts: {
|
||||
type: [Boolean, Array],
|
||||
default: true
|
||||
},
|
||||
inputName: {
|
||||
type: String,
|
||||
default: 'date'
|
||||
},
|
||||
inputClass: {
|
||||
type: [String, Array],
|
||||
default: 'mx-input'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentValue: this.range ? [null, null] : null,
|
||||
userInput: null,
|
||||
popupVisible: false,
|
||||
position: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler: 'handleValueChange'
|
||||
},
|
||||
popupVisible (val) {
|
||||
if (val) {
|
||||
this.initCalendar()
|
||||
} else {
|
||||
this.userInput = null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
innerPlaceholder () {
|
||||
if (typeof this.placeholder === 'string') {
|
||||
return this.placeholder
|
||||
}
|
||||
return this.range ? t('placeholder.dateRange') : t('placeholder.date')
|
||||
},
|
||||
text () {
|
||||
if (this.userInput !== null) {
|
||||
return this.userInput
|
||||
}
|
||||
if (!this.range) {
|
||||
return isValidDate(this.value) ? this.stringify(this.value) : ''
|
||||
}
|
||||
return isValidRange(this.value)
|
||||
? `${this.stringify(this.value[0])} ${this.rangeSeparator} ${this.stringify(this.value[1])}`
|
||||
: ''
|
||||
},
|
||||
computedWidth () {
|
||||
if (typeof this.width === 'number' || (typeof this.width === 'string' && /^\d+$/.test(this.width))) {
|
||||
return this.width + 'px'
|
||||
}
|
||||
return this.width
|
||||
},
|
||||
showClearIcon () {
|
||||
return !this.disabled && this.clearable && (this.range ? isValidRange(this.value) : isValidDate(this.value))
|
||||
},
|
||||
innnerShortcuts () {
|
||||
if (Array.isArray(this.shortcuts)) {
|
||||
return this.shortcuts
|
||||
}
|
||||
if (this.shortcuts === false) {
|
||||
return []
|
||||
}
|
||||
const pickers = t('pickers')
|
||||
const arr = [
|
||||
{
|
||||
text: pickers[0],
|
||||
start: new Date(),
|
||||
end: new Date(Date.now() + 3600 * 1000 * 24 * 7)
|
||||
},
|
||||
{
|
||||
text: pickers[1],
|
||||
start: new Date(),
|
||||
end: new Date(Date.now() + 3600 * 1000 * 24 * 30)
|
||||
},
|
||||
{
|
||||
text: pickers[2],
|
||||
start: new Date(Date.now() - 3600 * 1000 * 24 * 7),
|
||||
end: new Date()
|
||||
},
|
||||
{
|
||||
text: pickers[3],
|
||||
start: new Date(Date.now() - 3600 * 1000 * 24 * 30),
|
||||
end: new Date()
|
||||
}
|
||||
]
|
||||
return arr
|
||||
}
|
||||
},
|
||||
created () {
|
||||
use(this.lang)
|
||||
},
|
||||
methods: {
|
||||
initCalendar () {
|
||||
this.handleValueChange(this.value)
|
||||
this.displayPopup()
|
||||
},
|
||||
stringify (date, format) {
|
||||
format = format || this.format
|
||||
return fecha.format(date, format)
|
||||
},
|
||||
parseDate (value, format) {
|
||||
try {
|
||||
format = format || this.format
|
||||
return fecha.parse(value, format)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
dateEqual (a, b) {
|
||||
return isDateObejct(a) && isDateObejct(b) && a.getTime() === b.getTime()
|
||||
},
|
||||
rangeEqual (a, b) {
|
||||
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => this.dateEqual(item, b[index]))
|
||||
},
|
||||
selectRange (range) {
|
||||
this.currentValue = [ new Date(range.start), new Date(range.end) ]
|
||||
this.updateDate(true)
|
||||
},
|
||||
clearDate () {
|
||||
const date = this.range ? [null, null] : null
|
||||
this.currentValue = date
|
||||
this.updateDate(true)
|
||||
},
|
||||
confirmDate () {
|
||||
const valid = this.range ? isValidRange(this.currentValue) : isValidDate(this.currentValue)
|
||||
if (valid) {
|
||||
this.updateDate(true)
|
||||
}
|
||||
this.$emit('confirm', this.currentValue)
|
||||
this.closePopup()
|
||||
},
|
||||
updateDate (confirm = false) {
|
||||
if ((this.confirm && !confirm) || this.disabled) {
|
||||
return false
|
||||
}
|
||||
const equal = this.range ? this.rangeEqual(this.value, this.currentValue) : this.dateEqual(this.value, this.currentValue)
|
||||
if (equal) {
|
||||
return false
|
||||
}
|
||||
this.$emit('input', this.currentValue)
|
||||
this.$emit('change', this.currentValue)
|
||||
return true
|
||||
},
|
||||
handleValueChange (value) {
|
||||
if (!this.range) {
|
||||
this.currentValue = isValidDate(value) ? new Date(value) : null
|
||||
} else {
|
||||
this.currentValue = isValidRange(value) ? [new Date(value[0]), new Date(value[1])] : [null, null]
|
||||
}
|
||||
},
|
||||
selectDate (date) {
|
||||
this.currentValue = date
|
||||
this.updateDate() && this.closePopup()
|
||||
},
|
||||
selectStartDate (date) {
|
||||
this.$set(this.currentValue, 0, date)
|
||||
if (this.currentValue[1]) {
|
||||
this.updateDate()
|
||||
}
|
||||
},
|
||||
selectEndDate (date) {
|
||||
this.$set(this.currentValue, 1, date)
|
||||
if (this.currentValue[0]) {
|
||||
this.updateDate()
|
||||
}
|
||||
},
|
||||
selectTime (time) {
|
||||
this.currentValue = time
|
||||
this.updateDate()
|
||||
},
|
||||
selectStartTime (time) {
|
||||
this.selectStartDate(time)
|
||||
},
|
||||
selectEndTime (time) {
|
||||
this.selectEndDate(time)
|
||||
},
|
||||
showPopup () {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
this.popupVisible = true
|
||||
},
|
||||
closePopup () {
|
||||
this.popupVisible = false
|
||||
},
|
||||
displayPopup () {
|
||||
const dw = document.documentElement.clientWidth
|
||||
const dh = document.documentElement.clientHeight
|
||||
const InputRect = this.$el.getBoundingClientRect()
|
||||
const PopupRect = this.$refs.calendar.getBoundingClientRect()
|
||||
this.position = {}
|
||||
if (
|
||||
dw - InputRect.left < PopupRect.width &&
|
||||
InputRect.right < PopupRect.width
|
||||
) {
|
||||
this.position.left = 1 - InputRect.left + 'px'
|
||||
} else if (InputRect.left + InputRect.width / 2 <= dw / 2) {
|
||||
this.position.left = 0
|
||||
} else {
|
||||
this.position.right = 0
|
||||
}
|
||||
if (
|
||||
InputRect.top <= PopupRect.height + 1 &&
|
||||
dh - InputRect.bottom <= PopupRect.height + 1
|
||||
) {
|
||||
this.position.top = dh - InputRect.top - PopupRect.height - 1 + 'px'
|
||||
} else if (InputRect.top + InputRect.height / 2 <= dh / 2) {
|
||||
this.position.top = '100%'
|
||||
} else {
|
||||
this.position.bottom = '100%'
|
||||
}
|
||||
},
|
||||
handleInput (event) {
|
||||
this.userInput = event.target.value
|
||||
},
|
||||
handleChange (event) {
|
||||
const value = event.target.value
|
||||
if (this.editable && this.userInput !== null) {
|
||||
const calendar = this.$children[0]
|
||||
const checkDate = calendar.type === 'date' ? calendar.isDisabledDate : calendar.isDisabledTime
|
||||
if (this.range) {
|
||||
const range = value.split(` ${this.rangeSeparator} `)
|
||||
if (range.length === 2) {
|
||||
const start = this.parseDate(range[0], this.format)
|
||||
const end = this.parseDate(range[1], this.format)
|
||||
if (start && end && !checkDate(start, null, end) && !checkDate(end, start, null)) {
|
||||
this.currentValue = [ start, end ]
|
||||
this.updateDate(true)
|
||||
this.closePopup()
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const date = this.parseDate(value, this.format)
|
||||
if (date && !checkDate(date, null, null)) {
|
||||
this.currentValue = date
|
||||
this.updateDate(true)
|
||||
this.closePopup()
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$emit('input-error', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
import Languages from './languages'
|
||||
import { isPlainObject } from '@/utils/index'
|
||||
|
||||
let lang = 'zh'
|
||||
|
||||
export function use (target) {
|
||||
if (isPlainObject(target)) {
|
||||
lang = { ...Languages.en, ...target }
|
||||
} else {
|
||||
lang = Languages[target] || Languages.en
|
||||
}
|
||||
}
|
||||
|
||||
export function t (path) {
|
||||
const arr = path.split('.')
|
||||
let current = lang
|
||||
let value
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
const prop = arr[i]
|
||||
value = current[prop]
|
||||
if (i === len - 1) {
|
||||
return value
|
||||
}
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
current = value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { t } from '@/locale/index'
|
||||
|
||||
export default {
|
||||
name: 'panelDate',
|
||||
props: {
|
||||
value: null,
|
||||
startAt: null,
|
||||
endAt: null,
|
||||
calendarMonth: {
|
||||
default: new Date().getMonth()
|
||||
},
|
||||
calendarYear: {
|
||||
default: new Date().getFullYear()
|
||||
},
|
||||
firstDayOfWeek: {
|
||||
default: 7,
|
||||
type: Number,
|
||||
validator: val => val >= 1 && val <= 7
|
||||
},
|
||||
disabledDate: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectDate ({ year, month, day }) {
|
||||
const date = new Date(year, month, day)
|
||||
if (this.disabledDate(date)) {
|
||||
return
|
||||
}
|
||||
this.$emit('select', date)
|
||||
},
|
||||
getDays (firstDayOfWeek) {
|
||||
const days = t('days')
|
||||
const firstday = parseInt(firstDayOfWeek, 10)
|
||||
return days.concat(days).slice(firstday, firstday + 7)
|
||||
},
|
||||
getDates (year, month, firstDayOfWeek) {
|
||||
const arr = []
|
||||
const time = new Date(year, month)
|
||||
|
||||
time.setDate(0) // 把时间切换到上个月最后一天
|
||||
const lastMonthLength = (time.getDay() + 7 - firstDayOfWeek) % 7 + 1 // time.getDay() 0是星期天, 1是星期一 ...
|
||||
const lastMonthfirst = time.getDate() - (lastMonthLength - 1)
|
||||
for (let i = 0; i < lastMonthLength; i++) {
|
||||
arr.push({ year, month: month - 1, day: lastMonthfirst + i })
|
||||
}
|
||||
|
||||
time.setMonth(time.getMonth() + 2, 0) // 切换到这个月最后一天
|
||||
const curMonthLength = time.getDate()
|
||||
for (let i = 0; i < curMonthLength; i++) {
|
||||
arr.push({ year, month, day: 1 + i })
|
||||
}
|
||||
|
||||
time.setMonth(time.getMonth() + 1, 1) // 切换到下个月第一天
|
||||
const nextMonthLength = 42 - (lastMonthLength + curMonthLength)
|
||||
for (let i = 0; i < nextMonthLength; i++) {
|
||||
arr.push({ year, month: month + 1, day: 1 + i })
|
||||
}
|
||||
|
||||
return arr
|
||||
},
|
||||
getCellClasses ({ year, month, day }) {
|
||||
const classes = []
|
||||
const cellTime = new Date(year, month, day).getTime()
|
||||
const today = new Date().setHours(0, 0, 0, 0)
|
||||
const curTime = this.value && new Date(this.value).setHours(0, 0, 0, 0)
|
||||
const startTime = this.startAt && new Date(this.startAt).setHours(0, 0, 0, 0)
|
||||
const endTime = this.endAt && new Date(this.endAt).setHours(0, 0, 0, 0)
|
||||
|
||||
if (month < this.calendarMonth) {
|
||||
classes.push('last-month')
|
||||
} else if (month > this.calendarMonth) {
|
||||
classes.push('next-month')
|
||||
} else {
|
||||
classes.push('cur-month')
|
||||
}
|
||||
|
||||
if (cellTime === today) {
|
||||
classes.push('today')
|
||||
}
|
||||
|
||||
if (this.disabledDate(cellTime)) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
|
||||
if (curTime) {
|
||||
if (cellTime === curTime) {
|
||||
classes.push('actived')
|
||||
} else if (startTime && cellTime <= curTime) {
|
||||
classes.push('inrange')
|
||||
} else if (endTime && cellTime >= curTime) {
|
||||
classes.push('inrange')
|
||||
}
|
||||
}
|
||||
|
||||
return classes
|
||||
},
|
||||
getCellTitle ({ year, month, day }) {
|
||||
return new Date(year, month, day).toLocaleDateString()
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
const ths = this.getDays(this.firstDayOfWeek).map(day => {
|
||||
return <th>{day}</th>
|
||||
})
|
||||
|
||||
const dates = this.getDates(this.calendarYear, this.calendarMonth, this.firstDayOfWeek)
|
||||
const tbody = Array.apply(null, { length: 6 }).map((week, i) => {
|
||||
const tds = dates.slice(7 * i, 7 * i + 7).map(date => {
|
||||
const attrs = {
|
||||
class: this.getCellClasses(date)
|
||||
}
|
||||
return (
|
||||
<td
|
||||
class="cell"
|
||||
{...attrs}
|
||||
title={this.getCellTitle(date)}
|
||||
onClick={this.selectDate.bind(this, date)}>
|
||||
{date.day}
|
||||
</td>
|
||||
)
|
||||
})
|
||||
return <tr>{tds}</tr>
|
||||
})
|
||||
|
||||
return (
|
||||
<table class="mx-panel mx-panel-date">
|
||||
<thead>
|
||||
<tr>{ths}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tbody}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { t } from '@/locale/index'
|
||||
|
||||
export default {
|
||||
name: 'panelMonth',
|
||||
props: {
|
||||
value: null,
|
||||
calendarYear: {
|
||||
default: new Date().getFullYear()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectMonth (month) {
|
||||
this.$emit('select', month)
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
let months = t('months')
|
||||
const currentYear = this.value && new Date(this.value).getFullYear()
|
||||
const currentMonth = this.value && new Date(this.value).getMonth()
|
||||
months = months.map((v, i) => {
|
||||
return <span
|
||||
class={{
|
||||
'cell': true,
|
||||
'actived': currentYear === this.calendarYear && currentMonth === i
|
||||
}}
|
||||
onClick={this.selectMonth.bind(this, i)}>
|
||||
{v}
|
||||
</span>
|
||||
})
|
||||
return <div class="mx-panel mx-panel-month">{months}</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { formatTime, parseTime } from '@/utils/index'
|
||||
|
||||
export default {
|
||||
name: 'panelTime',
|
||||
props: {
|
||||
timePickerOptions: {
|
||||
type: [Object, Function],
|
||||
default () {
|
||||
return null
|
||||
}
|
||||
},
|
||||
minuteStep: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
validator: val => val >= 0 && val <= 60
|
||||
},
|
||||
value: null,
|
||||
disabledTime: Function
|
||||
},
|
||||
computed: {
|
||||
currentHours () {
|
||||
return new Date(this.value).getHours()
|
||||
},
|
||||
currentMinutes () {
|
||||
return new Date(this.value).getMinutes()
|
||||
},
|
||||
currentSeconds () {
|
||||
return new Date(this.value).getSeconds()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stringifyText (value) {
|
||||
return ('00' + value).slice(String(value).length)
|
||||
},
|
||||
selectTime (time) {
|
||||
if (typeof this.disabledTime === 'function' && this.disabledTime(time)) {
|
||||
return
|
||||
}
|
||||
this.$emit('select', new Date(time))
|
||||
},
|
||||
getTimeSelectOptions () {
|
||||
const result = []
|
||||
const options = this.timePickerOptions
|
||||
if (!options) {
|
||||
return []
|
||||
}
|
||||
if (typeof options === 'function') {
|
||||
return options() || []
|
||||
}
|
||||
const start = parseTime(options.start)
|
||||
const end = parseTime(options.end)
|
||||
const step = parseTime(options.step)
|
||||
if (start && end && step) {
|
||||
const startMinutes = start.minutes + start.hours * 60
|
||||
const endMinutes = end.minutes + end.hours * 60
|
||||
const stepMinutes = step.minutes + step.hours * 60
|
||||
const len = Math.floor((endMinutes - startMinutes) / stepMinutes)
|
||||
for (let i = 0; i <= len; i++) {
|
||||
let timeMinutes = startMinutes + i * stepMinutes
|
||||
let hours = Math.floor(timeMinutes / 60)
|
||||
let minutes = timeMinutes % 60
|
||||
let value = {
|
||||
hours, minutes
|
||||
}
|
||||
result.push({
|
||||
value,
|
||||
label: formatTime(value)
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
},
|
||||
render (h) {
|
||||
const date = new Date(this.value)
|
||||
const disabledTime = typeof this.disabledTime === 'function' && this.disabledTime
|
||||
|
||||
let pickers = this.getTimeSelectOptions()
|
||||
if (Array.isArray(pickers) && pickers.length) {
|
||||
pickers = pickers.map(picker => {
|
||||
const pickHours = picker.value.hours
|
||||
const pickMinutes = picker.value.minutes
|
||||
const time = new Date(date).setHours(pickHours, pickMinutes, 0)
|
||||
return (
|
||||
<li
|
||||
class={{
|
||||
'mx-time-picker-item': true,
|
||||
'cell': true,
|
||||
'actived': pickHours === this.currentHours && pickMinutes === this.currentMinutes,
|
||||
'disabled': disabledTime && disabledTime(time)
|
||||
}}
|
||||
onClick={this.selectTime.bind(this, time)}>{picker.label}</li>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div class="mx-panel mx-panel-time">
|
||||
<ul class="mx-time-list">
|
||||
{pickers}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const hours = Array.apply(null, { length: 24 }).map((_, i) => {
|
||||
const time = new Date(date).setHours(i)
|
||||
return <li
|
||||
class={{
|
||||
'cell': true,
|
||||
'actived': i === this.currentHours,
|
||||
'disabled': disabledTime && disabledTime(time)
|
||||
}}
|
||||
onClick={this.selectTime.bind(this, time)}
|
||||
>{this.stringifyText(i)}</li>
|
||||
})
|
||||
|
||||
const step = this.minuteStep || 1
|
||||
const length = parseInt(60 / step)
|
||||
const minutes = Array.apply(null, { length }).map((_, i) => {
|
||||
const value = i * step
|
||||
const time = new Date(date).setMinutes(value)
|
||||
return <li
|
||||
class={{
|
||||
'cell': true,
|
||||
'actived': value === this.currentMinutes,
|
||||
'disabled': disabledTime && disabledTime(time)
|
||||
}}
|
||||
onClick={this.selectTime.bind(this, time)}
|
||||
>{this.stringifyText(value)}</li>
|
||||
})
|
||||
|
||||
const seconds = Array.apply(null, { length: 60 }).map((_, i) => {
|
||||
const time = new Date(date).setSeconds(i)
|
||||
return <li
|
||||
class={{
|
||||
'cell': true,
|
||||
'actived': i === this.currentSeconds,
|
||||
'disabled': disabledTime && disabledTime(time)
|
||||
}}
|
||||
onClick={this.selectTime.bind(this, time)}
|
||||
>{this.stringifyText(i)}</li>
|
||||
})
|
||||
|
||||
let times = [hours, minutes]
|
||||
if (this.minuteStep === 0) {
|
||||
times.push(seconds)
|
||||
}
|
||||
|
||||
times = times.map(list => (
|
||||
<ul class="mx-time-list"
|
||||
style={{ width: 100 / times.length + '%' }}>
|
||||
{list}
|
||||
</ul>
|
||||
))
|
||||
|
||||
return (
|
||||
<div class="mx-panel mx-panel-time">
|
||||
{times}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
name: 'panelYear',
|
||||
props: {
|
||||
value: null,
|
||||
firstYear: Number
|
||||
},
|
||||
methods: {
|
||||
selectYear (year) {
|
||||
this.$emit('select', year)
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
// 当前年代
|
||||
const firstYear = Math.floor(this.firstYear / 10) * 10
|
||||
const currentYear = this.value && new Date(this.value).getFullYear()
|
||||
const years = Array.apply(null, { length: 10 }).map((_, i) => {
|
||||
const year = firstYear + i
|
||||
return <span
|
||||
class={{
|
||||
'cell': true,
|
||||
'actived': currentYear === year
|
||||
}}
|
||||
onClick={this.selectYear.bind(this, year)}
|
||||
>{year}</span>
|
||||
})
|
||||
return <div class="mx-panel mx-panel-year">{years}</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export function isPlainObject (obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]'
|
||||
}
|
||||
|
||||
export function isDateObejct (value) {
|
||||
return value instanceof Date
|
||||
}
|
||||
|
||||
export function isValidDate (date) {
|
||||
if (date === null || date === undefined) {
|
||||
return false
|
||||
}
|
||||
return !!new Date(date).getTime()
|
||||
}
|
||||
|
||||
export function isValidRange (date) {
|
||||
return (
|
||||
Array.isArray(date) &&
|
||||
date.length === 2 &&
|
||||
isValidDate(date[0]) &&
|
||||
isValidDate(date[1]) &&
|
||||
(new Date(date[1]).getTime() >= new Date(date[0]).getTime())
|
||||
)
|
||||
}
|
||||
|
||||
export function parseTime (time) {
|
||||
const values = (time || '').split(':')
|
||||
if (values.length >= 2) {
|
||||
const hours = parseInt(values[0], 10)
|
||||
const minutes = parseInt(values[1], 10)
|
||||
return {
|
||||
hours,
|
||||
minutes
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function formatTime (time, type = '24') {
|
||||
let hours = time.hours
|
||||
hours = (type === '24') ? hours : (hours % 12 || 12)
|
||||
hours = hours < 10 ? '0' + hours : hours
|
||||
let minutes = time.minutes < 10 ? '0' + time.minutes : time.minutes
|
||||
let result = hours + ':' + minutes
|
||||
if (type === '12') {
|
||||
result += time.hours >= 12 ? ' pm' : ' am'
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export default function scrollIntoView (container, selected) {
|
||||
if (!selected) {
|
||||
container.scrollTop = 0
|
||||
return
|
||||
}
|
||||
|
||||
const offsetParents = []
|
||||
let pointer = selected.offsetParent
|
||||
while (pointer && container !== pointer && container.contains(pointer)) {
|
||||
offsetParents.push(pointer)
|
||||
pointer = pointer.offsetParent
|
||||
}
|
||||
const top = selected.offsetTop + offsetParents.reduce((prev, curr) => (prev + curr.offsetTop), 0)
|
||||
const bottom = top + selected.offsetHeight
|
||||
const viewRectTop = container.scrollTop
|
||||
const viewRectBottom = viewRectTop + container.clientHeight
|
||||
|
||||
if (top < viewRectTop) {
|
||||
container.scrollTop = top
|
||||
} else if (bottom > viewRectBottom) {
|
||||
container.scrollTop = bottom - container.clientHeight
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
import Vue from 'vue'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import { use } from '../src/locale/index'
|
||||
import DatePicker from '../src/index.vue'
|
||||
import CalendarPanel from '../src/calendar.vue'
|
||||
|
||||
import DatePanel from '../src/panel/date'
|
||||
import TimePanel from '../src/panel/time'
|
||||
import YearPanel from '../src/panel/year'
|
||||
|
||||
use('zh')
|
||||
|
||||
let wrapper
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
|
||||
describe('datepicker', () => {
|
||||
it('click: pick date', () => {
|
||||
wrapper = mount(DatePicker, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 2)
|
||||
}
|
||||
})
|
||||
// 2018-05-03
|
||||
const vm = wrapper.vm
|
||||
|
||||
let td = wrapper.find('.mx-panel-date td:nth-child(5)')
|
||||
expect(td.classes()).not.toContain('actived')
|
||||
expect(vm.text).toBe('2018-05-02')
|
||||
const time = new Date(2018, 4, 3).getTime()
|
||||
td.trigger('click')
|
||||
const emitted = wrapper.emitted()
|
||||
expect(emitted.input[0][0].getTime()).toBe(time)
|
||||
expect(emitted.change[0][0].getTime()).toBe(time)
|
||||
wrapper.setProps({ value: emitted.input[0][0] })
|
||||
td = wrapper.find('.mx-panel-date td:nth-child(5)')
|
||||
expect(td.classes()).toContain('actived')
|
||||
expect(vm.text).toBe('2018-05-03')
|
||||
|
||||
})
|
||||
|
||||
it('click: clear icon', () => {
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 2)
|
||||
}
|
||||
})
|
||||
const vm = wrapper.vm
|
||||
const clear = wrapper.find('.mx-clear-wrapper')
|
||||
clear.trigger('click')
|
||||
const emitted = wrapper.emitted()
|
||||
expect(emitted.input[0][0]).toBe(null)
|
||||
wrapper.setProps({ value: null })
|
||||
expect(vm.text).toBe('')
|
||||
})
|
||||
|
||||
it('prop: range', (done) => {
|
||||
wrapper = mount(DatePicker, {
|
||||
propsData: {
|
||||
range: true,
|
||||
value: [],
|
||||
confirm: false
|
||||
},
|
||||
sync: false // sync bug
|
||||
})
|
||||
|
||||
const calendars = wrapper.findAll(CalendarPanel)
|
||||
const calendar1 = calendars.at(0)
|
||||
const calendar2 = calendars.at(1)
|
||||
|
||||
const td1 = calendar1.findAll('.mx-panel-date tbody td')
|
||||
const td2 = calendar2.findAll('.mx-panel-date tbody td')
|
||||
|
||||
td1.at(14).trigger('click')
|
||||
|
||||
Vue.nextTick(() => {
|
||||
let emitted = wrapper.emittedByOrder()
|
||||
expect(emitted).toHaveLength(0)
|
||||
expect(td1.at(14).classes()).toContain('actived')
|
||||
expect(td2.at(13).classes()).toContain('disabled')
|
||||
expect(td2.at(14).classes()).not.toContain('disabled')
|
||||
|
||||
const date1 = new Date(td1.at(14).element.title)
|
||||
td2.at(16).trigger('click')
|
||||
Vue.nextTick(() => {
|
||||
emitted = wrapper.emittedByOrder()
|
||||
|
||||
const date2 = new Date(td2.at(16).element.title)
|
||||
|
||||
expect(td2.at(16).classes()).toContain('actived')
|
||||
expect(td1.at(15).classes()).toContain('inrange')
|
||||
expect(td1.at(16).classes()).toContain('inrange')
|
||||
expect(td1.at(17).classes()).toContain('disabled')
|
||||
|
||||
expect(emitted).toHaveLength(2)
|
||||
expect(emitted[0].args[0]).toEqual([date1, date2])
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('prop: rangeSeparator', () => {
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
range: true,
|
||||
value: [new Date('2018-06-01'), new Date('2018-06-10')],
|
||||
rangeSeparator: '至'
|
||||
}
|
||||
})
|
||||
const vm = wrapper.vm
|
||||
expect(vm.text).toBe('2018-06-01 至 2018-06-10')
|
||||
|
||||
})
|
||||
|
||||
it('prop: confirm', () => {
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
confirm: true
|
||||
}
|
||||
})
|
||||
const vm = wrapper.vm
|
||||
const btn = wrapper.find('.mx-datepicker-btn-confirm')
|
||||
expect(btn.exists()).toBe(true)
|
||||
// click the date expect popup don't close
|
||||
wrapper.setData({ popupVisible: true })
|
||||
vm.selectDate(new Date(2018, 5, 5))
|
||||
expect(vm.popupVisible).toBe(true)
|
||||
expect(wrapper.emittedByOrder()).toHaveLength(0)
|
||||
btn.trigger('click')
|
||||
expect(wrapper.emittedByOrder()).toHaveLength(3)
|
||||
expect(vm.popupVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('prop: confirmText', () => {
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
confirm: true,
|
||||
confirmText: '确定'
|
||||
}
|
||||
})
|
||||
const btn = wrapper.find('.mx-datepicker-btn-confirm')
|
||||
expect(btn.text()).toBe('确定')
|
||||
})
|
||||
|
||||
it('prop: width', () => {
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
width: 300
|
||||
}
|
||||
})
|
||||
const el = wrapper.find('.mx-datepicker').element
|
||||
let width = el.style.width
|
||||
expect(width).toBe('300px')
|
||||
wrapper.setProps({ width: '100%' })
|
||||
width = el.style.width
|
||||
expect(width).toBe('100%')
|
||||
})
|
||||
|
||||
it('prop: disabled', () => {
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
disabled: true,
|
||||
value: new Date(2018, 4, 5),
|
||||
clearable: true
|
||||
}
|
||||
})
|
||||
const vm = wrapper.vm
|
||||
// don't show the clearIocn
|
||||
expect(vm.showClearIcon).toBe(false)
|
||||
// don't show the popup
|
||||
vm.showPopup()
|
||||
expect(vm.popupVisible).toBe(false)
|
||||
})
|
||||
|
||||
it('prop: input attribte - inputName inputClass placeholder', () => {
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 5),
|
||||
inputName: 'datepicker',
|
||||
inputClass: ['mx-input', 'mx-my'],
|
||||
placeholder: 'hehe'
|
||||
}
|
||||
})
|
||||
const el = wrapper.find('.mx-input').element
|
||||
|
||||
expect(el.className).toBe('mx-input mx-my')
|
||||
expect(el.getAttribute('name')).toBe('datepicker')
|
||||
expect(el.getAttribute('placeholder')).toBe('hehe')
|
||||
})
|
||||
|
||||
it('prop: lang', () => {
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
lang: 'en'
|
||||
}
|
||||
})
|
||||
const el = wrapper.find('.mx-input').element
|
||||
expect(el.getAttribute('placeholder')).toBe('Select Date')
|
||||
})
|
||||
|
||||
it('prop: shortcuts', () => {
|
||||
const today = new Date()
|
||||
wrapper = shallowMount(DatePicker, {
|
||||
propsData: {
|
||||
shortcuts: [
|
||||
{
|
||||
text: 'Today',
|
||||
start: today,
|
||||
end: today
|
||||
}
|
||||
],
|
||||
range: true
|
||||
}
|
||||
})
|
||||
let shortcuts = wrapper.findAll('.mx-shortcuts')
|
||||
expect(shortcuts).toHaveLength(1)
|
||||
shortcuts = shortcuts.at(0)
|
||||
shortcuts.trigger('click')
|
||||
expect(wrapper.emitted()).toEqual({
|
||||
input: [[[today, today]]],
|
||||
change: [[[today, today]]]
|
||||
})
|
||||
wrapper.setProps({
|
||||
shortcuts: false
|
||||
})
|
||||
shortcuts = wrapper.find('.mx-shortcuts-wrapper')
|
||||
expect(shortcuts.exists()).toBe(false)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('calendar-panel', () => {
|
||||
|
||||
it('click: prev/next month', () => {
|
||||
wrapper = mount(CalendarPanel)
|
||||
|
||||
const nextBtn = wrapper.find('.mx-icon-next-month')
|
||||
const lastBtn = wrapper.find('.mx-icon-last-month')
|
||||
const vm = wrapper.vm
|
||||
let count = 12
|
||||
while (count--) {
|
||||
const oldYear = vm.calendarYear
|
||||
const oldMonth = vm.calendarMonth
|
||||
nextBtn.trigger('click')
|
||||
const newYear = vm.calendarYear
|
||||
const newMonth = vm.calendarMonth
|
||||
if (oldMonth === 11) {
|
||||
expect(newMonth).toBe(0)
|
||||
expect(newYear).toBe(oldYear + 1)
|
||||
} else {
|
||||
expect(newMonth).toBe(oldMonth + 1)
|
||||
expect(newYear).toBe(oldYear)
|
||||
}
|
||||
}
|
||||
count = 12
|
||||
while (count--) {
|
||||
const oldYear = vm.calendarYear
|
||||
const oldMonth = vm.calendarMonth
|
||||
lastBtn.trigger('click')
|
||||
const newYear = vm.calendarYear
|
||||
const newMonth = vm.calendarMonth
|
||||
if (oldMonth === 0) {
|
||||
expect(newMonth).toBe(11)
|
||||
expect(newYear).toBe(oldYear - 1)
|
||||
} else {
|
||||
expect(newMonth).toBe(oldMonth - 1)
|
||||
expect(newYear).toBe(oldYear)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('click: prev/next year', () => {
|
||||
wrapper = mount(CalendarPanel, {
|
||||
value: new Date(2018,4,5)
|
||||
})
|
||||
const nextBtn = wrapper.find('.mx-icon-next-year')
|
||||
const lastBtn = wrapper.find('.mx-icon-last-year')
|
||||
const yearBtn = wrapper.find('.mx-current-year')
|
||||
const vm = wrapper.vm
|
||||
const oldYear = vm.calendarYear
|
||||
expect(oldYear).toBe(2018)
|
||||
nextBtn.trigger('click')
|
||||
let newYear = vm.calendarYear
|
||||
expect(newYear).toBe(2019)
|
||||
lastBtn.trigger('click')
|
||||
newYear = vm.calendarYear
|
||||
expect(newYear).toBe(oldYear)
|
||||
// 年视图测试
|
||||
yearBtn.trigger('click')
|
||||
expect(vm.panel).toBe('YEAR')
|
||||
expect(vm.firstYear).toBe(2010)
|
||||
nextBtn.trigger('click')
|
||||
expect(vm.firstYear).toBe(2020)
|
||||
lastBtn.trigger('click')
|
||||
lastBtn.trigger('click')
|
||||
expect(vm.firstYear).toBe(2000)
|
||||
})
|
||||
|
||||
it('prop: notBefore/notAfter', () => {
|
||||
wrapper = mount(CalendarPanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 2),
|
||||
notBefore: new Date(2018, 4, 1, 12),
|
||||
notAfter: new Date(2018, 4, 31, 12)
|
||||
}
|
||||
})
|
||||
const tds = wrapper.findAll('.mx-panel-date td')
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const td = tds.at(i)
|
||||
const classes = td.classes()
|
||||
if (i < 2 || i > 32) {
|
||||
expect(classes).toContain('disabled')
|
||||
} else {
|
||||
expect(classes).not.toContain('disabled')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('prop: disabledDays(Array)', () => {
|
||||
const disabledDays = ['2018-05-01', new Date(2018, 4, 3)]
|
||||
wrapper = mount(CalendarPanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 2),
|
||||
disabledDays
|
||||
}
|
||||
})
|
||||
const tds = wrapper.findAll('.mx-panel-date td.disabled')
|
||||
expect(tds.length).toBe(disabledDays.length)
|
||||
for (let i = 0, len = tds.length; i < len; i++) {
|
||||
const tdDate = new Date(tds.at(i).element.title).getTime()
|
||||
const expectDate = new Date(disabledDays[i]).setHours(0, 0, 0, 0)
|
||||
expect(tdDate).toBe(expectDate)
|
||||
}
|
||||
})
|
||||
|
||||
it('prop: disabledDays(Function)', () => {
|
||||
const disabledDays = function (date) {
|
||||
return date < new Date(2018, 4, 1) || date > new Date(2018, 4, 31)
|
||||
}
|
||||
wrapper = mount(CalendarPanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 4),
|
||||
disabledDays
|
||||
}
|
||||
})
|
||||
const tds = wrapper.findAll('.mx-panel-date td')
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const td = tds.at(i)
|
||||
const classes = td.classes()
|
||||
if (i < 2 || i > 32) {
|
||||
expect(classes).toContain('disabled')
|
||||
} else {
|
||||
expect(classes).not.toContain('disabled')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('feat: when the time panel show, scroll to the right position', () => {
|
||||
wrapper = mount(CalendarPanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 4),
|
||||
type: 'datetime'
|
||||
}
|
||||
})
|
||||
wrapper.setData({
|
||||
panel: 'TIME'
|
||||
})
|
||||
const list = wrapper.find('.mx-time-list').element
|
||||
list.scrollTop = 200
|
||||
wrapper.setData({
|
||||
panel: 'DATE'
|
||||
})
|
||||
wrapper.setData({
|
||||
panel: 'TIME'
|
||||
})
|
||||
setTimeout(() => {
|
||||
expect(list.scrollTop).toBe(0)
|
||||
}, 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('date-panel', () => {
|
||||
const testRenderCalendar = (i) => it(`feat: render the corrent date panel firstDayOfWeek: ${i}`, () => {
|
||||
wrapper = mount(DatePanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 1),
|
||||
calendarMonth: 4,
|
||||
calendarYear: 2018,
|
||||
firstDayOfWeek: i
|
||||
}
|
||||
})
|
||||
const vm = wrapper.vm
|
||||
const lastMonth = new Date(2018, 3, 30)
|
||||
const lastMonthDay = 30
|
||||
const lastMonthLength = (lastMonth.getDay() + 7 - i) % 7 + 1
|
||||
const currentMonthLength = 31
|
||||
const tds = wrapper.findAll('.mx-panel-date td')
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const td = tds.at(i)
|
||||
const text = parseInt(td.text(), 10)
|
||||
const classes = td.classes()
|
||||
if (i < lastMonthLength) {
|
||||
expect(classes).toContain('last-month')
|
||||
expect(text).toBe(lastMonthDay - lastMonthLength + 1 + i)
|
||||
} else if (i < lastMonthLength + currentMonthLength) {
|
||||
expect(text).toBe(i - lastMonthLength + 1)
|
||||
expect(classes).toContain('cur-month')
|
||||
if (text === 1) {
|
||||
expect(classes).toContain('actived')
|
||||
}
|
||||
} else {
|
||||
expect(text).toBe(i - lastMonthLength - currentMonthLength + 1)
|
||||
expect(classes).toContain('next-month')
|
||||
}
|
||||
}
|
||||
const week = ['一', '二', '三', '四', '五', '六', '日']
|
||||
const firstWeek = wrapper.find('tr th').text()
|
||||
expect(firstWeek).toBe(week[i - 1])
|
||||
})
|
||||
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
testRenderCalendar(i)
|
||||
}
|
||||
})
|
||||
|
||||
describe('year-panel', () => {
|
||||
it('feat: render the corrent year panel', () => {
|
||||
wrapper = mount(YearPanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 4, 1),
|
||||
firstYear: 2010
|
||||
}
|
||||
})
|
||||
|
||||
const cells = wrapper.findAll('.cell')
|
||||
|
||||
for (let i = 0, len = cells.length; i < len; i++) {
|
||||
const cell = cells.at(i)
|
||||
expect(parseInt(cell.text())).toBe(2010 + i)
|
||||
if (i === 8) {
|
||||
expect(cell.classes()).toContain('actived')
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.setProps({ firstYear: 2020 })
|
||||
|
||||
for (let i = 0, len = cells.length; i < len; i++) {
|
||||
const cell = cells.at(i)
|
||||
expect(parseInt(cell.text())).toBe(2020 + i)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('time-panel', () => {
|
||||
|
||||
it('click: pick time emitted the select event', () => {
|
||||
wrapper = mount(TimePanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 5, 5)
|
||||
}
|
||||
})
|
||||
const list = wrapper.findAll('.mx-time-list')
|
||||
expect(list).toHaveLength(3)
|
||||
const hours = list.at(0).findAll('.cell')
|
||||
const minutes = list.at(1).findAll('.cell')
|
||||
const seconds = list.at(2).findAll('.cell')
|
||||
|
||||
expect(hours).toHaveLength(24)
|
||||
expect(minutes).toHaveLength(60)
|
||||
expect(seconds).toHaveLength(60)
|
||||
|
||||
hours.at(1).trigger('click')
|
||||
minutes.at(1).trigger('click')
|
||||
seconds.at(1).trigger('click')
|
||||
expect(wrapper.emitted()).toEqual({
|
||||
select: [[new Date(2018, 5, 5, 1)], [new Date(2018, 5, 5, 0, 1)], [new Date(2018, 5, 5, 0, 0, 1)]]
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
it('prop: minuteStep', () => {
|
||||
wrapper = mount(TimePanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 5, 5),
|
||||
minuteStep: 30
|
||||
}
|
||||
})
|
||||
const list = wrapper.findAll('.mx-time-list')
|
||||
expect(list).toHaveLength(2)
|
||||
const minutes = list.at(1).findAll('.cell')
|
||||
expect(minutes).toHaveLength(2)
|
||||
expect(minutes.at(0).text()).toBe('00')
|
||||
expect(minutes.at(1).text()).toBe('30')
|
||||
})
|
||||
|
||||
it('prop: timePickerOptions', () => {
|
||||
wrapper = mount(TimePanel, {
|
||||
propsData: {
|
||||
value: new Date(2018, 5, 5),
|
||||
timePickerOptions: { start: '01:00', step: '00:30', end: '23:00' }
|
||||
}
|
||||
})
|
||||
const list = wrapper.findAll('.mx-time-list')
|
||||
expect(list).toHaveLength(1)
|
||||
const cells = list.at(0).findAll('.cell')
|
||||
expect(cells).toHaveLength(45)
|
||||
expect(cells.at(0).text()).toBe('01:00')
|
||||
expect(cells.at(44).text()).toBe('23:00')
|
||||
cells.at(0).trigger('click')
|
||||
const emitted = wrapper.emitted()
|
||||
expect(emitted).toEqual({
|
||||
select: [[new Date(2018, 5, 5, 1)]]
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@@ -1,45 +0,0 @@
|
||||
var path = require('path')
|
||||
var webpack = require('webpack')
|
||||
|
||||
module.exports = {
|
||||
entry: './demo/main.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, './demo'),
|
||||
publicPath: '/demo/',
|
||||
filename: 'build.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
loaders: {
|
||||
}
|
||||
// other vue-loader options go here
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
noInfo: true,
|
||||
port: 9000
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
devtool: '#eval-source-map'
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.config.js')
|
||||
const webpack = require('webpack')
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
sourceMap: false,
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
}),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
module.exports = webpackConfig
|
||||
@@ -1,34 +0,0 @@
|
||||
const path = require('path')
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.config.js')
|
||||
const webpack = require('webpack')
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
entry: './index.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, './dist'),
|
||||
publicPath: '/dist/',
|
||||
filename: 'build.js',
|
||||
library: "DatePicker",
|
||||
libraryTarget: "umd"
|
||||
},
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
sourceMap: false,
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
}),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
module.exports = webpackConfig
|
||||
Reference in New Issue
Block a user