diff --git a/devui/date-picker/README.md b/devui/date-picker/README.md new file mode 100644 index 0000000000000000000000000000000000000000..06d4c75be04c9e1b5dd3165fe75c727b45cc7bef --- /dev/null +++ b/devui/date-picker/README.md @@ -0,0 +1,293 @@ +# DatePicker设计思路与实现 + +# 1. 需求梳理 + +## 1.1 主要结构 + +直观上分析,组件主要包括两部分: + +- 日期面板 + - 日期显示面板 + - 跳转到`今天`按钮 +- 顶部操作栏 + +## 1.2 交互分析 + +交互上分析,大致包括: + +- 日期面板操作 + - 单日选择 + - 区域选择 + - 开始日期(select start) + - 区间日期(start + hover) + - 结束日期(select end) + - 跳转到`今天` +- 顶栏操作 + - 显示当前日期 + - 年/月翻页功能 + +## 1.3 事件响应 + +消息传递(事件接口)上分析,应该具备以下几种能力: + +- 日期面板 + - `onSelected` 单板日期值被选中 + - `onSelectStart` 区域日期开始选择 + - `onSelecting` 区域中间值发生变化 + - `onSelectEnd` 区域日期值选择结束 + - `onToday` 跳转当天 + - `onChange` 日期值输出变化 + - `onReset` 需要重置状态 +- 顶部操作 + - `onNextYear` 跳到下一年 + - `onNextMonth` 跳到下一月 + - `onPreviousYear` 跳上一年 + - `onPreviousMonth` 跳上一月 + +## 1.4 状态管理 + +- 日期面板 + - 当月/非当月日期的显示和响应 + - 限制区域外日期的显示和响应 +- 顶部操作 + - 区域双板翻页的互作用 + - 区域限制的显示和响应 + +## 1.5 样式实现 + +- 使用`flex`布局 + + +# 2. 实现思路 + +整体上,`容器组件 + 子组件`的设计思路,将视觉、交互按组件拆分,化整为零,简化问题。 + +## 2.1 子组件拆解 + +根据`1.1`的结构,将`DatePicker`拆解为4个子组件: + +- `calendar` + - `panel` + - `today` +- `toolbar` + +其中`panel`和`today`都是孙级子组件。考虑到代码的可维护性,所有组件在文件存储路径上平级。 + +## 2.2 子组件开发 + +每个子孙组件都是纯展示组件,无内部状态;`DatePicker`作为容器组件,统一管理状态和属性派发。 + +每个子组件,按`1.3`创建事件接口;容器组件订阅这些接口消息,实现子组件与容器组件间通信。 + +### 2.2.1 单项数据流的函数式组件 + +函数式组件,是最佳的单向数据流模式,没有之一。来看一下`today`组件代码: + +```tsx +type TProps = { + onSelected?: (date: Date) => void + disabled?: boolean +} + +export default (props: TProps) => { + const { onSelected = () => 0, disabled = false } = props + return ( +
+ +
+ ) +} +``` + +该组件当中,没有任何的内置状态管理,所有属性都来自`props`,也就是父组件的传递值。 + +- `disabled`属性,考虑到在配置日期区域限制后,`today`有可能不在区域内,此时`today`按钮应当不可用,通过该属性控制; +- `onSelected`事件,用于容器组件订阅`button`的点击事件,来实现约定的`onToday`事件接口。 + +### 2.2.2 在VUE3中使用TSX函数式组件 + +由于`VSCode`对`t.ds`的良好支持——即便是受益于`React`,以及由此带来的开发侧良好体验,`DatePicker`的子孙组件(单向数据流组件)的开发全部使用`纯函数+TSX`。 + +实现详情和`webpack`支持,详见我的另外两篇文章: + +[《DevUI中VUE的TSX函数式组件实践》](https://juejin.cn/post/6999260884631552037) + +[《再聊Vue的TSX函数式组件》](https://juejin.cn/post/7000688749017317407) + +## 2.3 组件样式 + +- 所有布局使用`flex` +- `scss` + 全局样式复用。 + + +# 3 日历面板组件 `panel` + +## 3.1 具体需求: + +- 3.1.1 统一使用7x6的面板,共42天。(虽然有些月份35天面板即可完整显示,但会导致高度不一致) +- 3.1.2 面板补齐 + - 3.1.2.1 一周以周日作为起点(国际默认) + - 3.1.2.2 当月1日非周日的话,向前补齐上月尾巴到周日。 + - 3.1.2.3 使用下月日期向后补齐42天。 +- 3.1.3 非当月日期 + - 3.1.3.1 上月、下月用于补齐面板的日期格虚化显示 + - 3.1.3.2 单板选择后,面板跳转到对应月 + - 3.1.3.3 双板选择后 + - 3.1.3.3.1 双面板按日期大小排序,左板为较早时间 + - 3.1.3.3.2 双板分别跳转到对应月 + - 3.1.3.3.3 当选择区域没有跨月,显示于用户操作面板,另一面板保持。 +- 3.1.4 非限制区域内日期 + - 3.1.4.1 虚化显示 + - 3.1.4.2 鼠标悬停时cursor更改为禁用 +- 3.1.5 交互 + - 3.1.5.1 单板日期选择,触发`onSelected`,通知更新`dateStart`,然后触发`onChange` + - 3.1.5.2 单板`today`点击,触发`onToday`,通知更新`dateStart` + - 3.1.5.3 双板第一次点击,触发`onSelectStart`,通知更新区间日期开始时间`dateStart` + - 3.1.5.4 双板第一次点击后,面板内触发`onSelecting`,通知更新鼠标位置所在日期`dateHover` + - 3.1.5.5 双板第二次点击,触发`onSelectEnd`,通知更新区间日期结束时间`dateEnd`,然后触发`onChange` + - 3.1.5.6 如果`dateStart`和`dateEnd`都已在`props`中定义,则触发`onReset`,通知父组件重置状态。 + - 3.1.5.7 限制区域外日期格禁止交互(不绑定交互事件方法) + + +## 3.2 工具准备 + +由于需要大量的日期类型操作和比较,为了方便开发,所以需要先准备一些工具。 + +### 3.2.1 获取月份42天面板(3.1.2) + +```ts +const getMonthDays = (year: number, month: number) => { + // 当月第一天 + const first = new Date(year, month - 1, 1) + // 当月最后一天 + const last = new Date(year, month, 0) + // 输出结构 + const dates: TDateCell[] = [] + + // 第一天的星期几,0~6,0为周日 + let day = first.getDay() + // 向左补齐星期日期 + while (day > 0) { + day -= 1 + dates.push({ date: new Date(year, month - 1, -day), current: -1 }) + } + // 填充当月日期 + day = last.getDate() - first.getDate() + for (let i = 0; i <= day; i++) { + const date = new Date(first) + date.setDate(i + 1) + dates.push({ date, current: 0 }) + } + + // 向右补齐星期 + day = last.getDay() + let tail: Date = last + for (let i = day; i < 6; i++) { + tail = new Date(year, month, i - day + 1) + dates.push({ date: tail, current: 1 }) + } + // 按42天补齐日期 + // 这里不统一补齐方式,为35天动态面板预留。 + if(dates.length < 42) { + day = tail.getDate() + for (let i = 1; i <= 7; i++) { + tail = new Date(year, month, day + i) + dates.push({ date: tail, current: 1 }) + } + } + return dates +} +``` + +这里的一个技巧,是获取月末日期。 + +因为月份涉及`大小月`、`闰年`等变量因素,月末可能是`28/29/30/31`共4种情况,如果直接计算(尤其是闰年的计算)会比较麻烦。这里使用`取下个月第一天的前一天`的方式,来获取当月月末日期: + +```ts +const last = new Date(2000, 2, 0) // 获取2000年2月最后一天 +// -> Tue Feb 29 2000 00:00:00 GMT+0800 (中国标准时间) +``` + +这里`Date`的第3个参数为0,相当于: + +```ts +let last = new Date(2000, 2, 1) // 获取2000年3月1日 +last = last.setDate(-1) // last设置为3月1日前一天,即2月最后一天,具体日期Date会自己算。 +``` + +同样的方法我们也可以获取到年度的最后一天: + +```ts +const last = new Date(2000, 12, 0) // 获取2000年12月最后一天,也就是年度最后一天。 +// -> Sun Dec 31 2000 00:00:00 GMT+0800 (中国标准时间) +``` + +计算一年天数的时候,有可能用到。 + +**注意月份取值是索引值** + +### 3.2.2 日期比较和区间判断(3.1.5.7) + +```ts +/** 一天毫秒数 */ +const ONE_DAY = 1000 * 60 * 60 * 24 + +/** 按年、月、日三种颗粒度计算一个整数值 */ +export const dateCounter = (date: Date, type: 'y' | 'm' | 'd') => { + switch(type) { + case 'y': return date.getFullYear() + case 'm': return date.getFullYear() * 12 + date.getMonth() + } + return date.getTime() / ONE_DAY >> 0 +} + +/** 按年、月、日三种颗粒比较两个日期 */ +export const compareDateSort = (d1: Date, d2: Date, type: 'y' | 'm' | 'd') => { + const t1 = dateCounter(d1, type), t2 = dateCounter(d2, type) + return t1 < t2 ? -1 : t1 > t2 ? 1 : 0 +} + +/** + * d 是否在 [left, right] 区间 + * @param date 日期 + * @param left 最小日期 + * @param right 最大日期 + * @returns boolean + */ +export const betweenDate = (date: Date, left: any, right: any): boolean => { + if(left instanceof Date && compareDateSort(left, date, 'd') > 0) { + return false + } + if(right instanceof Date && compareDateSort(date, right, 'd') > 0) { + return false + } + return true +} +``` + +### 3.2.3 日期转换(3.1.5.7) + +```ts +/** 字符串转日期 */ +export const parseDate = (str?: string) : Date | null => { + if(!str || typeof str !== 'string') { + return null + } + + const [dateStr = '', timeStr = ''] = str.split(/([ ]|T)+/) + if(!dateStr) { + return null + } + const [y, m, d] = dateStr.split(/[^\d]+/) + const year = _parseInt(y), month = _parseInt(m), date = _parseInt(d) || 1 + if(!year || !month) { + return null + } + const time = parseTime(timeStr) + return new Date(year, month - 1, date, ...time) +} +``` \ No newline at end of file diff --git a/devui/date-picker/date-picker.tsx b/devui/date-picker/date-picker.tsx index e86e834e4740a8290cf1df4adfe3748cdf9da84c..d8d82463e38528687a834f64e7f243d52d0456de 100644 --- a/devui/date-picker/date-picker.tsx +++ b/devui/date-picker/date-picker.tsx @@ -48,7 +48,6 @@ export default defineComponent({ attachInputDom: { type: String }, dateMin: { type: String }, dateMax: { type: String }, - showToday: { type: Boolean, default: false }, }, setup(props, ctx) { diff --git a/sites/components/date-picker/index.md b/sites/components/date-picker/index.md index 0a0ff6e30b63d05db2b6a97595887853d844f4ff..e72945be78792ae4cd3c5970b80a03098f703e8f 100644 --- a/sites/components/date-picker/index.md +++ b/sites/components/date-picker/index.md @@ -1,66 +1,16 @@ - -