first commit

This commit is contained in:
lingxiao865
2026-02-10 08:05:03 +08:00
commit c5af079d8c
1094 changed files with 97530 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
:: BASE_DOC ::
## API
### Calendar Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
allow-same-day | Boolean | false | \- | N
auto-close | Boolean | true | \- | N
confirm-btn | String / Object | '' | [see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
first-day-of-week | Number | 0 | \- | N
format | Function | - | Typescript: `CalendarFormatType ` `type CalendarFormatType = (day: TDate) => TDate` `type TDateType = 'selected' \| 'disabled' \| 'start' \| 'start-end' \|'centre' \| 'end' \| ''` `interface TDate { date: Date; day: number; type: TDateType; className?: string; prefix?: string; suffix?: string;}`。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
locale-text | Object | - | Typescript: `CalendarLocaleText` `interface CalendarLocaleText {title?: string; weekdays?: string[]; monthTitle?: string; months?: string[]; confirm?: string;}`。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
max-date | Number | - | \- | N
min-date | Number | - | \- | N
readonly | Boolean | - | \- | N
switch-mode | String | none | options: none/month/year-month | N
title | String | - | \- | N
type | String | single | options: single/multiple/range | N
use-popup | Boolean | true | \- | N
using-custom-navbar | Boolean | false | \- | N
value | Number / Array | - | `v-model:value` is supported。Typescript: `number \| number[]` | N
default-value | Number / Array | - | uncontrolled property。Typescript: `number \| number[]` | N
visible | Boolean | false | \- | N
### Calendar Events
name | params | description
-- | -- | --
change | `(context: { value: number \| number[] })` | \-
close | `(context: { trigger: CalendarTrigger })` | [see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts)。<br/>`type CalendarTrigger = 'close-btn' \| 'confirm-btn' \| 'overlay' \| 'auto-close'`<br/>
confirm | `(context: { value: number \| number[] })` | \-
panel-change | `(context: { year: number, month: number })` | \-
scroll | `(context: {scrollLeft: number, scrollTop: number, scrollHeight: number, scrollWidth: number, deltaX: number, deltaY: number})` | triggered when scrolling
select | `(context: { value: number \| number[] })` | \-
### Calendar Slots
name | Description
-- | --
confirm-btn | \-
title | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-calendar-active-color | @brand-color | -
--td-calendar-bg-color | @bg-color-container | -
--td-calendar-days-color | @text-color-secondary | -
--td-calendar-item-centre-color | @brand-color-light | -
--td-calendar-item-disabled-color | @text-color-disabled | -
--td-calendar-item-suffix-color | @text-color-placeholder | -
--td-calendar-radius | 24rpx | -
--td-calendar-selected-border-radius | @radius-default | -
--td-calendar-selected-color | @text-color-anti | -
--td-calendar-switch-mode-icon-color | @text-color-secondary | -
--td-calendar-switch-mode-icon-disabled-color | @text-color-disabled | -
--td-calendar-title-color | @text-color-primary | -
--td-calendar-title-font | @font-title-large | -

View File

@@ -0,0 +1,113 @@
---
title: Calendar 日历
description: 按照日历形式展示数据或日期的容器。
spline: form
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TCalendar from '@tdesign/uniapp/calendar/calendar.vue';
```
### 组件类型
#### 单个选择日历
{{ base }}
#### 多个选择日历
{{ multiple }}
#### 带单行/双行描述的日历
{{ custom-text }}
#### 带翻页功能的日历
{{ switch-mode }}
#### 可选择区间日期的日历
{{ range }}
### 组件样式
#### 国际化
{{ local-text }}
#### 含不可选的日历
{{ custom-range }}
#### 不使用 Popup
{{ without-popup }}
## API
### Calendar Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
allow-same-day | Boolean | false | 是否允许区间选择日历的起止时间相同,仅当 `type='range'` 时有效 | N
auto-close | Boolean | true | 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭,不需要手动设置 visible | N
confirm-btn | String / Object | '' | 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
first-day-of-week | Number | 0 | 第一天从星期几开始,默认 0 = 周日 | N
format | Function | - | 用于格式化日期的函数。TS 类型:`CalendarFormatType ` `type CalendarFormatType = (day: TDate) => TDate` `type TDateType = 'selected' \| 'disabled' \| 'start' \| 'start-end' \|'centre' \| 'end' \| ''` `interface TDate { date: Date; day: number; type: TDateType; className?: string; prefix?: string; suffix?: string;}`。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
locale-text | Object | - | 国际化文案。TS 类型:`CalendarLocaleText` `interface CalendarLocaleText {title?: string; weekdays?: string[]; monthTitle?: string; months?: string[]; confirm?: string;}`。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts) | N
max-date | Number | - | 最大可选的日期,不传则默认半年后 | N
min-date | Number | - | 最小可选的日期,不传则默认今天 | N
readonly | Boolean | - | 是否只读,只读状态下不能选择日期 | N
switch-mode | String | none | 切换模式。 `none` 表示平铺展示所有月份; `month` 表示支持按月切换, `year-month` 表示既按年切换也支持按月切换。可选项none/month/year-month | N
title | String | - | 标题,不传默认为“请选择日期” | N
type | String | single | 日历的选择类型single = 单选multiple = 多选; range = 区间选择。可选项single/multiple/range | N
use-popup | Boolean | true | 是否使用弹出层包裹日历 | N
using-custom-navbar | Boolean | false | 是否使用了自定义导航栏 | N
value | Number / Array | - | 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组。支持语法糖 `v-model:value`。TS 类型:`number \| number[]` | N
default-value | Number / Array | - | 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组。非受控属性。TS 类型:`number \| number[]` | N
visible | Boolean | false | 是否显示日历;`usePopup` 为 true 时有效 | N
### Calendar Events
名称 | 参数 | 描述
-- | -- | --
change | `(context: { value: number \| number[] })` | 不显示 confirm-btn 时,完成选择时触发(暂不支持 type = multiple
close | `(context: { trigger: CalendarTrigger })` | 关闭按钮时触发。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/calendar/type.ts)。<br/>`type CalendarTrigger = 'close-btn' \| 'confirm-btn' \| 'overlay' \| 'auto-close'`<br/>
confirm | `(context: { value: number \| number[] })` | 点击确认按钮时触发
panel-change | `(context: { year: number, month: number })` | 切换月或年时触发switch-mode 不为 none 时有效)
scroll | `(context: {scrollLeft: number, scrollTop: number, scrollHeight: number, scrollWidth: number, deltaX: number, deltaY: number})` | 滚动时触发
select | `(context: { value: number \| number[] })` | 点击日期时触发
### Calendar Slots
名称 | 描述
-- | --
confirm-btn | 确认按钮
title | 标题
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-calendar-active-color | @brand-color | -
--td-calendar-bg-color | @bg-color-container | -
--td-calendar-days-color | @text-color-secondary | -
--td-calendar-item-centre-color | @brand-color-light | -
--td-calendar-item-disabled-color | @text-color-disabled | -
--td-calendar-item-suffix-color | @text-color-placeholder | -
--td-calendar-radius | 24rpx | -
--td-calendar-selected-border-radius | @radius-default | -
--td-calendar-selected-color | @text-color-anti | -
--td-calendar-switch-mode-icon-color | @text-color-secondary | -
--td-calendar-switch-mode-icon-disabled-color | @text-color-disabled | -
--td-calendar-title-color | @text-color-primary | -
--td-calendar-title-font | @font-title-large | -

View File

@@ -0,0 +1,42 @@
export default {
tClass: {
type: String,
default: '',
},
classPrefix: {
type: String,
default: '',
},
switchMode: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
preYearBtnDisable: {
type: Boolean,
},
prevMonthBtnDisable: {
type: Boolean,
},
nextYearBtnDisable: {
type: Boolean,
},
nextMonthBtnDisable: {
type: Boolean,
},
tId: {
type: String,
default: '',
},
// realLocalText: {
// type: Object,
// default: () => ({}),
// },
// currentMonth: {
// type: Array,
// default: () => ([]),
// },
};

View File

@@ -0,0 +1,98 @@
<template>
<view
:id="tId"
:class="' ' + tClass + ' ' + classPrefix + ' '+(switchMode !== 'none' ? classPrefix + '__with-action' : '')"
>
<view
v-if="switchMode !== 'none'"
:class="classPrefix + '__action'"
>
<view
v-if="switchMode === 'year-month'"
:class="utils.cls(classPrefix + '__icon', [['disabled', preYearBtnDisable]])"
:data-disabled="preYearBtnDisable"
data-type="pre-year"
@click="handleSwitchModeChange"
>
<t-icon name="chevron-left-double" />
</view>
<view
:class="utils.cls(classPrefix + '__icon', [['disabled', prevMonthBtnDisable]])"
:data-disabled="prevMonthBtnDisable"
data-type="pre-month"
@click="handleSwitchModeChange"
>
<t-icon name="chevron-left" />
</view>
</view>
<view :class="classPrefix + '__title'">
{{ title }}
</view>
<view
v-if="switchMode !== 'none'"
:class="classPrefix + '__action'"
>
<view
:class="utils.cls(classPrefix + '__icon', [['disabled', nextMonthBtnDisable]])"
:data-disabled="nextMonthBtnDisable"
data-type="next-month"
@click="handleSwitchModeChange"
>
<t-icon name="chevron-right" />
</view>
<view
v-if="switchMode === 'year-month'"
:class="utils.cls(classPrefix + '__icon', [['disabled', nextYearBtnDisable]])"
:data-disabled="nextYearBtnDisable"
data-type="next-year"
@click="handleSwitchModeChange"
>
<t-icon name="chevron-right-double" />
</view>
</view>
</view>
</template>
<script>
import TIcon from '../icon/icon.vue';
import utils from '../common/utils.wxs';
import props from './calendar-header.props';
import { getMonthTitle } from './computed';
export default {
name: 'TCalendarHeader',
options: {
styleIsolation: 'shared',
},
components: {
TIcon,
},
props: {
...props,
},
emits: [
'handleSwitchModeChange',
],
data() {
return {
utils,
};
},
watch: {
},
mounted() {
},
methods: {
handleSwitchModeChange(...args) {
this.$emit('handleSwitchModeChange', ...args);
},
getMonthTitle,
},
};
</script>
<style scoped>
@import './calendar.css';
</style>

View File

@@ -0,0 +1,205 @@
.t-calendar {
width: inherit;
position: relative;
z-index: 9999;
background: var(--td-calendar-bg-color, var(--td-bg-color-container, var(--td-font-white-1, #ffffff)));
overflow-x: hidden;
}
.t-calendar--popup {
border-top-left-radius: var(--td-calendar-radius, 24rpx);
border-top-right-radius: var(--td-calendar-radius, 24rpx);
}
.t-calendar__title {
display: flex;
align-items: center;
justify-content: center;
font: var(--td-calendar-title-font, var(--td-font-title-large, 600 36rpx / 52rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
color: var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
padding: var(--td-spacer-2, 32rpx);
}
.t-calendar__title:focus {
outline: 0;
}
.t-calendar__close-btn {
position: absolute;
top: var(--td-spacer-2, 32rpx);
right: var(--td-spacer-2, 32rpx);
margin: calc(-1 * var(--td-spacer-1, 24rpx));
padding: var(--td-spacer-1, 24rpx);
color: var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
}
.t-calendar__days {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-column-gap: 8rpx;
padding: 0 32rpx;
text-align: center;
line-height: 92rpx;
}
.t-calendar__days-item {
height: 92rpx;
font-size: 28rpx;
color: var(--td-calendar-days-color, var(--td-text-color-secondary, var(--td-font-gray-2, rgba(0, 0, 0, 0.6))));
}
.t-calendar__content {
min-height: 400rpx;
display: flex;
flex-direction: column;
}
.t-calendar__month {
font: var(--td-font-title-small, 600 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
color: var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
padding: 32rpx 0 0;
}
.t-calendar__months {
height: 712rpx;
padding: 0 32rpx 32rpx;
box-sizing: border-box;
}
.t-calendar__months::-webkit-scrollbar {
display: none;
}
.t-calendar__dates {
flex: 1;
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-column-gap: 8rpx;
}
.t-calendar__dates-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
font: var(--td-font-title-medium, 600 32rpx / 48rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
border-radius: var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx));
height: 120rpx;
margin-top: 16rpx;
color: var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
cursor: pointer;
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
user-select: none;
}
.t-calendar__dates-item-prefix,
.t-calendar__dates-item-suffix {
position: absolute;
font: var(--td-font-body-extraSmall, 20rpx / 32rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
width: 100%;
text-align: center;
}
.t-calendar__dates-item-prefix {
top: 8rpx;
}
.t-calendar__dates-item-suffix {
bottom: 8rpx;
color: var(--td-calendar-item-suffix-color, var(--td-text-color-placeholder, var(--td-font-gray-3, rgba(0, 0, 0, 0.4))));
}
.t-calendar__dates-item-suffix--selected,
.t-calendar__dates-item-suffix--start,
.t-calendar__dates-item-suffix--end {
color: var(--td-calendar-selected-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
}
.t-calendar__dates-item-suffix--disabled {
color: var(--td-calendar-item-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-calendar__dates-item--selected,
.t-calendar__dates-item--start-end,
.t-calendar__dates-item--start,
.t-calendar__dates-item--end {
background: var(--td-calendar-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
color: var(--td-calendar-selected-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
border-radius: var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx));
}
.t-calendar__dates-item--start {
border-radius: var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx)) 0 0 var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx));
}
.t-calendar__dates-item--end {
border-radius: 0 var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx)) var(--td-calendar-selected-border-radius, var(--td-radius-default, 12rpx)) 0;
}
.t-calendar__dates-item--start + .t-calendar__dates-item--end::before {
content: '';
display: block;
position: absolute;
top: 0;
width: 8rpx;
height: 100%;
background: var(--td-calendar-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
}
.t-calendar__dates-item--start + .t-calendar__dates-item--end:before {
left: -8rpx;
}
.t-calendar__dates-item--centre {
border-radius: 0;
background-color: var(--td-calendar-item-centre-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-calendar__dates-item--centre::before,
.t-calendar__dates-item--centre::after {
content: '';
display: block;
position: absolute;
top: 0;
width: 8rpx;
height: 100%;
background-color: var(--td-calendar-item-centre-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
}
.t-calendar__dates-item--centre:before {
left: -8rpx;
}
.t-calendar__dates-item--centre:after {
right: -8rpx;
}
.t-calendar__dates-item--disabled {
color: var(--td-calendar-item-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
cursor: default;
}
.t-calendar__footer {
padding: 32rpx;
}
.t-calendar-switch-mode--none > .t-calendar__months {
height: 60vh;
}
.t-calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.t-calendar-header__with-action {
padding: 0rpx 32rpx 16rpx 32rpx;
box-sizing: border-box;
position: relative;
}
.t-calendar-header__with-action::after {
content: '';
display: block;
position: absolute;
top: unset;
bottom: 0;
left: unset;
right: unset;
background-color: var(--td-border-color, var(--td-gray-color-3, #e7e7e7));
}
.t-calendar-header__with-action::after {
height: 1px;
left: 0;
right: 0;
transform: scaleY(0.5);
}
.t-calendar-header__with-action .t-calendar-header__title {
flex: 1;
text-align: center;
font: var(--td-font-title-small, 600 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
}
.t-calendar-header__action {
display: flex;
font-size: 40rpx;
color: var(--td-calendar-switch-mode-icon-color, var(--td-text-color-secondary, var(--td-font-gray-2, rgba(0, 0, 0, 0.6))));
}
.t-calendar-header__icon {
padding: 16rpx;
}
.t-calendar-header__icon--disabled {
color: var(--td-calendar-switch-mode-icon-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, 0.26))));
}
.t-calendar-header__title {
text-align: left;
}

View File

@@ -0,0 +1,454 @@
<template>
<view>
<t-popup
v-if="usePopup"
:visible="visible"
:using-custom-navbar="usingCustomNavbar"
:custom-navbar-height="customNavbarHeight"
placement="bottom"
@visible-change="onVisibleChange"
>
<CalendarTemplate
:class-prefix="classPrefix"
:use-popup="usePopup"
:switch-mode="switchMode"
:t-class="tClass"
:custom-style="tools._style([customStyle])"
:title="title"
:real-local-text="realLocalText"
:months="months"
:current-month="currentMonth"
:action-buttons="actionButtons"
:days="days"
:scroll-into-view="scrollIntoView"
:first-day-of-week="firstDayOfWeek"
:inner-confirm-btn="innerConfirmBtn"
@scroll="onScroll"
@close="handleClose"
@select="handleSelect"
@clickButton="onTplButtonTap"
@handleSwitchModeChange="handleSwitchModeChange"
>
<template #confirm-btn>
<slot name="confirm-btn" />
</template>
<template #title>
<slot name="title" />
</template>
</CalendarTemplate>
</t-popup>
<block v-else>
<CalendarTemplate
:class-prefix="classPrefix"
:use-popup="usePopup"
:switch-mode="switchMode"
:t-class="tClass"
:custom-style="tools._style([customStyle])"
:title="title"
:real-local-text="realLocalText"
:months="months"
:current-month="currentMonth"
:action-buttons="actionButtons"
:days="days"
:scroll-into-view="scrollIntoView"
:first-day-of-week="firstDayOfWeek"
:inner-confirm-btn="innerConfirmBtn"
@scroll="onScroll"
@close="handleClose"
@select="handleSelect"
@clickButton="onTplButtonTap"
@handleSwitchModeChange="handleSwitchModeChange"
>
<template #confirm-btn>
<slot name="confirm-btn" />
</template>
<template #title>
<slot name="title" />
</template>
</CalendarTemplate>
</block>
</view>
</template>
<script>
import TPopup from '../popup/popup';
import TButton from '../button/button';
import TIcon from '../icon/icon';
import CalendarTemplate from './template.vue';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import { coalesce } from '../common/utils';
import props from './props';
import TCalendar from '../common/shared/calendar/index';
import useCustomNavbar from '../mixins/using-custom-navbar';
import { getPrevMonth, getPrevYear, getNextMonth, getNextYear } from './utils';
import tools from '../common/utils.wxs';
import {
getMonthTitle,
getDateLabel,
isDateSelected,
} from './computed.js';
const name = `${prefix}-calendar`;
const defaultLocaleText = {
title: '请选择日期',
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
monthTitle: '{year} 年 {month}',
months: ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月'],
confirm: '确认',
};
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [
{
key: 'value',
event: 'confirm',
},
{
key: 'value',
event: 'change',
},
],
externalClasses: [
`${prefix}-class`,
],
mixins: [useCustomNavbar],
components: {
TPopup,
TButton,
TIcon,
CalendarTemplate,
},
props: {
...props,
},
emits: [
'update:visible',
],
data() {
return {
prefix,
classPrefix: name,
months: [],
scrollIntoView: '',
innerConfirmBtn: {},
realLocalText: {},
currentMonth: {},
actionButtons: {
preYearBtnDisable: false,
prevMonthBtnDisable: false,
nextMonthBtnDisable: false,
nextYearBtnDisable: false,
},
tools,
dataVisible: this.visible,
dataValue: coalesce(this.value, this.defaultValue),
days: [],
};
},
watch: {
type: {
handler(v) {
this.base.type = v;
},
},
allowSameDay(v) {
this.base.allowSameDay = v;
},
confirmBtn: {
handler(v) {
if (typeof v === 'string') {
this.innerConfirmBtn = v === 'slot' ? 'slot' : { content: v };
} else if (typeof v === 'object') {
this.innerConfirmBtn = v;
}
},
immediate: true,
},
firstDayOfWeek: 'onWatchMinMaxDate',
minDate: 'onWatchMinMaxDate',
maxDate: 'onWatchMinMaxDate',
value: {
handler(v) {
this.dataValue = v;
},
immediate: true,
deep: true,
},
visible: {
handler(v) {
this.dataVisible = v;
},
immediate: true,
},
dataValue: {
handler(v) {
this.base.value = v;
this.calcMonths();
this.updateCurrentMonth(Array.isArray(v) ? v[0] : v);
},
deep: true,
},
dataVisible: {
handler(v) {
if (v) {
this.onScrollIntoView();
this.base.value = this.dataValue;
this.calcMonths();
}
},
immediate: true,
},
format: {
handler(v) {
const { usePopup, dataVisible: visible } = this;
if (this.base) {
this.base.format = v;
}
if (!usePopup || visible) {
this.calcMonths();
}
},
immediate: true,
},
},
created() {
const values = Object.keys(props).reduce((acc, key) => ({
...acc,
[key]: this[key],
}));
this.base = new TCalendar(values);
},
mounted() {
const realLocalText = { ...defaultLocaleText, ...this.localeText };
this.initialValue();
this.onWatchMinMaxDate();
this.days = this.base.getDays(realLocalText.weekdays);
this.realLocalText = realLocalText;
this.calcMonths();
this.updateCurrentMonth();
if (!this.usePopup) {
this.onScrollIntoView();
}
},
methods: {
getMonthTitle,
getDateLabel,
isDateSelected,
initialValue() {
const { dataValue: value, type, minDate } = this;
if (!value) {
const today = new Date();
const now = minDate || new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime(); // 获取 0 点的时间戳
const initialValue = type === 'single' ? now : [now];
if (type === 'range') {
initialValue[1] = now + 24 * 3600 * 1000; // 第二天
}
this.dataValue = initialValue;
this.base.value = initialValue;
}
},
onScrollIntoView() {
const { dataValue: value } = this;
if (!value) return;
const date = new Date(Array.isArray(value) ? value[0] : value);
if (date) {
this.scrollIntoView = `year_${date.getFullYear()}_month_${date.getMonth()}`;
}
},
getCurrentYearAndMonth(v) {
const date = new Date(v);
return { year: date.getFullYear(), month: date.getMonth() };
},
updateActionButton(value) {
const _min = this.getCurrentYearAndMonth(this.base.minDate);
const _max = this.getCurrentYearAndMonth(this.base.maxDate);
const _value = this.getCurrentYearAndMonth(value);
const _minTimestamp = new Date(_min.year, _min.month, 1).getTime();
const _maxTimestamp = new Date(_max.year, _max.month, 1).getTime();
const _dateValue = new Date(_value.year, _value.month, 1);
const _prevYearTimestamp = getPrevYear(_dateValue).getTime();
const _prevMonthTimestamp = getPrevMonth(_dateValue).getTime();
const _nextMonthTimestamp = getNextMonth(_dateValue).getTime();
const _nextYearTimestamp = getNextYear(_dateValue).getTime();
const preYearBtnDisable = _prevYearTimestamp < _minTimestamp || _prevMonthTimestamp < _minTimestamp;
const prevMonthBtnDisable = _prevMonthTimestamp < _minTimestamp;
const nextYearBtnDisable = _nextMonthTimestamp > _maxTimestamp || _nextYearTimestamp > _maxTimestamp;
const nextMonthBtnDisable = _nextMonthTimestamp > _maxTimestamp;
this.actionButtons = {
preYearBtnDisable,
prevMonthBtnDisable,
nextYearBtnDisable,
nextMonthBtnDisable,
};
},
updateCurrentMonth(newValue) {
if (this.switchMode === 'none') return;
this.calcCurrentMonth(newValue);
},
calcCurrentMonth(newValue) {
const date = newValue || this.getCurrentDate();
const { year, month } = this.getCurrentYearAndMonth(date);
const currentMonth = this.months.filter(item => item.year === year && item.month === month);
this.updateActionButton(date);
this.currentMonth = currentMonth.length > 0 ? currentMonth : [this.months[0]];
},
calcMonths() {
if (!this.base) return;
const months = this.base.getMonths();
this.months = months;
},
close(trigger) {
if (this.autoClose) {
this.$emit('update:visible', false);
this.dataVisible = false;
}
this.$emit('close', { trigger });
},
onVisibleChange() {
this.close('overlay');
},
handleClose() {
this.close('close-btn');
},
handleSelect(e) {
const { readonly } = this;
const { date, year, month } = e.currentTarget.dataset;
if (date.type === 'disabled' || readonly) return;
const rawValue = this.base.select({ cellType: date.type, year, month, date: date.day });
const value = this.toTime(rawValue);
this.calcMonths();
this.updateCurrentMonth();
if (this.confirmBtn == null) {
// 不显示确认按钮,则选择完即关闭 popup
if (this.type === 'single' || rawValue.length === 2) {
this.dataVisible = false;
this._trigger('change', { value }); // 受控
}
}
this.$emit('select', { value });
},
onTplButtonTap() {
const rawValue = this.base.getTrimValue();
const value = this.toTime(rawValue);
this.close('confirm-btn');
this._trigger('confirm', { value });
},
toTime(val) {
if (!val) return null;
if (Array.isArray(val)) {
return val.map(item => item.getTime());
}
return val.getTime();
},
onScroll(e) {
this.$emit('scroll', e.detail);
},
getCurrentDate() {
let time = Array.isArray(this.base.value) ? this.base.value[0] : this.base.value;
if (this.currentMonth.length > 0) {
const year = this.currentMonth[0]?.year;
const month = this.currentMonth[0]?.month;
time = new Date(year, month, 1).getTime();
}
return time;
},
handleSwitchModeChange(e) {
const { type, disabled } = e.currentTarget.dataset;
if (disabled) return;
const date = this.getCurrentDate();
const funcMap = {
'pre-year': () => getPrevYear(date),
'pre-month': () => getPrevMonth(date),
'next-month': () => getNextMonth(date),
'next-year': () => getNextYear(date),
};
const newValue = funcMap[type]();
if (!newValue) return;
const { year, month } = this.getCurrentYearAndMonth(newValue);
this.$emit('panel-change', { year, month: month + 1 });
this.calcCurrentMonth(newValue);
},
onWatchMinMaxDate() {
const { firstDayOfWeek, minDate, maxDate } = this;
firstDayOfWeek && (this.base.firstDayOfWeek = firstDayOfWeek);
minDate && (this.base.minDate = minDate);
maxDate && (this.base.maxDate = maxDate);
this.calcMonths();
},
},
});
</script>
<style scoped>
@import './calendar.css';
</style>
<style scoped>
.t-calendar-switch-mode--none > .t-calendar__months {
/* support mp-alipay */
width: 100%;
}
</style>

View File

@@ -0,0 +1,42 @@
import { getRegExp } from '../common/runtime/wxs-polyfill';
export function isDateSelected(dateItem) {
return ['start', 'end', 'selected', 'centre'].indexOf(dateItem.type) >= 0;
}
export function getMonthTitle(year, month, pattern = '') {
// prettier-ignore
const REGEXP = getRegExp('\\{year\\}|\\{month\\}', 'g');
return pattern.replace(REGEXP, (match) => {
const replacements = {
'{year}': year,
'{month}': month < 10 ? `0${month}` : month,
};
return replacements[match] || match;
});
}
export function getDateLabel(monthItem, dateItem) {
const weekdayText = ['日', '一', '二', '三', '四', '五', '六'];
const weekday = (monthItem.weekdayOfFirstDay + dateItem.day - 1) % 7;
let label = `${monthItem.month + 1}${dateItem.day}日, 星期${weekdayText[weekday]}`;
if (dateItem.type === 'start') {
label = `开始日期:${label}`;
}
if (dateItem.type === 'end') {
label = `结束日期:${label}`;
}
if (isDateSelected(dateItem)) {
label = `已选中, ${label}`;
}
if (dateItem.prefix) {
label += `, ${dateItem.prefix}`;
}
if (dateItem.suffix) {
label += `, ${dateItem.suffix}`;
}
return label;
}

View File

@@ -0,0 +1,113 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdCalendarProps } from './type';
export default {
/** 是否允许区间选择日历的起止时间相同,仅当 `type='range'` 时有效 */
allowSameDay: Boolean,
/** 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭,不需要手动设置 visible */
autoClose: {
type: Boolean,
default: true,
},
/** 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性 */
confirmBtn: {
type: [String, Object],
default: '' as TdCalendarProps['confirmBtn'],
},
/** 第一天从星期几开始,默认 0 = 周日 */
firstDayOfWeek: {
type: Number,
default: 0,
},
/** 用于格式化日期的函数 */
format: {
type: Function,
},
/** 国际化文案 */
localeText: {
type: Object,
},
/** 最大可选的日期,不传则默认半年后 */
maxDate: {
type: Number,
},
/** 最小可选的日期,不传则默认今天 */
minDate: {
type: Number,
},
/** 是否只读,只读状态下不能选择日期 */
readonly: Boolean,
/** 切换模式。 `none` 表示平铺展示所有月份; `month` 表示支持按月切换, `year-month` 表示既按年切换,也支持按月切换 */
switchMode: {
type: String,
default: 'none' as TdCalendarProps['switchMode'],
validator(val: TdCalendarProps['switchMode']): boolean {
if (!val) return true;
return ['none', 'month', 'year-month'].includes(val);
},
},
/** 标题,不传默认为“请选择日期” */
title: {
type: String,
},
/** 日历的选择类型single = 单选multiple = 多选; range = 区间选择 */
type: {
type: String,
default: 'single' as TdCalendarProps['type'],
validator(val: TdCalendarProps['type']): boolean {
if (!val) return true;
return ['single', 'multiple', 'range'].includes(val);
},
},
/** 是否使用弹出层包裹日历 */
usePopup: {
type: Boolean,
default: true,
},
/** 是否使用了自定义导航栏 */
usingCustomNavbar: Boolean,
/** 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组 */
value: {
type: [Number, Array],
},
/** 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组,非受控属性 */
defaultValue: {
type: [Number, Array],
},
/** 是否显示日历;`usePopup` 为 true 时有效 */
visible: Boolean,
/** 不显示 confirm-btn 时,完成选择时触发(暂不支持 type = multiple */
onChange: {
type: Function,
default: () => ({}),
},
/** 关闭按钮时触发 */
onClose: {
type: Function,
default: () => ({}),
},
/** 点击确认按钮时触发 */
onConfirm: {
type: Function,
default: () => ({}),
},
/** 切换月或年时触发switch-mode 不为 none 时有效) */
onPanelChange: {
type: Function,
default: () => ({}),
},
/** 滚动时触发 */
onScroll: {
type: Function,
default: () => ({}),
},
/** 点击日期时触发 */
onSelect: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,57 @@
export default {
classPrefix: {
type: String,
default: '',
},
usePopup: {
type: Boolean,
},
switchMode: {
type: String,
default: '',
},
tClass: {
type: String,
default: '',
},
customStyle: {
type: [String, Object],
default: '',
},
title: {
type: String,
default: '',
},
realLocalText: {
type: Object,
default: () => ({}),
},
months: {
type: Array,
default: () => ([]),
},
currentMonth: {
type: [Array, Object],
default: () => ([]),
},
actionButtons: {
type: Object,
default: () => ({}),
},
days: {
type: Array,
default: () => ([]),
},
scrollIntoView: {
type: String,
default: '',
},
firstDayOfWeek: {
type: Number,
default: 0,
},
innerConfirmBtn: {
type: [Object, String],
default: '',
},
};

View File

@@ -0,0 +1,261 @@
<template>
<view
:class="utils.cls(classPrefix, [['popup', usePopup]]) + ' ' + classPrefix + '-switch-mode--' + switchMode + ' ' + tClass"
:style="customStyle"
>
<view
:class="classPrefix + '__title'"
tabindex="0"
>
<slot name="title" />
<text v-if="title || realLocalText.title">
{{ title || realLocalText.title }}
</text>
</view>
<t-icon
v-if="usePopup"
name="close"
:t-class="classPrefix + '__close-btn'"
size="48rpx"
aria-role="button"
aria-label="关闭"
:custom-style="closeBtnCustomStyle"
@click="handleClose"
/>
<block
v-if="switchMode !== 'none'"
name="calendar-header"
>
<calendar-header
:class-prefix="classPrefix + '-header'"
:switch-mode="switchMode"
:title="getMonthTitle(
currentMonth[0] && currentMonth[0].year,
realLocalText.months && currentMonth[0] && realLocalText.months[currentMonth[0].month],
realLocalText.monthTitle
)"
:pre-year-btn-disable="actionButtons.preYearBtnDisable"
:prev-month-btn-disable="actionButtons.prevMonthBtnDisable"
:next-year-btn-disable="actionButtons.nextYearBtnDisable"
:next-month-btn-disable="actionButtons.nextMonthBtnDisable"
@handleSwitchModeChange="handleSwitchModeChange"
/>
</block>
<view
aria-hidden
:class="classPrefix + '__days'"
>
<view
v-for="(item, index) in days"
:key="index"
:class="classPrefix + '__days-item'"
>
{{ item }}
</view>
</view>
<scroll-view
:class="classPrefix + '__months'"
:scroll-into-view="scrollIntoView"
scroll-y
enhanced
:show-scrollbar="false"
@scroll="onScroll"
>
<block
v-for="(item, index) in switchMode === 'none' ? months : currentMonth"
:key="index"
>
<block
v-if="switchMode === 'none'"
name="calendar-header"
>
<calendar-header
:t-class="classPrefix + '__month'"
:class-prefix="classPrefix + '-header'"
:switch-mode="switchMode"
:t-id="'year_' + item.year + '_month_' + item.month"
:title="getMonthTitle(item.year, realLocalText.months && realLocalText.months[item.month], realLocalText.monthTitle)"
:pre-year-btn-disable="actionButtons.preYearBtnDisable"
:prev-month-btn-disable="actionButtons.prevMonthBtnDisable"
:next-year-btn-disable="actionButtons.nextYearBtnDisable"
:next-month-btn-disable="actionButtons.nextMonthBtnDisable"
@handleSwitchModeChange="handleSwitchModeChange"
/>
</block>
<view :class="classPrefix + '__dates'">
<view
v-for="(item, index1) in (item.weekdayOfFirstDay - firstDayOfWeek + 7) % 7"
:key="index1"
/>
<block
v-for="(dateItem, dateIndex) in item.months"
:key="dateIndex"
>
<view
:class="classPrefix + '__dates-item ' + dateItem.className + ' ' + classPrefix + '__dates-item--' + dateItem.type"
:data-year="item.year"
:data-month="item.month"
:data-date="dateItem"
aria-role="button"
:aria-label="getDateLabel(item, dateItem)"
:aria-disabled="dateItem.type === 'disabled'"
@click="handleSelect"
>
<view
v-if="dateItem.prefix"
:class="classPrefix + '__dates-item-prefix'"
>
{{ dateItem.prefix }}
</view>
{{ dateItem.day }}
<view
v-if="dateItem.suffix"
:class="classPrefix + '__dates-item-suffix ' + classPrefix + '__dates-item-suffix--' + dateItem.type"
>
{{ dateItem.suffix }}
</view>
</view>
</block>
</view>
</block>
</scroll-view>
<view
v-if="innerConfirmBtn != null && usePopup"
:class="classPrefix + '__footer'"
>
<slot
v-if="innerConfirmBtn === 'slot'"
name="confirm-btn"
/>
<block v-else-if="innerConfirmBtn">
<t-button
:t-id="innerConfirmBtn.tId"
:custom-style="innerConfirmBtn.style"
:block="coalesce(innerConfirmBtn.block, true)"
:t-class="coalesce(coalesce(innerConfirmBtn.tClass, prefix + '-class-action'))"
:class="prefix + '-calendar__confirm-btn'"
:disabled="innerConfirmBtn.disabled"
:data-type="'action'"
:data-extra="innerConfirmBtn.dataExtra"
:custom-dataset="innerConfirmBtn.customDataset"
:content="innerConfirmBtn.content || realLocalText.confirm"
:icon="innerConfirmBtn.icon"
:loading="innerConfirmBtn.loading"
:loading-props="innerConfirmBtn.loadingProps"
:theme="coalesce(innerConfirmBtn.theme, 'primary')"
:ghost="innerConfirmBtn.ghost"
:shape="innerConfirmBtn.shape"
:size="innerConfirmBtn.size"
:variant="innerConfirmBtn.variant"
:open-type="innerConfirmBtn.openType"
:hover-class="innerConfirmBtn.hoverClass"
:hover-stop-propagation="innerConfirmBtn.hoverStopPropagation"
:hover-start-time="innerConfirmBtn.hoverStartTime"
:hover-stay-time="innerConfirmBtn.hoverStayTime"
:lang="innerConfirmBtn.lang"
:session-from="innerConfirmBtn.sessionFrom"
:send-message-title="innerConfirmBtn.sendMessageTitle"
:send-message-path="innerConfirmBtn.sendMessagePath"
:send-message-img="innerConfirmBtn.sendMessageImg"
:app-parameter="innerConfirmBtn.appParameter"
:show-message-card="innerConfirmBtn.showMessageCard"
:aria-label="innerConfirmBtn.ariaLabel"
@click="onTplButtonTap"
@getuserinfo="onTplButtonTap"
@contact="onTplButtonTap"
@getphonenumber="onTplButtonTap"
@error="onTplButtonTap"
@opensetting="onTplButtonTap"
@launchapp="onTplButtonTap"
@agreeprivacyauthorization="onTplButtonTap"
>
<slot v-if="innerConfirmBtn.useDefaultSlot" />
</t-button>
</block>
</view>
</view>
</template>
<script>
import TIcon from '../icon/icon.vue';
import TButton from '../button/button.vue';
import utils from '../common/utils.wxs';
import {
getDateLabel,
getMonthTitle,
} from './computed.js';
import CalendarHeader from './calendar-header.vue';
import { prefix } from '../common/config';
import { coalesce } from '../common/utils';
import props from './template.props';
export default {
name: 'TCalendarContent',
options: {
styleIsolation: 'shared',
},
components: {
CalendarHeader,
TIcon,
TButton,
},
props: {
...props,
},
emits: [
'clickButton',
'close',
'scroll',
'select',
'handleSwitchModeChange',
],
data() {
return {
prefix,
utils,
};
},
computed: {
closeBtnCustomStyle() {
return utils._style({
position: 'absolute',
top: '16px',
right: '16px',
margin: '-12px',
padding: '12px',
color: 'var(--td-calendar-title-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, .9))))',
});
},
},
watch: {
},
mounted() {
},
methods: {
coalesce,
getDateLabel,
getMonthTitle,
onTplButtonTap() {
this.$emit('clickButton');
},
handleSelect(...args) {
this.$emit('select', ...args);
},
handleClose() {
this.$emit('close');
},
onScroll(...args) {
this.$emit('scroll', ...args);
},
handleSwitchModeChange(...args) {
this.$emit('handleSwitchModeChange', ...args);
},
},
};
</script>
<style scoped>
@import './calendar.css';
</style>

View File

@@ -0,0 +1,141 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdButtonProps as ButtonProps } from '../button/type';
export interface TdCalendarProps {
/**
* 是否允许区间选择日历的起止时间相同,仅当 `type='range'` 时有效
* @default false
*/
allowSameDay?: boolean;
/**
* 自动关闭;在点击关闭按钮、确认按钮、遮罩层时自动关闭,不需要手动设置 visible
* @default true
*/
autoClose?: boolean;
/**
* 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性
* @default ''
*/
confirmBtn?: string | ButtonProps | null;
/**
* 第一天从星期几开始,默认 0 = 周日
* @default 0
*/
firstDayOfWeek?: number;
/**
* 用于格式化日期的函数
*/
format?: CalendarFormatType;
/**
* 国际化文案
*/
localeText?: CalendarLocaleText;
/**
* 最大可选的日期,不传则默认半年后
*/
maxDate?: number;
/**
* 最小可选的日期,不传则默认今天
*/
minDate?: number;
/**
* 是否只读,只读状态下不能选择日期
*/
readonly?: boolean;
/**
* 切换模式。 `none` 表示平铺展示所有月份; `month` 表示支持按月切换, `year-month` 表示既按年切换,也支持按月切换
* @default none
*/
switchMode?: 'none' | 'month' | 'year-month';
/**
* 标题,不传默认为“请选择日期”
*/
title?: string;
/**
* 日历的选择类型single = 单选multiple = 多选; range = 区间选择
* @default single
*/
type?: 'single' | 'multiple' | 'range';
/**
* 是否使用弹出层包裹日历
* @default true
*/
usePopup?: boolean;
/**
* 是否使用了自定义导航栏
* @default false
*/
usingCustomNavbar?: boolean;
/**
* 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组
*/
value?: number | number[];
/**
* 当前选择的日期,不传则选用 minDate 属性值或今天优先级minDate > today。当 type = multiple 或 range 时传入数组,非受控属性
*/
defaultValue?: number | number[];
/**
* 是否显示日历;`usePopup` 为 true 时有效
* @default false
*/
visible?: boolean;
/**
* 不显示 confirm-btn 时,完成选择时触发(暂不支持 type = multiple
*/
onChange?: (context: { value: number | number[] }) => void;
/**
* 关闭按钮时触发
*/
onClose?: (context: { trigger: CalendarTrigger }) => void;
/**
* 点击确认按钮时触发
*/
onConfirm?: (context: { value: number | number[] }) => void;
/**
* 切换月或年时触发switch-mode 不为 none 时有效)
*/
onPanelChange?: (context: { year: number; month: number }) => void;
/**
* 滚动时触发
*/
onScroll?: (context: {
scrollLeft: number;
scrollTop: number;
scrollHeight: number;
scrollWidth: number;
deltaX: number;
deltaY: number;
}) => void;
/**
* 点击日期时触发
*/
onSelect?: (context: { value: number | number[] }) => void;
}
export type CalendarFormatType = (day: TDate) => TDate;
export type TDateType = 'selected' | 'disabled' | 'start' | 'start-end' | 'centre' | 'end' | '';
export interface TDate {
date: Date;
day: number;
type: TDateType;
className?: string;
prefix?: string;
suffix?: string;
}
export interface CalendarLocaleText {
title?: string;
weekdays?: string[];
monthTitle?: string;
months?: string[];
confirm?: string;
}
export type CalendarTrigger = 'close-btn' | 'confirm-btn' | 'overlay' | 'auto-close';

View File

@@ -0,0 +1,16 @@
export function getMonthByOffset(date, offset) {
const _date = new Date(date);
_date.setMonth(_date.getMonth() + offset);
return _date;
}
export function getYearByOffset(date, offset) {
const _date = new Date(date);
_date.setFullYear(_date.getFullYear() + offset);
return _date;
}
export const getPrevMonth = date => getMonthByOffset(date, -1);
export const getNextMonth = date => getMonthByOffset(date, 1);
export const getPrevYear = date => getYearByOffset(date, -1);
export const getNextYear = date => getYearByOffset(date, 1);