diff --git a/build/compiler.js b/build/compiler.js
index 7ec5f61c..6aa6c582 100644
--- a/build/compiler.js
+++ b/build/compiler.js
@@ -18,6 +18,8 @@ const libDir = path.resolve(__dirname, '../lib');
const esDir = path.resolve(__dirname, '../dist');
const exampleDir = path.resolve(__dirname, '../example/dist');
+const baseCssPath = path.resolve(__dirname, '../packages/common/index.wxss');
+
const lessCompiler = dist =>
function compileLess() {
return gulp
@@ -27,7 +29,11 @@ const lessCompiler = dist =>
.pipe(
insert.transform((contents, file) => {
if (!file.path.includes('packages' + path.sep + 'common')) {
- contents = `@import '../common/index.wxss';${contents}`;
+ const relativePath = path.relative(
+ path.normalize(`${file.path}${path.sep}..`),
+ baseCssPath
+ );
+ contents = `@import '${relativePath}';${contents}`;
}
return contents;
})
diff --git a/example/app.json b/example/app.json
index c1f84519..c507c92e 100644
--- a/example/app.json
+++ b/example/app.json
@@ -48,7 +48,8 @@
"pages/dropdown-menu/index",
"pages/index-bar/index",
"pages/skeleton/index",
- "pages/divider/index"
+ "pages/divider/index",
+ "pages/calendar/index"
],
"window": {
"navigationBarBackgroundColor": "#f8f8f8",
@@ -119,7 +120,8 @@
"van-grid-item": "./dist/grid-item/index",
"van-dropdown-menu": "./dist/dropdown-menu/index",
"van-dropdown-item": "./dist/dropdown-item/index",
- "van-skeleton": "./dist/skeleton/index"
+ "van-skeleton": "./dist/skeleton/index",
+ "van-calendar": "./dist/calendar/index"
},
"sitemapLocation": "sitemap.json"
}
diff --git a/example/config.js b/example/config.js
index 2b87364f..c92b3448 100644
--- a/example/config.js
+++ b/example/config.js
@@ -37,6 +37,10 @@ export default [
groupName: '表单组件',
icon: 'https://img.yzcdn.cn/vant/form-0401.svg',
list: [
+ {
+ path: '/calendar',
+ title: 'Calendar 日历',
+ },
{
path: '/checkbox',
title: 'Checkbox 复选框'
diff --git a/example/pages/calendar/index.js b/example/pages/calendar/index.js
new file mode 100644
index 00000000..3becd5b0
--- /dev/null
+++ b/example/pages/calendar/index.js
@@ -0,0 +1,150 @@
+import Page from '../../common/page';
+
+Page({
+ data: {
+ date: {
+ maxRange: [],
+ selectSingle: null,
+ selectRange: [],
+ selectMultiple: [],
+ quickSelect1: null,
+ quickSelect2: [],
+ customColor: [],
+ customConfirm: [],
+ customRange: null,
+ customDayText: [],
+ customPosition: null
+ },
+ type: 'single',
+ round: true,
+ color: undefined,
+ minDate: Date.now(),
+ maxDate: new Date(
+ new Date().getFullYear(),
+ new Date().getMonth() + 6,
+ new Date().getDate()
+ ).getTime(),
+ maxRange: undefined,
+ position: undefined,
+ formatter: undefined,
+ showConfirm: false,
+ showCalendar: false,
+ tiledMinDate: new Date(2012, 0, 10).getTime(),
+ tiledMaxDate: new Date(2012, 2, 20).getTime(),
+ confirmText: undefined,
+ confirmDisabledText: undefined
+ },
+
+ onConfirm(event) {
+ console.log(event);
+ this.setData({ showCalendar: false });
+
+ this.setData({
+ [`date.${this.data.id}`]: event.detail
+ });
+ },
+
+ onSelect(event) {
+ console.log(event);
+ },
+
+ onClose() {
+ this.setData({ showCalendar: false });
+ },
+
+ onOpen() {
+ console.log('open');
+ },
+
+ onOpened() {
+ console.log('opened');
+ },
+
+ onClosed() {
+ console.log('closed');
+ },
+
+ resetSettings() {
+ this.setData({
+ round: true,
+ color: null,
+ minDate: Date.now(),
+ maxDate: new Date(
+ new Date().getFullYear(),
+ new Date().getMonth() + 6,
+ new Date().getDate()
+ ).getTime(),
+ maxRange: null,
+ position: 'bottom',
+ formatter: null,
+ showConfirm: true,
+ confirmText: '确定',
+ confirmDisabledText: null
+ });
+ },
+
+ show(event) {
+ this.resetSettings();
+ const { type, id } = event.currentTarget.dataset;
+ const data = {
+ id,
+ type,
+ showCalendar: true
+ };
+
+ switch (id) {
+ case 'quickSelect1':
+ case 'quickSelect2':
+ data.showConfirm = false;
+ break;
+ case 'customColor':
+ data.color = '#07c160';
+ break;
+ case 'customConfirm':
+ data.confirmText = '完成';
+ data.confirmDisabledText = '请选择结束时间';
+ break;
+ case 'customRange':
+ data.minDate = new Date(2010, 0, 1).getTime();
+ data.maxDate = new Date(2010, 0, 31).getTime();
+ break;
+ case 'customDayText':
+ data.minDate = new Date(2010, 4, 1).getTime();
+ data.maxDate = new Date(2010, 4, 31).getTime();
+ data.formatter = this.dayFormatter;
+ break;
+ case 'customPosition':
+ data.round = false;
+ data.position = 'right';
+ break;
+ case 'maxRange':
+ data.maxRange = 3;
+ break;
+ }
+
+ this.setData(data);
+ },
+
+ dayFormatter(day) {
+ const month = day.date.getMonth() + 1;
+ const date = day.date.getDate();
+
+ if (month === 5) {
+ if (date === 1) {
+ day.topInfo = '劳动节';
+ } else if (date === 4) {
+ day.topInfo = '五四青年节';
+ } else if (date === 11) {
+ day.text = '今天';
+ }
+ }
+
+ if (day.type === 'start') {
+ day.bottomInfo = '入店';
+ } else if (day.type === 'end') {
+ day.bottomInfo = '离店';
+ }
+
+ return day;
+ }
+});
diff --git a/example/pages/calendar/index.json b/example/pages/calendar/index.json
new file mode 100644
index 00000000..b8b169cc
--- /dev/null
+++ b/example/pages/calendar/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "Calendar 日历"
+}
diff --git a/example/pages/calendar/index.wxml b/example/pages/calendar/index.wxml
new file mode 100644
index 00000000..1aeee9d9
--- /dev/null
+++ b/example/pages/calendar/index.wxml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/pages/calendar/index.wxs b/example/pages/calendar/index.wxs
new file mode 100644
index 00000000..3ee7e00b
--- /dev/null
+++ b/example/pages/calendar/index.wxs
@@ -0,0 +1,33 @@
+/* eslint-disable */
+function formatDate(date) {
+ if (date) {
+ date = getDate(date);
+ return date.getMonth() + 1 + '/' + date.getDate();
+ }
+}
+
+function formatFullDate(date) {
+ if (date) {
+ date = getDate(date);
+ return date.getFullYear() + '/' + formatDate(date);
+ }
+}
+
+function formatMultiple(dates) {
+ if (dates.length) {
+ return '选择了 ' + dates.length + '个日期';
+ }
+}
+
+function formatRange(dateRange) {
+ if (dateRange.length) {
+ return formatDate(dateRange[0]) + ' - ' + formatDate(dateRange[1]);
+ }
+}
+
+module.exports = {
+ formatDate: formatDate,
+ formatFullDate: formatFullDate,
+ formatMultiple: formatMultiple,
+ formatRange: formatRange
+};
diff --git a/example/pages/calendar/index.wxss b/example/pages/calendar/index.wxss
new file mode 100644
index 00000000..c4b0001f
--- /dev/null
+++ b/example/pages/calendar/index.wxss
@@ -0,0 +1,3 @@
+.tiled-calendar {
+ --calendar-height: 500px;
+}
diff --git a/example/project.config.json b/example/project.config.json
index 733fc56b..a85a795d 100644
--- a/example/project.config.json
+++ b/example/project.config.json
@@ -343,6 +343,12 @@
"pathName": "pages/dropdown-menu/index",
"query": "",
"scene": null
+ },
+ {
+ "id": -1,
+ "name": "calendar",
+ "pathName": "pages/calendar/index",
+ "scene": null
}
]
}
diff --git a/packages/calendar/README.md b/packages/calendar/README.md
new file mode 100644
index 00000000..463da588
--- /dev/null
+++ b/packages/calendar/README.md
@@ -0,0 +1,330 @@
+# Calendar 日历
+
+### 引入
+
+在`app.json`或`index.json`中引入组件,详细介绍见[快速上手](#/quickstart#yin-ru-zu-jian)
+
+```json
+"usingComponents": {
+ "van-calendar": "path/to/@vant/weapp/dist/calendar/index"
+}
+```
+
+## 代码演示
+
+### 选择单个日期
+
+下面演示了结合单元格来使用日历组件的用法,日期选择完成后会触发`confirm`事件
+
+```html
+
+
+```
+
+```js
+Page({
+ data: {
+ date: '',
+ show: false
+ },
+
+ onShow() {
+ this.setData({ show: true });
+ },
+ onClose() {
+ this.setData({ show: false });
+ },
+ formatDate(date) {
+ date = new Date(date);
+ return `${date.getMonth() + 1}/${date.getDate()}`;
+ },
+ onConfirm(event) {
+ this.setData({
+ show: false,
+ date: this.formatDate(event.detail)
+ });
+ }
+});
+```
+
+### 选择多个日期
+
+设置`type`为`multiple`后可以选择多个日期,此时`confirm`事件返回的 date 为数组结构,数组包含若干个选中的日期。
+
+```html
+
+
+```
+
+```js
+Page({
+ data: {
+ text: '',
+ show: false
+ },
+
+ onShow() {
+ this.setData({ show: true });
+ },
+ onClose() {
+ this.setData({ show: false });
+ },
+ onConfirm(event) {
+ this.setData({
+ show: false,
+ date: `选择了 ${event.detail.length} 个日期`
+ });
+ }
+});
+```
+
+### 选择日期区间
+
+设置`type`为`range`后可以选择日期区间,此时`confirm`事件返回的 date 为数组结构,数组第一项为开始时间,第二项为结束时间。
+
+```html
+
+
+```
+
+```js
+Page({
+ data: {
+ date: '',
+ show: false
+ },
+
+ onShow() {
+ this.setData({ show: true });
+ },
+ onClose() {
+ this.setData({ show: false });
+ },
+ formatDate(date) {
+ date = new Date(date);
+ return `${date.getMonth() + 1}/${date.getDate()}`;
+ },
+ onConfirm(date) {
+ const [start, end] = date;
+ this.setData({
+ show: false,
+ date: `${this.formatDate(start)} - ${this.formatDate(end)}`
+ });
+ }
+});
+```
+
+### 快捷选择
+
+将`show-confirm`设置为`false`可以隐藏确认按钮,这种情况下选择完成后会立即触发`confirm`事件
+
+```html
+
+```
+
+### 自定义颜色
+
+通过`color`属性可以自定义日历的颜色,对选中日期和底部按钮生效
+
+```html
+
+```
+
+### 自定义日期范围
+
+通过`min-date`和`max-date`定义日历的范围
+
+```html
+
+```
+
+```js
+Page({
+ data: {
+ show: false,
+ minDate: new Date(2010, 0, 1).getTime(),
+ maxDate: new Date(2010, 0, 31).getTime()
+ }
+});
+```
+
+### 自定义按钮文字
+
+通过`confirm-text`设置按钮文字,通过`confirm-disabled-text`设置按钮禁用时的文字
+
+```html
+
+```
+
+### 自定义日期文案
+
+通过传入`formatter`函数来对日历上每一格的内容进行格式化
+
+```html
+
+```
+
+```js
+Page({
+ data: {
+ formatter(day) {
+ const month = day.date.getMonth() + 1;
+ const date = day.date.getDate();
+
+ if (month === 5) {
+ if (date === 1) {
+ day.topInfo = '劳动节';
+ } else if (date === 4) {
+ day.topInfo = '五四青年节';
+ } else if (date === 11) {
+ day.text = '今天';
+ }
+ }
+
+ if (day.type === 'start') {
+ day.bottomInfo = '入住';
+ } else if (day.type === 'end') {
+ day.bottomInfo = '离店';
+ }
+
+ return day;
+ }
+ }
+});
+```
+
+### 自定义弹出位置
+
+通过`position`属性自定义弹出层的弹出位置,可选值为`top`、`left`、`right`
+
+```html
+
+```
+
+### 日期区间最大范围
+
+选择日期区间时,可以通过`max-range`属性来指定最多可选天数,选择的范围超过最多可选天数时,会弹出相应的提示文案
+
+```html
+
+```
+
+### 平铺展示
+
+将`poppable`设置为`false`,日历会直接展示在页面内,而不是以弹层的形式出现
+
+```html
+
+```
+
+```css
+.calendar {
+ --calendar-height: 500px;
+}
+```
+
+## API
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --------------------- | -------------------------------------------------------------------------------------------------- | -------------------- | ------------------ |
+| type | 选择类型:
`single`表示选择单个日期,
`multiple`表示选择多个日期,
`range`表示选择日期区间 | _string_ | `single` |
+| title | 日历标题 | _string_ | `日期选择` |
+| color | 主题色,对底部按钮和选中日期生效 | _string_ | `#ee0a24` |
+| min-date | 可选择的最小日期 | _number_ | 当前日期 |
+| max-date | 可选择的最大日期 | _number_ | 当前日期的六个月后 |
+| default-date | 默认选中的日期,`type`为`multiple`或`range`时为数组 | _number \| number[]_ | 今天 |
+| row-height | 日期行高 | _number \| string_ | `64` |
+| formatter | 日期格式化函数 | _(day: Day) => Day_ | - |
+| poppable | 是否以弹层的形式展示日历 | _boolean_ | `true` |
+| show-mark | 是否显示月份背景水印 | _boolean_ | `true` |
+| show-title | 是否展示日历标题 | _boolean_ | `true` |
+| show-subtitle | 是否展示日历副标题(年月) | _boolean_ | `true` |
+| show-confirm | 是否展示确认按钮 | _boolean_ | `true` |
+| confirm-text | 确认按钮的文字 | _string_ | `确定` |
+| confirm-disabled-text | 确认按钮处于禁用状态时的文字 | _string_ | `确定` |
+
+### Poppable Props
+
+当 Canlendar 的 `poppable` 为 `true` 时,支持以下 props:
+
+| 参数 | 说明 | 类型 | 默认值 |
+| ---------------------- | --------------------------------------- | --------- | -------- |
+| show | 是否显示日历弹窗 | _boolean_ | `false` |
+| position | 弹出位置,可选值为 `top` `right` `left` | _string_ | `bottom` |
+| round | 是否显示圆角弹窗 | _boolean_ | `true` |
+| close-on-click-overlay | 是否在点击遮罩层后关闭 | _boolean_ | `true` |
+| safe-area-inset-bottom | 是否开启底部安全区适配 | _boolean_ | `true` |
+
+### Range Props
+
+当 Canlendar 的 `type` 为 `range` 时,支持以下 props:
+
+| 参数 | 说明 | 类型 | 默认值 |
+| -------------- | ------------------------------------ | ------------------ | ------------------------ |
+| max-range | 日期区间最多可选天数,默认无限制 | _number \| string_ | - |
+| range-prompt | 范围选择超过最多可选天数时的提示文案 | _string_ | `选择天数不能超过 xx 天` |
+| allow-same-day | 是否允许日期范围的起止时间为同一天 | _boolean_ | `fasle` |
+
+### Day 数据结构
+
+日历中的每个日期都对应一个 Day 对象,通过`formatter`属性可以自定义 Day 对象的内容
+
+| 键名 | 说明 | 类型 |
+| ---------- | ------------------------------------------------------------------ | -------- |
+| date | 日期对应的 Date 对象 | _Date_ |
+| type | 日期类型,可选值为`selected`、`start`、`middle`、`end`、`disabled` | _string_ |
+| text | 中间显示的文字 | _string_ |
+| topInfo | 上方的提示信息 | _string_ |
+| bottomInfo | 下方的提示信息 | _string_ |
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+| ------- | ------------------------------------------------------------------ | ----------------------- |
+| select | 点击任意日期时触发 | _value: Date \| Date[]_ |
+| confirm | 日期选择完成后触发,若`show-confirm`为`true`,则点击确认按钮后触发 | _value: Date \| Date[]_ |
+| open | 打开弹出层时触发 | - |
+| close | 关闭弹出层时触发 | - |
+| opened | 打开弹出层且动画结束后触发 | - |
+| closed | 关闭弹出层且动画结束后触发 | - |
+
+### Slots
+
+| 名称 | 说明 |
+| ------ | ------------------ |
+| title | 自定义标题 |
+| footer | 自定义底部区域内容 |
+
+### 方法
+
+通过 selectComponent 可以获取到 Calendar 实例并调用实例方法
+
+| 方法名 | 说明 | 参数 | 返回值 |
+| ------ | ---------------------- | ---- | ------ |
+| reset | 重置选中的日期到默认值 | - | - |
diff --git a/packages/calendar/calendar.wxml b/packages/calendar/calendar.wxml
new file mode 100644
index 00000000..09a60b3e
--- /dev/null
+++ b/packages/calendar/calendar.wxml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/calendar/components/header/index.json b/packages/calendar/components/header/index.json
new file mode 100644
index 00000000..467ce294
--- /dev/null
+++ b/packages/calendar/components/header/index.json
@@ -0,0 +1,3 @@
+{
+ "component": true
+}
diff --git a/packages/calendar/components/header/index.less b/packages/calendar/components/header/index.less
new file mode 100644
index 00000000..c270ac34
--- /dev/null
+++ b/packages/calendar/components/header/index.less
@@ -0,0 +1,39 @@
+@import '../../../common/style/var.less';
+@import '../../../common/style/theme.less';
+
+.van-calendar {
+ &__header {
+ flex-shrink: 0;
+ .theme(box-shadow, '@calendar-header-box-shadow');
+ }
+
+ &__header-title,
+ &__header-subtitle {
+ text-align: center;
+
+ .theme(height, '@calendar-header-title-height');
+ .theme(font-weight, '@font-weight-bold');
+ .theme(line-height, '@calendar-header-title-height');
+ }
+
+ &__header-title:empty,
+ &__header-title + &__header-title {
+ display: none;
+ }
+
+ &__header-title:empty + &__header-title {
+ display: block !important;
+ }
+
+ &__weekdays {
+ display: flex;
+ }
+
+ &__weekday {
+ flex: 1;
+ text-align: center;
+
+ .theme(font-size, '@calendar-weekdays-font-size');
+ .theme(line-height, '@calendar-weekdays-height');
+ }
+}
diff --git a/packages/calendar/components/header/index.ts b/packages/calendar/components/header/index.ts
new file mode 100644
index 00000000..fd126503
--- /dev/null
+++ b/packages/calendar/components/header/index.ts
@@ -0,0 +1,19 @@
+import { VantComponent } from '../../../common/component';
+
+VantComponent({
+ props: {
+ title: {
+ type: String,
+ value: '日期选择'
+ },
+ subtitle: String,
+ showTitle: Boolean,
+ showSubtitle: Boolean
+ },
+
+ data: {
+ weekdays: ['日', '一', '二', '三', '四', '五', '六']
+ },
+
+ methods: {}
+});
diff --git a/packages/calendar/components/header/index.wxml b/packages/calendar/components/header/index.wxml
new file mode 100644
index 00000000..eb8e4b47
--- /dev/null
+++ b/packages/calendar/components/header/index.wxml
@@ -0,0 +1,16 @@
+
diff --git a/packages/calendar/components/month/index.json b/packages/calendar/components/month/index.json
new file mode 100644
index 00000000..467ce294
--- /dev/null
+++ b/packages/calendar/components/month/index.json
@@ -0,0 +1,3 @@
+{
+ "component": true
+}
diff --git a/packages/calendar/components/month/index.less b/packages/calendar/components/month/index.less
new file mode 100644
index 00000000..0be00465
--- /dev/null
+++ b/packages/calendar/components/month/index.less
@@ -0,0 +1,125 @@
+@import '../../../common/style/var';
+@import '../../../common/style/theme.less';
+
+.van-calendar {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ .theme(background-color, '@calendar-background-color');
+
+ &__month-title {
+ text-align: center;
+ .theme(height, '@calendar-header-title-height');
+ .theme(font-weight, '@font-weight-bold');
+ .theme(font-size, '@calendar-month-title-font-size');
+ .theme(line-height, '@calendar-header-title-height');
+ }
+
+ &__days {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ user-select: none;
+ }
+
+ &__month-mark {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 0;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+
+ .theme(color, '@calendar-month-mark-color');
+ .theme(font-size, '@calendar-month-mark-font-size');
+ }
+
+ &__day,
+ &__selected-day {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ }
+
+ &__day {
+ position: relative;
+ width: 14.285%;
+
+ .theme(height, '@calendar-day-height');
+ .theme(font-size, '@calendar-day-font-size');
+
+ &--end,
+ &--start,
+ &--start-end,
+ &--multiple-middle,
+ &--multiple-selected {
+ .theme(color, '@calendar-range-edge-color');
+ .theme(background-color, '@calendar-range-edge-background-color');
+ }
+
+ &--start {
+ .theme(border-radius, '@border-radius-md 0 0 @border-radius-md');
+ }
+
+ &--end {
+ .theme(border-radius, '0 @border-radius-md @border-radius-md 0');
+ }
+
+ &--start-end,
+ &--multiple-selected {
+ .theme(border-radius, '@border-radius-md');
+ }
+
+ &--middle {
+ .theme(color, '@calendar-range-middle-color');
+
+ &::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: currentColor;
+ content: '';
+
+ .theme(opacity, '@calendar-range-middle-background-opacity');
+ }
+ }
+
+ &--disabled {
+ cursor: default;
+
+ .theme(color, '@calendar-day-disabled-color');
+ }
+ }
+
+ &__top-info,
+ &__bottom-info {
+ position: absolute;
+ right: 0;
+ left: 0;
+ .theme(font-size, '@calendar-info-font-size');
+ .theme(line-height, '@calendar-info-line-height');
+
+ @media (max-width: 350px) {
+ font-size: 9px;
+ }
+ }
+
+ &__top-info {
+ top: 6px;
+ }
+
+ &__bottom-info {
+ bottom: 6px;
+ }
+
+ &__selected-day {
+ .theme(width, '@calendar-selected-day-size');
+ .theme(height, '@calendar-selected-day-size');
+ .theme(color, '@calendar-selected-day-color');
+ .theme(background-color, '@calendar-selected-day-background-color');
+ .theme(border-radius, '@border-radius-md');
+ }
+}
diff --git a/packages/calendar/components/month/index.ts b/packages/calendar/components/month/index.ts
new file mode 100644
index 00000000..02a47843
--- /dev/null
+++ b/packages/calendar/components/month/index.ts
@@ -0,0 +1,190 @@
+import { VantComponent } from '../../../common/component';
+import {
+ getMonthEndDay,
+ compareDay,
+ getPrevDay,
+ getNextDay
+} from '../../utils';
+
+VantComponent({
+ props: {
+ date: {
+ type: null,
+ observer: 'setDays'
+ },
+ type: {
+ type: String,
+ observer: 'setDays'
+ },
+ color: String,
+ minDate: {
+ type: null,
+ observer: 'setDays'
+ },
+ maxDate: {
+ type: null,
+ observer: 'setDays'
+ },
+ showMark: Boolean,
+ rowHeight: [Number, String],
+ formatter: {
+ type: null,
+ observer: 'setDays'
+ },
+ currentDate: {
+ type: [null, Array],
+ observer: 'setDays'
+ },
+ allowSameDay: Boolean,
+ showSubtitle: Boolean,
+ showMonthTitle: Boolean
+ },
+
+ data: {
+ visible: true,
+ days: []
+ },
+
+ methods: {
+ onClick(event) {
+ const { index } = event.currentTarget.dataset;
+ const item = this.data.days[index];
+ if (item.type !== 'disabled') {
+ this.$emit('click', item);
+ }
+ },
+
+ setDays() {
+ const days = [];
+ const startDate = new Date(this.data.date);
+ const year = startDate.getFullYear();
+ const month = startDate.getMonth();
+
+ const totalDay = getMonthEndDay(
+ startDate.getFullYear(),
+ startDate.getMonth() + 1
+ );
+
+ for (let day = 1; day <= totalDay; day++) {
+ const date = new Date(year, month, day);
+ const type = this.getDayType(date);
+
+ let config = {
+ date,
+ type,
+ text: day,
+ bottomInfo: this.getBottomInfo(type)
+ };
+
+ if (this.data.formatter) {
+ config = this.data.formatter(config);
+ }
+
+ days.push(config);
+ }
+
+ this.setData({ days });
+ },
+
+ getMultipleDayType(day) {
+ const { currentDate } = this.data;
+ if (!Array.isArray(currentDate)) {
+ return '';
+ }
+
+ const isSelected = date =>
+ currentDate.some(item => compareDay(item, date) === 0);
+
+ if (isSelected(day)) {
+ const prevDay = getPrevDay(day);
+ const nextDay = getNextDay(day);
+ const prevSelected = isSelected(prevDay);
+ const nextSelected = isSelected(nextDay);
+
+ if (prevSelected && nextSelected) {
+ return 'multiple-middle';
+ }
+
+ if (prevSelected) {
+ return 'end';
+ }
+
+ return nextSelected ? 'start' : 'multiple-selected';
+ }
+
+ return '';
+ },
+
+ getRangeDayType(day) {
+ const { currentDate, allowSameDay } = this.data;
+ if (!Array.isArray(currentDate)) {
+ return;
+ }
+
+ const [startDay, endDay] = currentDate;
+
+ if (!startDay) {
+ return;
+ }
+
+ const compareToStart = compareDay(day, startDay);
+
+ if (!endDay) {
+ return compareToStart === 0 ? 'start' : '';
+ }
+
+ const compareToEnd = compareDay(day, endDay);
+
+ if (compareToStart === 0 && compareToEnd === 0 && allowSameDay) {
+ return 'start-end';
+ }
+
+ if (compareToStart === 0) {
+ return 'start';
+ }
+
+ if (compareToEnd === 0) {
+ return 'end';
+ }
+
+ if (compareToStart > 0 && compareToEnd < 0) {
+ return 'middle';
+ }
+ },
+
+ getDayType(day) {
+ const { type, minDate, maxDate, currentDate } = this.data;
+
+ if (compareDay(day, minDate) < 0 || compareDay(day, maxDate) > 0) {
+ return 'disabled';
+ }
+
+ if (type === 'single') {
+ return compareDay(day, currentDate) === 0 ? 'selected' : '';
+ }
+
+ if (type === 'multiple') {
+ return this.getMultipleDayType(day);
+ }
+
+ /* istanbul ignore else */
+ if (type === 'range') {
+ return this.getRangeDayType(day);
+ }
+ },
+
+ getBottomInfo(type) {
+ if (this.data.type === 'range') {
+ if (type === 'start') {
+ return '开始';
+ }
+ if (type === 'end') {
+ return '结束';
+ }
+ if (type === 'start-end') {
+ return '开始/结束';
+ }
+ }
+ }
+ }
+});
diff --git a/packages/calendar/components/month/index.wxml b/packages/calendar/components/month/index.wxml
new file mode 100644
index 00000000..55bab83f
--- /dev/null
+++ b/packages/calendar/components/month/index.wxml
@@ -0,0 +1,39 @@
+
+
+
+
+
+ {{ computed.formatMonthTitle(date) }}
+
+
+
+
+ {{ computed.getMark(date) }}
+
+
+
+
+ {{ item.topInfo }}
+ {{ item.text }}
+
+ {{ item.bottomInfo }}
+
+
+
+
+ {{ item.topInfo }}
+ {{ item.text }}
+
+ {{ item.bottomInfo }}
+
+
+
+
+
diff --git a/packages/calendar/components/month/index.wxs b/packages/calendar/components/month/index.wxs
new file mode 100644
index 00000000..a0570798
--- /dev/null
+++ b/packages/calendar/components/month/index.wxs
@@ -0,0 +1,67 @@
+/* eslint-disable */
+var utils = require('../../utils.wxs');
+
+function getMark(date) {
+ return getDate(date).getMonth() + 1;
+}
+
+var ROW_HEIGHT = 64;
+
+function getDayStyle(type, index, date, rowHeight, color) {
+ var style = [];
+ var offset = getDate(date).getDay();
+
+ if (index === 0) {
+ style.push(['margin-left', (100 * offset) / 7 + '%']);
+ }
+
+ if (rowHeight !== ROW_HEIGHT) {
+ style.push(['height', rowHeight + 'px']);
+ }
+
+ if (color) {
+ if (
+ type === 'start' ||
+ type === 'end' ||
+ type === 'multiple-selected' ||
+ type === 'multiple-middle'
+ ) {
+ style.push(['background', color]);
+ } else if (type === 'middle') {
+ style.push(['color', color]);
+ }
+ }
+
+ return style
+ .map(function(item) {
+ return item.join(':');
+ })
+ .join(';');
+}
+
+function formatMonthTitle(date) {
+ date = getDate(date);
+ return date.getFullYear() + '年' + (date.getMonth() + 1) + '月';
+}
+
+function getMonthStyle(visible, date, rowHeight) {
+ if (!visible) {
+ date = getDate(date);
+
+ var totalDay = utils.getMonthEndDay(
+ date.getFullYear(),
+ date.getMonth() + 1
+ );
+ var offset = getDate(date).getDay();
+ var padding = Math.ceil((totalDay + offset) / 7) * rowHeight;
+
+ return 'padding-bottom:' + padding + 'px';
+ }
+}
+
+module.exports = {
+ getMark: getMark,
+ getDayStyle: getDayStyle,
+ formatMonthTitle: formatMonthTitle,
+ getMonthStyle: getMonthStyle
+};
diff --git a/packages/calendar/index.json b/packages/calendar/index.json
new file mode 100644
index 00000000..61dec086
--- /dev/null
+++ b/packages/calendar/index.json
@@ -0,0 +1,9 @@
+{
+ "component": true,
+ "usingComponents": {
+ "header": "./components/header/index",
+ "month": "./components/month/index",
+ "van-button": "../button/index",
+ "van-popup": "../popup/index"
+ }
+}
diff --git a/packages/calendar/index.less b/packages/calendar/index.less
new file mode 100644
index 00000000..0c3d4cce
--- /dev/null
+++ b/packages/calendar/index.less
@@ -0,0 +1,54 @@
+@import '../common/style/var';
+@import '../common/style/theme.less';
+
+.van-calendar {
+ display: flex;
+ flex-direction: column;
+ .theme(height, '@calendar-height');
+ .theme(background-color, '@calendar-background-color');
+
+ &__close-icon {
+ top: 11px;
+ }
+
+ &__popup--top,
+ &__popup--bottom {
+ .theme(height, '@calendar-popup-height');
+ }
+
+ &__popup--left,
+ &__popup--right {
+ height: 100%;
+ }
+
+ &__body {
+ flex: 1;
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ &__footer {
+ flex-shrink: 0;
+ .theme(padding, '0 @padding-md');
+
+ &--safe-area-inset-bottom {
+ padding-bottom: constant(safe-area-inset-bottom);
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+ }
+
+ &__footer:empty,
+ &__footer + &__footer {
+ display: none;
+ }
+
+ &__footer:empty + &__footer {
+ display: block !important;
+ }
+
+ &__confirm {
+ .theme(height, '@calendar-confirm-button-height') !important;
+ .theme(margin, '@calendar-confirm-button-margin') !important;
+ .theme(line-height, '@calendar-confirm-button-line-height') !important;
+ }
+}
diff --git a/packages/calendar/index.ts b/packages/calendar/index.ts
new file mode 100644
index 00000000..d71bd2eb
--- /dev/null
+++ b/packages/calendar/index.ts
@@ -0,0 +1,302 @@
+import { VantComponent } from '../common/component';
+import {
+ ROW_HEIGHT,
+ getNextDay,
+ compareDay,
+ copyDates,
+ calcDateNum,
+ formatMonthTitle,
+ compareMonth,
+ getMonths
+} from './utils';
+
+import Toast from '../toast/toast';
+
+VantComponent({
+ props: {
+ title: {
+ type: String,
+ value: '日期选择'
+ },
+ color: String,
+ show: {
+ type: Boolean,
+ observer(val) {
+ if (val) {
+ this.initRect();
+ this.scrollIntoView();
+ }
+ }
+ },
+ formatter: null,
+ confirmText: {
+ type: String,
+ value: '确定'
+ },
+ rangePrompt: String,
+ defaultDate: {
+ type: [Number, Array],
+ observer(val) {
+ this.setData({ currentDate: val });
+ this.scrollIntoView();
+ }
+ },
+ allowSameDay: Boolean,
+ confirmDisabledText: String,
+ type: {
+ type: String,
+ value: 'single',
+ observer: 'reset'
+ },
+ minDate: {
+ type: null,
+ value: Date.now()
+ },
+ maxDate: {
+ type: null,
+ value: new Date(
+ new Date().getFullYear(),
+ new Date().getMonth() + 6,
+ new Date().getDate()
+ ).getTime()
+ },
+ position: {
+ type: String,
+ value: 'bottom'
+ },
+ rowHeight: {
+ type: [Number, String],
+ value: ROW_HEIGHT
+ },
+ round: {
+ type: Boolean,
+ value: true
+ },
+ poppable: {
+ type: Boolean,
+ value: true
+ },
+ showMark: {
+ type: Boolean,
+ value: true
+ },
+ showTitle: {
+ type: Boolean,
+ value: true
+ },
+ showConfirm: {
+ type: Boolean,
+ value: true
+ },
+ showSubtitle: {
+ type: Boolean,
+ value: true
+ },
+ safeAreaInsetBottom: {
+ type: Boolean,
+ value: true
+ },
+ closeOnClickOverlay: {
+ type: Boolean,
+ value: true
+ },
+ maxRange: {
+ type: [Number, String],
+ value: null
+ }
+ },
+
+ data: {
+ subtitle: '',
+ currentDate: null,
+ scrollIntoView: ''
+ },
+
+ created() {
+ this.setData({
+ currentDate: this.getInitialDate()
+ });
+ },
+
+ mounted() {
+ if (this.data.show || !this.data.poppable) {
+ this.initRect();
+ this.scrollIntoView();
+ }
+ },
+
+ methods: {
+ reset() {
+ this.setData({ currentDate: this.getInitialDate() });
+ this.scrollIntoView();
+ },
+
+ initRect() {
+ if (this.contentObserver != null) {
+ this.contentObserver.disconnect();
+ }
+
+ const contentObserver = this.createIntersectionObserver({
+ thresholds: [0, 0.1, 0.9, 1],
+ observeAll: true
+ });
+
+ this.contentObserver = contentObserver;
+
+ contentObserver.relativeTo('.van-calendar__body');
+ contentObserver.observe('.month', res => {
+ if (res.boundingClientRect.top <= res.relativeRect.top) {
+ // @ts-ignore
+ this.setData({ subtitle: formatMonthTitle(res.dataset.date) });
+ }
+ });
+ },
+
+ getInitialDate() {
+ const { type, defaultDate, minDate } = this.data;
+
+ if (type === 'range') {
+ const [startDay, endDay] = defaultDate || [];
+ return [
+ startDay || minDate,
+ endDay || getNextDay(new Date(minDate)).getTime()
+ ];
+ }
+
+ if (type === 'multiple') {
+ return [defaultDate || minDate];
+ }
+
+ return defaultDate || minDate;
+ },
+
+ scrollIntoView() {
+ setTimeout(() => {
+ const {
+ currentDate,
+ type,
+ show,
+ poppable,
+ minDate,
+ maxDate
+ } = this.data;
+ const targetDate = type === 'single' ? currentDate : currentDate[0];
+ const displayed = show || !poppable;
+ if (!targetDate || !displayed) {
+ return;
+ }
+
+ const months = getMonths(minDate, maxDate);
+
+ months.some((month, index) => {
+ if (compareMonth(month, targetDate) === 0) {
+ this.setData({ scrollIntoView: `month${index}` });
+ return true;
+ }
+
+ return false;
+ });
+ }, 100);
+ },
+
+ onOpen() {
+ this.$emit('open');
+ },
+
+ onOpened() {
+ this.$emit('opened');
+ },
+
+ onClose() {
+ this.$emit('close');
+ },
+
+ onClosed() {
+ this.$emit('closed');
+ },
+
+ onClickDay(event) {
+ const { date } = event.detail;
+ const { type, currentDate, allowSameDay } = this.data;
+
+ if (type === 'range') {
+ const [startDay, endDay] = currentDate;
+
+ if (startDay && !endDay) {
+ const compareToStart = compareDay(date, startDay);
+
+ if (compareToStart === 1) {
+ this.select([startDay, date], true);
+ } else if (compareToStart === -1) {
+ this.select([date, null]);
+ } else if (allowSameDay) {
+ this.select([date, date]);
+ }
+ } else {
+ this.select([date, null]);
+ }
+ } else if (type === 'multiple') {
+ let selectedIndex: number;
+
+ const selected = currentDate.some((dateItem: number, index: number) => {
+ const equal = compareDay(dateItem, date) === 0;
+ if (equal) {
+ selectedIndex = index;
+ }
+ return equal;
+ });
+
+ if (selected) {
+ currentDate.splice(selectedIndex, 1);
+ this.setData({ currentDate });
+ } else {
+ this.select([...currentDate, date]);
+ }
+ } else {
+ this.select(date, true);
+ }
+ },
+
+ select(date, complete) {
+ const getTime = (date: Date | number) =>
+ (date instanceof Date ? date.getTime() : date);
+
+ this.setData({
+ currentDate: Array.isArray(date) ? date.map(getTime) : getTime(date)
+ });
+ this.$emit('select', copyDates(date));
+
+ if (complete && this.data.type === 'range') {
+ const valid = this.checkRange();
+
+ if (!valid) {
+ return;
+ }
+ }
+
+ if (complete && !this.data.showConfirm) {
+ this.onConfirm();
+ }
+ },
+
+ checkRange() {
+ const { maxRange, currentDate, rangePrompt } = this.data;
+
+ if (maxRange && calcDateNum(currentDate) > maxRange) {
+ Toast(rangePrompt || `选择天数不能超过 ${maxRange} 天`);
+ return false;
+ }
+
+ return true;
+ },
+
+ onConfirm() {
+ if (this.data.type === 'range' && !this.checkRange()) {
+ return;
+ }
+ wx.nextTick(() => {
+ this.$emit('confirm', copyDates(this.data.currentDate));
+ });
+ }
+ }
+});
diff --git a/packages/calendar/index.wxml b/packages/calendar/index.wxml
new file mode 100644
index 00000000..1a4f59c7
--- /dev/null
+++ b/packages/calendar/index.wxml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/calendar/index.wxs b/packages/calendar/index.wxs
new file mode 100644
index 00000000..23df241f
--- /dev/null
+++ b/packages/calendar/index.wxs
@@ -0,0 +1,33 @@
+/* eslint-disable */
+var utils = require('./utils.wxs');
+
+function getMonths(minDate, maxDate) {
+ var months = [];
+ var cursor = getDate(minDate);
+
+ cursor.setDate(1);
+
+ do {
+ months.push(cursor.getTime());
+ cursor.setMonth(cursor.getMonth() + 1);
+ } while (utils.compareMonth(cursor, getDate(maxDate)) !== 1);
+
+ return months;
+}
+
+function getButtonDisabled(type, currentDate) {
+ if (type === 'range') {
+ return !currentDate[0] || !currentDate[1];
+ }
+
+ if (type === 'multiple') {
+ return !currentDate.length;
+ }
+
+ return !currentDate;
+}
+
+module.exports = {
+ getMonths: getMonths,
+ getButtonDisabled: getButtonDisabled
+};
diff --git a/packages/calendar/utils.ts b/packages/calendar/utils.ts
new file mode 100644
index 00000000..57651c6e
--- /dev/null
+++ b/packages/calendar/utils.ts
@@ -0,0 +1,103 @@
+export const ROW_HEIGHT = 64;
+
+export function formatMonthTitle(date: Date) {
+ if (!(date instanceof Date)) {
+ date = new Date(date);
+ }
+ return `${date.getFullYear()}年${date.getMonth() + 1}月`;
+}
+
+export function compareMonth(date1: Date | number, date2: Date | number) {
+ if (!(date1 instanceof Date)) {
+ date1 = new Date(date1);
+ }
+
+ if (!(date2 instanceof Date)) {
+ date2 = new Date(date2);
+ }
+
+ const year1 = date1.getFullYear();
+ const year2 = date2.getFullYear();
+ const month1 = date1.getMonth();
+ const month2 = date2.getMonth();
+
+ if (year1 === year2) {
+ return month1 === month2 ? 0 : month1 > month2 ? 1 : -1;
+ }
+
+ return year1 > year2 ? 1 : -1;
+}
+
+export function compareDay(day1: Date | number, day2: Date | number) {
+ if (!(day1 instanceof Date)) {
+ day1 = new Date(day1);
+ }
+
+ if (!(day2 instanceof Date)) {
+ day2 = new Date(day2);
+ }
+
+ const compareMonthResult = compareMonth(day1, day2);
+
+ if (compareMonthResult === 0) {
+ const date1 = day1.getDate();
+ const date2 = day2.getDate();
+
+ return date1 === date2 ? 0 : date1 > date2 ? 1 : -1;
+ }
+
+ return compareMonthResult;
+}
+
+function getDayByOffset(date: Date, offset: number) {
+ date = new Date(date);
+ date.setDate(date.getDate() + offset);
+
+ return date;
+}
+
+export function getPrevDay(date: Date) {
+ return getDayByOffset(date, -1);
+}
+
+export function getNextDay(date: Date) {
+ return getDayByOffset(date, 1);
+}
+
+export function calcDateNum(date: [Date, Date]) {
+ const day1 = new Date(date[0]).getTime();
+ const day2 = new Date(date[1]).getTime();
+ return (day2 - day1) / (1000 * 60 * 60 * 24) + 1;
+}
+
+export function copyDates(dates: Date | Date[]) {
+ if (Array.isArray(dates)) {
+ return dates.map(date => {
+ if (date === null) {
+ return date;
+ }
+
+ return new Date(date);
+ });
+ }
+
+ return new Date(dates);
+}
+
+export function getMonthEndDay(year: number, month: number): number {
+ return 32 - new Date(year, month - 1, 32).getDate();
+}
+
+export function getMonths(minDate: number, maxDate: number) {
+ const months = [];
+ const cursor = new Date(minDate);
+
+ cursor.setDate(1);
+
+ do {
+ months.push(cursor.getTime());
+ cursor.setMonth(cursor.getMonth() + 1);
+ } while (compareMonth(cursor, maxDate) !== 1);
+
+ return months;
+}
diff --git a/packages/calendar/utils.wxs b/packages/calendar/utils.wxs
new file mode 100644
index 00000000..e57f6b32
--- /dev/null
+++ b/packages/calendar/utils.wxs
@@ -0,0 +1,25 @@
+/* eslint-disable */
+function getMonthEndDay(year, month) {
+ return 32 - getDate(year, month - 1, 32).getDate();
+}
+
+function compareMonth(date1, date2) {
+ date1 = getDate(date1);
+ date2 = getDate(date2);
+
+ var year1 = date1.getFullYear();
+ var year2 = date2.getFullYear();
+ var month1 = date1.getMonth();
+ var month2 = date2.getMonth();
+
+ if (year1 === year2) {
+ return month1 === month2 ? 0 : month1 > month2 ? 1 : -1;
+ }
+
+ return year1 > year2 ? 1 : -1;
+}
+
+module.exports = {
+ getMonthEndDay: getMonthEndDay,
+ compareMonth: compareMonth
+};
diff --git a/packages/common/style/var.less b/packages/common/style/var.less
index 1d373f6c..8a6e2301 100644
--- a/packages/common/style/var.less
+++ b/packages/common/style/var.less
@@ -109,6 +109,35 @@
@button-plain-background-color: @white;
@button-disabled-opacity: @disabled-opacity;
+// Calendar
+@calendar-height: 100%;
+@calendar-background-color: @white;
+@calendar-popup-height: 80%;
+@calendar-header-box-shadow: 0 2px 10px rgba(125, 126, 128, 0.16);
+@calendar-header-title-height: 44px;
+@calendar-header-title-font-size: @font-size-lg;
+@calendar-header-subtitle-font-size: @font-size-md;
+@calendar-weekdays-height: 30px;
+@calendar-weekdays-font-size: @font-size-sm;
+@calendar-month-title-font-size: @font-size-md;
+@calendar-month-mark-color: fade(@gray-2, 80%);
+@calendar-month-mark-font-size: 160px;
+@calendar-day-height: 64px;
+@calendar-day-font-size: @font-size-lg;
+@calendar-range-edge-color: @white;
+@calendar-range-edge-background-color: @red;
+@calendar-range-middle-color: @red;
+@calendar-range-middle-background-opacity: 0.1;
+@calendar-selected-day-size: 54px;
+@calendar-selected-day-color: @white;
+@calendar-info-font-size: @font-size-xs;
+@calendar-info-line-height: 14px;
+@calendar-selected-day-background-color: @red;
+@calendar-day-disabled-color: @gray-5;
+@calendar-confirm-button-height: 36px;
+@calendar-confirm-button-margin: 7px 0;
+@calendar-confirm-button-line-height: 34px;
+
// Card
@card-padding: @padding-xs @padding-md;
@card-font-size: @font-size-sm;
diff --git a/packages/mixins/transition.ts b/packages/mixins/transition.ts
index ecf6dd66..9ddd3fcc 100644
--- a/packages/mixins/transition.ts
+++ b/packages/mixins/transition.ts
@@ -13,16 +13,16 @@ export const transition = function(showDefaultValue: boolean) {
return Behavior({
properties: {
customStyle: String,
- // @ts-ignore
show: {
type: Boolean,
value: showDefaultValue,
+ // @ts-ignore
observer: 'observeShow'
},
- // @ts-ignore
duration: {
type: null,
value: 300,
+ // @ts-ignore
observer: 'observeDuration'
},
name: {
diff --git a/packages/popup/index.ts b/packages/popup/index.ts
index 7fe066c3..7399f532 100644
--- a/packages/popup/index.ts
+++ b/packages/popup/index.ts
@@ -8,7 +8,8 @@ VantComponent({
'enter-to-class',
'leave-class',
'leave-active-class',
- 'leave-to-class'
+ 'leave-to-class',
+ 'close-icon-class'
],
mixins: [transition(false)],
diff --git a/packages/popup/index.wxml b/packages/popup/index.wxml
index a3846862..9b41e5c3 100644
--- a/packages/popup/index.wxml
+++ b/packages/popup/index.wxml
@@ -18,7 +18,7 @@