diff --git a/docs/src/document/zh-CN/components/datePicker.md b/docs/src/document/zh-CN/components/datePicker.md index 316e774b7419482bf0b741f163eae1f8aeb71abf..de5326e7ddaccba3847ae2b6922209fdf87a496f 100644 --- a/docs/src/document/zh-CN/components/datePicker.md +++ b/docs/src/document/zh-CN/components/datePicker.md @@ -7,54 +7,89 @@ ::: describe 高级 Web 日历组件,足以应对日期相关的各种业务场景。 ::: -::: title 基础使用 +::: title 多类型选择 ::: ::: demo - + + + + + + + + + + + + + + + + + + + + - ::: -::: title 日期时间 +::: title 范围选择 ::: ::: demo - + + + + + + + + + + + + + + + + + + + + + + + - ::: @@ -65,22 +100,13 @@ export default { ::: demo - + - ::: @@ -88,331 +114,452 @@ export default { ::: title 年份选择 ::: -::: demo 将 `type` 设置成 `"year"` 使用年份选择模式。可以用 `yearPage` 指定每页展示年份的数量,用 `yearStep` 指定生成年份的步进值。 +::: demo 将 `type` 设置成 `"year"` 使用年份选择模式。可以用 `year-page` 指定每页展示年份的数量。 - - - + + - ::: -::: title 月份选择 +::: title 快速选择 ::: -::: demo +::: demo 设置 `simple` 属性后,只需要点击一次后自动关闭,无需点击确认按钮。 对于 `datetime` 、 `time` 与对应 `range` 模式因交互影响无效。 - + + + + + + + + + + + + + + + + + + + + + + + + + + - ::: -::: title 时间选择 +::: title 时间戳模式 ::: -::: demo +::: demo 仅在 type 等于`date`、`datetime`时有效 传入的一个 Unix 时间戳 (13 位数字,从 1970 年 1 月 1 日 UTC 午夜开始所经过的毫秒数) - + + + + model-value: {{ timestamp1 }} + + + + model-value: {{ timestamp2 }} + + - ::: -::: title 年月选择 +::: title 禁止任何日期 ::: -::: demo +::: demo 通过 `disabled-date` 可禁止任何日期,优先级大于 `max` `min`。 - + + + 年份: + + + + + 月份: + + + + + 年月份: + + + + + 日期: + + + + + 日期时分秒: + + + + + 时分秒: + + + + - - -::: - -::: title 一次性选择 -::: - -::: demo 只需要点击一次后自动关闭,无需点击确认按钮,仅在 type 等于`year`、`month`、`date`时有效 - - - - - - - - - - - + ::: -::: title 范围选择 +::: title 最大或最小日期 ::: -::: demo +::: demo 通过预设`min`、`max`属性限制组件选择的最大值与最小值。 - + - - modelValue:{{rangeTime1}} + 日期: + - - modelValue:{{rangeTime2}} + 年: + - - default-time: 12:30:00 + 月: + - - modelValue:{{rangeTime3}} + 时间: + - - modelValue:{{rangeTime4}} + 日期区间: + ::: -::: title 时间戳模式 +::: title 默认日期 ::: -::: demo 仅在 type 等于`date`、`datetime`时有效 传入的一个 Unix 时间戳 (13 位数字,从 1970 年 1 月 1 日 UTC 午夜开始所经过的毫秒数) +::: demo `default-value` 可用于在 `modelValue` 为空时,面板展示的默认日期。 在 `range` 模式时,会设置 `左侧` 面板默认日期。 - - - model-value: {{ timestamp1 }} - - - - model-value: {{ timestamp2 }} - + + + - ::: -::: title 最大值,最小值 +::: title 格式化 ::: -::: demo 通过预设`min`、`max`属性限制组件选择的最大值与最小值,目前仅支持`date`、`year`、`month`、`time(不包含range)`模式,且无法在初始化时强制变更 modelValue,在未来的版本中将补齐这一特性并支持更多模式 +::: demo `format` 用于格式化输出 `modelValue`,`inputFormat` 用于格式化输入框显示。 - - 日期: - - - - 年: - - - - 月: - - - - 时间: - - - - 日期区间: - - + endTimeFormat: {{endTimeFormat}} + + endTimeInputFormat: {{endTimeInputFormat}} + - ::: -::: title 格式化 +::: title 快捷选项 ::: -::: demo 目前在 type 等于`year`、`month`这类输出值为非组合值时无效,使用`format`属性任意组合吧 +::: demo 通过 `shortcuts` 在左侧添加快捷选项 - + + + + - ::: + ::: title Date Picker 属性 ::: ::: table -| 属性 | 描述 | 类型 | 默认值 | 可选值 | 版本 | -| -------------- | -------------------------------------------- | ------------------------------------------- | ------------------- | --------------------------------------------------- | -------- | -| name | 原始属性 name | `string` | -- | -- | -- | -| v-model | 当前时间 | `string` `number` `string[]` | -- | -- | -- | -| type | 选择类型 | `string` | `date` | `date` `datetime` `year` `month` `time` `yearmonth` | -- | -| disabled | 是否禁止修改 | `boolean` | `false` | — | — | -| readonly | `input` 是否只读 | `boolean` | `false` | — | — | -| placeholder | `input` 占位符 | `string` | -- | — | — | -| allowClear | 允许清空 | `boolean` | `false` | -- | -- | -| simple | 一次性选择,无需点击确认按钮 | `boolean` | `false` | -- | -- | -| max | 最大可选日期 | `string` | -- | -- | -- | -| min | 最小可选日期 | `string` | -- | -- | -- | -| rang | 是否范围选择 | `boolean` | `false` | -- | -- | -| rangeSeparator | 范围分隔符 | `string` | `至` | -- | -- | -| size | 尺寸 | `string` | `lg` `md` `sm` `xs` | `md` | -- | -| prefix-icon | 前置图标 | `string` | `layui-icon-date` | 内置图标集 | `1.4.0` | -| suffix-icon | 后置图标 | `string` | -- | 内置图标集 | `1.4.0` | -| timestamp | 时间戳模式(13 位),仅对 date 和 datetime 有效 | `boolean` | `false` | `true` `false` | `1.6.5` | -| format | 输出格式化 | `string` | -- | 例如`YYYY-MM-DD` | - | -| default-time | 范围日期 `type=datetime` 时分秒默认时间 | `string` `Array` | -- | 例如`12:30:00` | `2.17.2` | -| contentStyle | 内容自定义样式 | `StyleValue` | -- | -- | -- | -| contentClass | 内容自定义 Class | `string` `Array` `object` | -- | -- | -- | -| yearPage | 年份选择器每页年份的个数 | `2.18.4` | `number` | `15` | -- | -| yearStep | 年份选择器年份的步进值 | `2.18.4` | `number` | `1` | -- | +| 属性 | 描述 | 类型 | 默认值 | 可选值 | 版本 | +| --------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------- | --------------------------------------------------- | -------- | +| name | 原始属性 name | `string` | -- | -- | -- | +| v-model | 当前时间 | `DatePickerModelValueSingleType` `Array` | -- | -- | -- | +| type | 选择类型 | `string` | `date` | `date` `datetime` `year` `month` `time` `yearmonth` | -- | +| disabled | 是否禁止修改 | `boolean` | `false` | — | — | +| readonly | `input` 是否只读 | `boolean` | `false` | — | — | +| placeholder | `input` 占位符 | `string` `Array` | -- | — | — | +| allow-clear | 允许清空 | `boolean` | `false` | -- | -- | +| simple | 一次性选择,无需点击确认按钮 | `boolean` | `false` | -- | -- | +| disabled-date | 判断是否禁止日期的函数,接收一个 `Date` 对象参数,并返回一个 `boolean` 用于是否禁止 | `(value: Date)=> boolean` | -- | --| `2.19.0` | +| max | 最大可选日期 存在 `disabled-date` 此属性无效 | `DatePickerModelValueSingleType ` | -- | -- | -- | +| min | 最小可选日期 存在 `disabled-date` 此属性无效 | `DatePickerModelValueSingleType ` | -- | -- | -- | +| range | 是否范围选择 | `boolean` | `false` | -- | -- | +| range-separator | 范围分隔符 | `string` | `至` | -- | -- | +| size | 尺寸 | `string` | `lg` `md` `sm` `xs` | `md` | -- | +| prefix-icon | 前置图标 | `string` | `layui-icon-date` | 内置图标集 | `1.4.0` | +| suffix-icon | 后置图标 | `string` | -- | 内置图标集 | `1.4.0` | +| timestamp | 时间戳模式(13 位),仅对 date 和 datetime 有效 | `boolean` | `false` | `true` `false` | `1.6.5` | +| format | 输出格式化 | `string` | [查看映射](https://gitee.com/layui-vue/layui-vue/tree/master/packages/component/component/datePicker/hook/useDatePicker.ts#L56) | 参考[dayjs format](https://day.js.org/docs/en/display/format#list-of-all-available-formats) | - | +| input-format | 输入框格式化 | `string` | [查看映射](https://gitee.com/layui-vue/layui-vue/tree/master/packages/component/component/datePicker/hook/useDatePicker.ts#L56) | 参考[dayjs format](https://day.js.org/docs/en/display/format#list-of-all-available-formats) | `2.19.0` | +| default-value | `首次未点击` 时,下拉弹窗打开时默认显示的时间,传空为组件首次 `渲染时间` | `DatePickerModelValueSingleType` `Array` | -- | -- | `2.19.0` | +| default-time | 范围日期 `type=datetime` 时分秒默认时间 | `string` `Array` | -- | 例如`12:30:00` | `2.17.2` | +| content-style | 内容自定义样式 | `StyleValue` | -- | -- | -- | +| content-class | 内容自定义 Class | `string` `Array` `object` | -- | -- | -- | +| year-page | 年份选择器每页年份的个数 | `number` | `15` | --| `2.19.0` | +| shortcuts | 设置快捷选项,需要传入数组对象 | `Array` | -- | --| `2.19.0` | + ::: @@ -430,5 +577,24 @@ export default { ::: +::: title Types + +```ts + +import type { Dayjs } from "dayjs"; + +type DatePickerModelValueSingleType = string | number | Date | Dayjs | null | undefined; + +interface Shortcuts { + text: string | number; + value: + | DatePickerModelValueSingleType + | Array + | (() => DatePickerModelValueSingleType) + | (() => Array); +} + +``` + ::: previousNext datePicker ::: diff --git a/packages/component/component/datePicker/__tests__/datePicker.test.tsx b/packages/component/component/datePicker/__tests__/datePicker.test.tsx index 4263e9f88196c464cfe720ed936e015e0fbdb2e0..e389fa83af750e91a54b9aaae99118deef89f3e5 100644 --- a/packages/component/component/datePicker/__tests__/datePicker.test.tsx +++ b/packages/component/component/datePicker/__tests__/datePicker.test.tsx @@ -1,103 +1,261 @@ -import { mount } from "@vue/test-utils"; -import { afterEach, describe, expect, test } from "vitest"; - -import LayDatePicker from "../index.vue"; -import { nextTick } from "vue"; - -describe("LayDatePicker", () => { - afterEach(() => { - const popperDom = document.body.querySelector(".layui-popper"); - if (popperDom) { - popperDom.remove(); - } - }); - - test("modelValue", async () => { - const wrapper = mount(LayDatePicker, { - props: { - modelValue: "2024/10/01 10:00:00", - }, - }); - - const component = wrapper.findComponent(LayDatePicker); - await nextTick(); - expect((component.props() as any).modelValue).toBe("2024/10/01 10:00:00"); - expect((component.vm as any).currentYear).toBe(2024); - expect((component.vm as any).currentMonth).toBe(10 - 1); - expect((component.vm as any).currentDay).toBe( - new Date("2024/10/01 00:00:00").getTime() - ); - }); - - test("modelValue 修改", async () => { - const wrapper = mount(LayDatePicker, { - props: { - modelValue: "2024/10/01 10:00:00", - }, - }); - - const component = wrapper.findComponent(LayDatePicker); - await nextTick(); - wrapper.setProps({ - modelValue: "2024/10/02 10:00:00", - }); - await nextTick(); - expect((component.props() as any).modelValue).toBe("2024/10/02 10:00:00"); - expect((component.vm as any).currentYear).toBe(2024); - expect((component.vm as any).currentMonth).toBe(10 - 1); - expect((component.vm as any).currentDay).toBe( - new Date("2024/10/02 00:00:00").getTime() - ); - }); - - test("modelValue 负数时间", async () => { - const wrapper = mount(LayDatePicker, { - props: { - modelValue: "1970/01/01 00:00:00", - }, - }); - - const component = wrapper.findComponent(LayDatePicker); - await nextTick(); - expect((component.props() as any).modelValue).toBe("1970/01/01 00:00:00"); - expect((component.vm as any).currentYear).toBe(1970); - expect((component.vm as any).currentMonth).toBe(1 - 1); - expect((component.vm as any).currentDay).toBe( - new Date("1970/01/01 00:00:00").getTime() - ); - }); - - test("timestamp 负数时间", async () => { - const wrapper = mount(LayDatePicker, { - props: { - timestamp: true, - modelValue: -1, - }, - }); - - const component = wrapper.findComponent(LayDatePicker); - await nextTick(); - expect((component.vm as any).currentYear).toBe(1970); - expect((component.vm as any).currentMonth).toBe(1 - 1); - expect((component.vm as any).currentDay).toBe( - new Date("1970/01/01 00:00:00").getTime() - ); - }); - - test("format", async () => { - const wrapper = mount(LayDatePicker, { - props: { - modelValue: "2024/10/01 10:00:00", - format: "MM-dd", - }, - }); - - const component = wrapper.findComponent(LayDatePicker); - await nextTick(); - expect((component.vm as any).currentYear).toBe(2024); - expect((component.vm as any).currentMonth).toBe(10 - 1); - expect((component.vm as any).currentDay).toBe( - new Date("2024/10/01 00:00:00").getTime() - ); - }); -}); +import { mount } from "@vue/test-utils"; +import { afterEach, describe, expect, test } from "vitest"; +import { sleep } from "../../../test-utils"; + +import LayDatePicker from "../index.vue"; +import DateComponent from "../component/common/Date.vue"; +import Year from "../component/common/Year.vue"; +import Footer from "../component/common/Footer.vue"; +// import Month from "../component/common/Month.vue"; +import { nextTick } from "vue"; +import { dayjsToString } from "../util"; + +const mockInputClick = async (wrapper: any) => { + await wrapper.find(".layui-input").trigger("click"); + await nextTick(); + await sleep(); +}; + +describe("LayDatePicker date type", () => { + afterEach(() => { + document.querySelectorAll(".layui-popper").forEach((el) => el.remove()); + }); + + test("modelValue", async () => { + const wrapper = mount(LayDatePicker, { + props: { + modelValue: "2025/11/12 10:00:00", + }, + }); + + await mockInputClick(wrapper); + + const component = wrapper.findComponent(LayDatePicker); + const DateInstance = wrapper.findComponent(DateComponent); + + const [YearSpan, MonthSpan] = DateInstance.findAll( + ".layui-laydate-header .laydate-set-ym span" + ); + + expect((component.props() as any).modelValue).toBe("2025/11/12 10:00:00"); + + expect(YearSpan.text()).toContain("2025"); + expect(MonthSpan.text()).toContain("11"); + + const dateCurrent = DateInstance.find(".layui-laydate-content .layui-this"); + + expect(dateCurrent.exists()).toBeTruthy(); + expect(dateCurrent.text()).toBe("12"); + + await YearSpan.trigger("click"); + await sleep(); + + const YearInstance = wrapper.findComponent(Year); + + expect((YearInstance.vm as any).currentYear).toBe(2025); + + const YearCurrent = YearInstance.find(".layui-laydate-content .layui-this"); + + expect(YearCurrent.exists()).toBeTruthy(); + expect(YearCurrent.text()).toBe("2025"); + }); + + test("外部修改 modelValue 内部选中改变", async () => { + const wrapper = mount(LayDatePicker, { + props: { + modelValue: "2025/11/12", + }, + }); + + await mockInputClick(wrapper); + + await wrapper.setProps({ + modelValue: "2024/10/02", + }); + + await nextTick(); + + const component = wrapper.findComponent(LayDatePicker); + + expect((component.props() as any).modelValue).toBe("2024/10/02"); + + const DateInstance = wrapper.findComponent(DateComponent); + const [YearSpan, MonthSpan] = DateInstance.findAll( + ".layui-laydate-header .laydate-set-ym span" + ); + const dateCurrent = DateInstance.find(".layui-laydate-content .layui-this"); + + expect(YearSpan.text()).toContain("2024"); + expect(MonthSpan.text()).toContain("10"); + expect(dateCurrent.text()).toBe("2"); + + await YearSpan.trigger("click"); + await sleep(); + + const YearInstance = wrapper.findComponent(Year); + + expect((YearInstance.vm as any).currentYear).toBe(2024); + }); + + test("update:modelValue", async () => { + const wrapper = mount(LayDatePicker, { + props: { + modelValue: "2025/11/12", + }, + }); + const datePicker = wrapper.findComponent(LayDatePicker); + + await mockInputClick(wrapper); + const DateInstance = wrapper.findComponent(DateComponent); + + const dateCurrent = DateInstance.findAll( + ".layui-laydate-content td:not(.laydate-day-prev)" + ); + + // click 2号 + await dateCurrent[1].trigger("click"); + + const confirmBtn = wrapper + .findComponent(Footer) + .find(".laydate-footer-btns .laydate-btns-confirm"); + + await confirmBtn.trigger("click"); + + expect(datePicker.emitted()).toHaveProperty("update:modelValue"); + expect(datePicker.emitted()["update:modelValue"][0]).toEqual([ + "2025-11-02", + ]); + }); + + test("min/max", async () => { + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + + await mockInputClick(wrapper); + const DateInstance = wrapper.findComponent(DateComponent); + + const dateCurrent = DateInstance.findAll( + ".layui-laydate-content td:not(.laydate-day-prev)" + ); + + expect( + dateCurrent[0].element.classList.contains("layui-disabled") + ).toBeTruthy(); + expect( + dateCurrent[13].element.classList.contains("layui-disabled") + ).toBeTruthy(); + + expect( + dateCurrent[12].element.classList.contains("layui-disabled") + ).toBeFalsy(); + }); + + test("shortcuts", async () => { + const time = new window.Date(); + const wrapper = mount({ + setup() { + return () => ( + time.getTime() - 86400000, + }, + ]} + > + ); + }, + }); + + const datePickerInstance = wrapper.findComponent(LayDatePicker); + await mockInputClick(wrapper); + const shortcuts = document.body.querySelectorAll( + ".layui-laydate-shortcut li" + ); + + await shortcuts[1].dispatchEvent(new MouseEvent("click")); + await nextTick(); + await sleep(); + + expect(document.body.querySelector(".layui-this")?.textContent).toBe( + dayjsToString(time.getTime() - 86400000, "D") + ); + }); + + test("disabled-date", async () => { + const wrapper = mount({ + setup() { + const isSameDay = (dateA: Date, dateB: Date) => { + return ( + dateA.getFullYear() === dateB.getFullYear() && + dateA.getMonth() === dateB.getMonth() && + dateA.getDate() === dateB.getDate() + ); + }; + const disabledS = [ + new Date("2024-10-01"), + new Date("2024-10-03"), + new Date("2024-09-02"), + ]; + + const disabledDate = (date: Date): boolean => { + return disabledS.some((itemDate: any) => { + return isSameDay(itemDate, date); + }); + }; + + return () => ( + + ); + }, + }); + + await mockInputClick(wrapper); + + const DateInstance = wrapper.findComponent(DateComponent); + + const dateCurrent202410 = DateInstance.findAll( + ".layui-laydate-content td:not(.laydate-day-prev)" + ); + + dateCurrent202410.forEach((date, index) => { + // 2024-10 1/3 日禁止 + if (index === 0 || index === 2) { + expect(date.element.classList.contains("layui-disabled")).toBeTruthy(); + } else { + expect(date.element.classList.contains("layui-disabled")).toBeFalsy(); + } + }); + + const preMonth = DateInstance.find(".layui-icon-left"); + await preMonth.trigger("click"); + + const dateCurrent202409 = DateInstance.findAll( + ".layui-laydate-content td:not(.laydate-day-prev)" + ); + + dateCurrent202409.forEach((date, index) => { + // 2024-09-02 日禁止 + if (index === 1) { + expect(date.element.classList.contains("layui-disabled")).toBeTruthy(); + } else { + expect(date.element.classList.contains("layui-disabled")).toBeFalsy(); + } + }); + }); +}); diff --git a/packages/component/component/datePicker/__tests__/yearPanel.test.tsx b/packages/component/component/datePicker/__tests__/yearPanel.test.tsx index 736e52f02dba5a142bcf1e69a3c534af0d5b67f8..8280075bfca0ee2710e139107c784ee90377d770 100644 --- a/packages/component/component/datePicker/__tests__/yearPanel.test.tsx +++ b/packages/component/component/datePicker/__tests__/yearPanel.test.tsx @@ -1,19 +1,27 @@ import { mount } from "@vue/test-utils"; import { afterEach, describe, expect, test } from "vitest"; -import { nextTick } from "vue"; +import { nextTick, ref } from "vue"; import { sleep } from "../../../test-utils"; import LayDatePicker from "../index.vue"; import LayDropdown from "../../dropdown/index.vue"; -import YearPanel from "../components/YearPanel.vue"; -import { getYears } from "../day"; +import Year from "../component/common/Year.vue"; +import Footer from "../component/common/Footer.vue"; +import Shortcuts from "../component/common/Shortcuts.vue"; +import { getYears } from "../util"; -describe("YearPanel", () => { +const mockInputClick = async (wrapper: any) => { + await wrapper.find(".layui-input").trigger("click"); + await nextTick(); + await sleep(); +}; + +describe("LayDatePicker year type", () => { afterEach(() => { document.querySelectorAll(".layui-popper").forEach((el) => el.remove()); }); - test("render", async () => { + test("year render", async () => { const wrapper = mount(LayDatePicker, { props: { type: "year", @@ -24,16 +32,19 @@ describe("YearPanel", () => { const datePickerInstance = wrapper.findComponent(LayDatePicker); const dropdownInstance = datePickerInstance.findComponent(LayDropdown); await nextTick(); - datePickerInstance.find(".layui-input").trigger("click"); + + await datePickerInstance.find(".layui-input").trigger("click"); await nextTick(); await sleep(); - const yearPanelInstance = datePickerInstance.findComponent(YearPanel); + + const yearPanelInstance = datePickerInstance.findComponent(Year); + expect(dropdownInstance.exists()).toBe(true); expect((dropdownInstance.vm as any).open).toBe(true); expect(yearPanelInstance.findAll("li").length).toBe(getYears().length); }); - test("year 修改", async () => { + test("year 外部修改modelValue 内部状态更新", async () => { const wrapper = mount(LayDatePicker, { props: { type: "year", @@ -43,18 +54,240 @@ describe("YearPanel", () => { const datePickerInstance = wrapper.findComponent(LayDatePicker); await nextTick(); - datePickerInstance.find(".layui-input").trigger("click"); + await datePickerInstance.find(".layui-input").trigger("click"); await nextTick(); await sleep(); - const yearPanelInstance = datePickerInstance.findComponent(YearPanel); + + const yearPanelInstance = datePickerInstance.findComponent(Year); expect(yearPanelInstance.exists()).toBe(true); - expect((yearPanelInstance.vm as any).Year).toBe(2024); + expect((yearPanelInstance.vm as any).currentYear).toBe(2024); wrapper.setProps({ modelValue: "2023/10/01 10:00:00", }); await nextTick(); - // FIXME : 修改 modelValue 后,YearPanel 的 Year 属性没有更新 - // expect((yearPanelInstance.vm as any).Year).toBe(2023); + await sleep(); + + expect((yearPanelInstance.vm as any).currentYear).toBe(2023); + }); + + test("year yearPage", async () => { + const wrapper = mount(LayDatePicker, { + props: { + type: "year", + yearPage: 20, + }, + }); + + await mockInputClick(wrapper); + + const yearLis = wrapper + .findComponent(Year) + .findAll(".laydate-year-list li"); + + expect(yearLis.length).toBe(20); + }); + + test("year 点击年份 触发update:modelValue and change emit", async () => { + const wrapper = mount(LayDatePicker, { + props: { + type: "year", + }, + }); + + await mockInputClick(wrapper); + + const yearLis = wrapper + .findComponent(Year) + .findAll(".laydate-year-list li"); + + await yearLis[0].trigger("click"); + + const datePicker = wrapper.findComponent(LayDatePicker); + + const confirmBtn = wrapper + .findComponent(Footer) + .find(".laydate-footer-btns .laydate-btns-confirm"); + + await confirmBtn.trigger("click"); + + expect(datePicker.emitted()).toHaveProperty("update:modelValue"); + expect(datePicker.emitted()).toHaveProperty("change"); + + expect(datePicker.emitted().change[0]).toEqual(["2011"]); + expect(datePicker.emitted()["update:modelValue"][0]).toEqual(["2011"]); + }); + + test("year 手动改变input 触发改变modelValue", async () => { + const dateValue = ref(); + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + + const inputDom = wrapper.find("input"); + + inputDom.element.value = "2025"; + await inputDom.trigger("input"); + await inputDom.trigger("change"); + + await sleep(); + const datePicker = wrapper.findComponent(LayDatePicker); + expect(datePicker.props("modelValue")).toEqual("2025"); + + await mockInputClick(wrapper); + const YearInstance = wrapper.findComponent(Year); + const currentLi = YearInstance.find(".laydate-year-list .layui-this"); + expect(currentLi.text()).toEqual("2025"); + }); + + test("year simple", async () => { + const dateValue = ref(); + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + + await mockInputClick(wrapper); + + const FooterInstance = wrapper.findComponent(Footer); + + const confirmBtn = FooterInstance.find( + ".laydate-footer-btns .laydate-btns-confirm" + ); + + expect(confirmBtn.exists()).toBeFalsy(); + + const YearInstance = wrapper.findComponent(Year); + const lis = YearInstance.findAll(".laydate-year-list li"); + await lis[0].trigger("click"); + + const datePicker = wrapper.findComponent(LayDatePicker); + expect(datePicker.props("modelValue")).toBe("2011"); + }); + + test("year min/max", async () => { + const wrapper = mount({ + setup() { + return () => ( + + ); + }, + }); + + await mockInputClick(wrapper); + + const YearInstance = wrapper.findComponent(Year); + const currentLi = YearInstance.find( + ".laydate-year-list .layui-laydate-current" + ); + + const preLi = + currentLi.element.previousElementSibling?.previousElementSibling; + const nextLi = currentLi.element.nextElementSibling; + + expect(preLi?.className).toBe("layui-disabled"); + expect(nextLi?.className).toBe("layui-disabled"); + }); + + test("year shortcuts", async () => { + const dateValue = ref(); + + const wrapper = mount({ + setup() { + const shortcuts = [ + { + text: "明年", + value: "2025", + }, + { + text: "后年", + value: "2026", + }, + ]; + + return () => ( + + ); + }, + }); + + await mockInputClick(wrapper); + + const DatePicker = wrapper.findComponent(LayDatePicker); + const ShortcutsDom = wrapper.findComponent(Shortcuts); + const YearInstance = wrapper.findComponent(Year); + const FooterInstance = wrapper.findComponent(Footer); + const ShortcutsLi = ShortcutsDom.findAll("li"); + + expect(ShortcutsLi.length).toBe(2); + + await ShortcutsLi[1].trigger("click"); + + const thisLi = YearInstance.find(".layui-this"); + expect(thisLi.text()).toBe("2026"); + + const confirmBtn = FooterInstance.find( + ".laydate-footer-btns .laydate-btns-confirm" + ); + + await confirmBtn.trigger("click"); + + expect(DatePicker.props("modelValue")).toBe("2026"); + }); + + test("year disabled-date", async () => { + const wrapper = mount({ + setup() { + const disabledDate = (date: Date): boolean => { + return !!(date.getFullYear() % 2); + }; + + return () => ( + + ); + }, + }); + + await mockInputClick(wrapper); + + const YearInstance = wrapper.findComponent(Year); + const currentLi = YearInstance.find( + ".laydate-year-list .layui-laydate-current" + ); + + const preLi = currentLi.element.previousElementSibling; + const nextLi = currentLi.element.nextElementSibling; + + expect(preLi?.className).toBe("layui-disabled"); + expect(nextLi?.className).toBe("layui-disabled"); + + const inputDom = wrapper.find("input"); + + inputDom.element.value = "2025"; + await inputDom.trigger("input"); + await inputDom.trigger("change"); + + await sleep(); + const datePicker = wrapper.findComponent(LayDatePicker); + expect(datePicker.props("modelValue")).toEqual("2024"); }); }); diff --git a/packages/component/component/datePicker/__tests__/yearmonth.test.tsx b/packages/component/component/datePicker/__tests__/yearmonth.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dcdcea0c2848eba4eae005a8f5b7d11255988e61 --- /dev/null +++ b/packages/component/component/datePicker/__tests__/yearmonth.test.tsx @@ -0,0 +1,194 @@ +import { mount, VueWrapper } from "@vue/test-utils"; +import { afterEach, describe, expect, it } from "vitest"; +import LayDatePicker from "../index.vue"; +import { nextTick } from "vue"; +import { sleep } from "../../../test-utils"; +import { getYears } from "../util"; + +import Month from "../component/common/Month.vue"; + +const getInputElementValue = (el: VueWrapper) => el.get("input").element.value; +const getInstanceModelValue = (el: VueWrapper) => + (el.vm.$props as any).modelValue!; +const clickInput = async (el: VueWrapper) => { + await el.get(".layui-input").trigger("click"); + await nextTick(); + await sleep(); +}; +const getPopperElement = () => document.body.querySelector(".layui-popper"); +const openYearPanel = async () => { + const popperElement = getPopperElement(); + if (popperElement) { + try { + popperElement + .querySelector(".laydate-set-ym span")! + .dispatchEvent(new MouseEvent("click")); + } catch (e) { + // do nothing + } + await nextTick(); + await sleep(); + } +}; +const closeYearPanel = async () => { + const popperElement = getPopperElement(); + if (popperElement) { + try { + popperElement + .querySelector(".laydate-year-list li.layui-this")! + .dispatchEvent(new MouseEvent("click")); + } catch (e) { + // do nothing + } + await nextTick(); + await sleep(); + } +}; +const selectYear = async (year: number) => { + const popperElement = getPopperElement(); + if (popperElement) { + await openYearPanel(); + Array.from(popperElement.querySelectorAll(`.laydate-year-list li`)!) + .find((el) => el.textContent === year.toString())! + .dispatchEvent(new MouseEvent("click")); + await nextTick(); + await sleep(); + await closeYearPanel(); + } +}; +const yearRange = async () => { + await openYearPanel(); + const popperElement = getPopperElement(); + let r: Array = []; + if (popperElement) { + r = Array.from( + popperElement.querySelectorAll(`.laydate-year-list li`)! + ).map((el) => Number(el.textContent)); + } + await closeYearPanel(); + return r; +}; +const selectMonth = async (month: number) => { + const popperElement = getPopperElement(); + if (popperElement) { + await closeYearPanel(); + Array.from(popperElement.querySelectorAll(`.laydate-month-list li`)!) + .find((el) => `${el.textContent}` === `${month.toString()}月`)! + .dispatchEvent(new MouseEvent("click")); + await nextTick(); + await sleep(); + } +}; +const clickOk = async () => { + const popperElement = getPopperElement(); + if (popperElement) { + try { + popperElement + .querySelector(".laydate-btns-confirm")! + .dispatchEvent(new MouseEvent("click")); + } catch (e) { + // do nothing + } + await nextTick(); + await sleep(); + } +}; + +describe("LayDatePicker yearmonth type", () => { + afterEach(() => { + document.querySelectorAll(".layui-popper").forEach((el) => el.remove()); + }); + + it("render", async () => { + const wrapper = mount(LayDatePicker, { + props: { + type: "yearmonth", + modelValue: "", + }, + }); + + await nextTick(); + + const datePickerInstance = wrapper.findComponent(LayDatePicker); + expect(datePickerInstance.exists()).toBeTruthy(); + expect(getInputElementValue(datePickerInstance)).toBe(""); + expect(getInstanceModelValue(datePickerInstance)).toBe(""); + + wrapper.setProps({ + modelValue: "2024-10", + }); + + await nextTick(); + expect(getInstanceModelValue(datePickerInstance)).toBe("2024-10"); + expect(getInputElementValue(datePickerInstance)).toBe("2024-10"); + + await clickInput(wrapper); + expect(getPopperElement()).toBeTruthy(); + }); + + it("on simple mode", async () => { + const wrapper = mount(LayDatePicker, { + props: { + type: "yearmonth", + modelValue: "2024-10", + simple: true, + }, + }); + + await nextTick(); + + const datePickerInstance = wrapper.findComponent(LayDatePicker); + expect(datePickerInstance.exists()).toBeTruthy(); + expect(getInputElementValue(datePickerInstance)).toBe("2024-10"); + + await clickInput(wrapper); + const popperElement = getPopperElement(); + expect(popperElement).toBeTruthy(); + const years = await yearRange(); + expect(years).toEqual(getYears(2024)); + await selectYear(2022); + await selectMonth(4); + expect(datePickerInstance.emitted("update:modelValue")![0][0]).toBe( + "2022-04" + ); + }); + + it("yearmonth disabled-date", async () => { + const wrapper = mount(LayDatePicker, { + props: { + type: "yearmonth", + modelValue: "2024-01", + disabledDate(date: Date): boolean { + return ( + date.getFullYear() === 2023 && [1, 3].includes(date.getMonth() + 1) + ); + }, + }, + }); + + await clickInput(wrapper); + + const MonthInstance = wrapper.findComponent(Month); + const Lis2024 = MonthInstance.findAll(".laydate-month-list list"); + + Lis2024.forEach((li) => { + expect(li.element.classList.contains("layui-disabled")).toBeFalsy(); + }); + + // 切换至上一年 + const preBtn = MonthInstance.find(".layui-icon-prev"); + await preBtn.trigger("click"); + await sleep(); + + const Lis2023 = MonthInstance.findAll(".laydate-month-list list"); + + Lis2023.forEach((li, index) => { + // 1/3 月禁止 + if (index === 0 || index === 2) { + expect(li.element.classList.contains("layui-disabled")).toBeTruthy(); + } else { + expect(li.element.classList.contains("layui-disabled")).toBeFalsy(); + } + }); + }); +}); diff --git a/packages/component/component/datePicker/component/DatePicker.vue b/packages/component/component/datePicker/component/DatePicker.vue new file mode 100644 index 0000000000000000000000000000000000000000..5785163e782774ec255f72939beb44c1dc0c6262 --- /dev/null +++ b/packages/component/component/datePicker/component/DatePicker.vue @@ -0,0 +1,218 @@ + + + + + + + + + + (currentType = type)" + > + + + + + + diff --git a/packages/component/component/datePicker/component/DateRange.vue b/packages/component/component/datePicker/component/DateRange.vue new file mode 100644 index 0000000000000000000000000000000000000000..3dd78acb5fdf65d60a79720ea4b6741c1208dfde --- /dev/null +++ b/packages/component/component/datePicker/component/DateRange.vue @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + {{ leftDate.year() }} {{ t("datePicker.year") }} + + handleChangeYearMonthPick(val, 'year', yearLeftRef!)" + > + + + + + {{ MONTH_NAME[leftDate.month()] }} + + + handleChangeYearMonthPick(val, 'month', monthLeftRef!)" + > + + + + + {{ leftDate.format("HH:mm:ss") }} + + + + + + + + + + + + + + + + + + + + + {{ rightDate.year() }} {{ t("datePicker.year") }} + + handleChangeYearMonthPick(val, 'year', yearRightRef!)" + > + + + + + {{ MONTH_NAME[rightDate.month()] }} + + + handleChangeYearMonthPick(val.subtract(1, 'month'), 'month', monthRightRef!)" + > + + + + + {{ rightDate.format("HH:mm:ss") }} + + + + + + + + + + + + + + + + + diff --git a/packages/component/component/datePicker/component/MonthRange.vue b/packages/component/component/datePicker/component/MonthRange.vue new file mode 100644 index 0000000000000000000000000000000000000000..cda5fa77dc4b6c7c7f259b2f67dd283376045378 --- /dev/null +++ b/packages/component/component/datePicker/component/MonthRange.vue @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + {{ leftDate.year() }} {{ t("datePicker.year") }} + + + + + + + + 选择月份范围 + + + + + + + + + {{ leftDate.add(1, "year").year() }} + {{ t("datePicker.year") }} + + + + + + + + + + + + + diff --git a/packages/component/component/datePicker/component/TimeRange.vue b/packages/component/component/datePicker/component/TimeRange.vue new file mode 100644 index 0000000000000000000000000000000000000000..451ce07d06b8fb244463e47f3e0173007a979bf4 --- /dev/null +++ b/packages/component/component/datePicker/component/TimeRange.vue @@ -0,0 +1,106 @@ + + + + + + + + + + {{ t("datePicker.startTime") }} + + + + + {{ t("datePicker.endTime") }} + + + + + + diff --git a/packages/component/component/datePicker/component/YearRange.vue b/packages/component/component/datePicker/component/YearRange.vue new file mode 100644 index 0000000000000000000000000000000000000000..f30be9cf6951ce7898f8c1bba929100625a69017 --- /dev/null +++ b/packages/component/component/datePicker/component/YearRange.vue @@ -0,0 +1,171 @@ + + + + + + + + + + + + + {{ yearList.join(" - ") }} + + + + + + + + + + + {{ yearList.join(" - ") }} + + + + + + + + + diff --git a/packages/component/component/datePicker/component/common/Date.vue b/packages/component/component/datePicker/component/common/Date.vue new file mode 100644 index 0000000000000000000000000000000000000000..f78c1a63e691a77eaec56756cc1c05e57c624d67 --- /dev/null +++ b/packages/component/component/datePicker/component/common/Date.vue @@ -0,0 +1,119 @@ + + + + + + + {{ showDate.year() }} {{ t("datePicker.year") }} + + {{ MONTH_NAME[showDate.month()] }} + + + + + + + + + + + diff --git a/packages/component/component/datePicker/components/components/DateContent.vue b/packages/component/component/datePicker/component/common/DateContent.vue similarity index 30% rename from packages/component/component/datePicker/components/components/DateContent.vue rename to packages/component/component/datePicker/component/common/DateContent.vue index c35cdd1c0ff996f0d8c1981512e37d770bfd7fae..06e012c0525382fe8c03f30550eb8470e2203d83 100644 --- a/packages/component/component/datePicker/components/components/DateContent.vue +++ b/packages/component/component/datePicker/component/common/DateContent.vue @@ -8,7 +8,7 @@ {{ item.day }} @@ -44,29 +39,27 @@ diff --git a/packages/component/component/datePicker/components/PanelFoot.vue b/packages/component/component/datePicker/component/common/Footer.vue similarity index 49% rename from packages/component/component/datePicker/components/PanelFoot.vue rename to packages/component/component/datePicker/component/common/Footer.vue index 0a37de62e5a2a8a8c8b653d2d9cbd007821d4090..bed5b00c9a0bc39e0d254bb57fba31b586fa41ce 100644 --- a/packages/component/component/datePicker/components/PanelFoot.vue +++ b/packages/component/component/datePicker/component/common/Footer.vue @@ -2,25 +2,35 @@ + + + + + + + + + + {{ rangeSeparator }} + + + + + + + + + diff --git a/packages/component/component/datePicker/component/common/Month.vue b/packages/component/component/datePicker/component/common/Month.vue new file mode 100644 index 0000000000000000000000000000000000000000..f2fa7e07c76fec400e6a68f6e9482832fca725e7 --- /dev/null +++ b/packages/component/component/datePicker/component/common/Month.vue @@ -0,0 +1,182 @@ + + + + + + {{ + (Number(currentMonth) ?? -1) > -1 + ? MONTH_NAME[Number(currentMonth)] + : t("datePicker.selectMonth") + }} + + + + + {{ currentDate.year() }} + + + + + + + + + + {{ item.slice(0, 3) }} + + + + + + + diff --git a/packages/component/component/datePicker/component/common/Shortcuts.vue b/packages/component/component/datePicker/component/common/Shortcuts.vue new file mode 100644 index 0000000000000000000000000000000000000000..0924d8c6f3228dcb3132a7cc8fa7cd0b4f99bb06 --- /dev/null +++ b/packages/component/component/datePicker/component/common/Shortcuts.vue @@ -0,0 +1,31 @@ + + + + + + {{ shortcut.text }} + + + diff --git a/packages/component/component/datePicker/component/common/Time.vue b/packages/component/component/datePicker/component/common/Time.vue new file mode 100644 index 0000000000000000000000000000000000000000..a96932419c3fe12b8140515f97112fba2dd3c624 --- /dev/null +++ b/packages/component/component/datePicker/component/common/Time.vue @@ -0,0 +1,212 @@ + + + + + + {{ + t("datePicker.selectTime") + }} + + + + + + + + + {{ index.toString().padStart(2, "0") }} + + + + + + + + + diff --git a/packages/component/component/datePicker/component/common/Year.vue b/packages/component/component/datePicker/component/common/Year.vue new file mode 100644 index 0000000000000000000000000000000000000000..bd1b1abdf22c94387a451f69afcda0d84571bca0 --- /dev/null +++ b/packages/component/component/datePicker/component/common/Year.vue @@ -0,0 +1,127 @@ + + + + + + + + {{ yearRange.join(" - ") }} + + + + + + + + + + {{ item }} + + + + + + + diff --git a/packages/component/component/datePicker/component/interface.ts b/packages/component/component/datePicker/component/interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee688b8567ddfb5ae867591d4176aca25f49b740 --- /dev/null +++ b/packages/component/component/datePicker/component/interface.ts @@ -0,0 +1,29 @@ +import type { Dayjs } from "dayjs"; +import type { + DatePickerType, + DatePickerContextType, + DatePickerValue, +} from "../interface"; + +export interface BasePanelProps { + modelValue: Exclude>; + showDate: Dayjs; + dateType?: DatePickerType; + classes?: (val: Dayjs) => Record; +} + +export interface UniquePickerProps extends DatePickerContextType { + modelValue: Exclude>; +} + +export interface RangePickerProps extends DatePickerContextType { + modelValue: Array; +} + +export interface DateContentSingleDateObject { + day: number; + value: number; + isRange: boolean; + isSelected: boolean; + type: "prev" | "current" | "next"; +} diff --git a/packages/component/component/datePicker/components/DatePanel.vue b/packages/component/component/datePicker/components/DatePanel.vue deleted file mode 100644 index 864d18e448b2defab72533d2be0d1ac4e5487c9b..0000000000000000000000000000000000000000 --- a/packages/component/component/datePicker/components/DatePanel.vue +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - - {{ datePicker.currentYear.value }} {{ t("datePicker.year") }} - - {{ MONTH_NAME[datePicker.currentMonth.value] }} - - - - - - - - {{ t("datePicker.selectTime") }} - - - - - - diff --git a/packages/component/component/datePicker/components/DateRange.vue b/packages/component/component/datePicker/components/DateRange.vue deleted file mode 100644 index 20b61f21ac0e5a51afd6720196ddfd0a45c3d0ca..0000000000000000000000000000000000000000 --- a/packages/component/component/datePicker/components/DateRange.vue +++ /dev/null @@ -1,390 +0,0 @@ - - - - - - - - - - {{ startTime.year || "--" }} {{ t("datePicker.year") }} - - - - - - - {{ MONTH_NAME[startTime.month] }} - - - - - - - {{ - dayjs() - .hour(startTime.hms.hh) - .minute(startTime.hms.mm) - .second(startTime.hms.ss) - .format("HH:mm:ss") - }} - - - - - - - - - - - - - - {{ - startTime.month + 1 > 11 ? startTime.year + 1 : startTime.year - }} - {{ t("datePicker.year") }} - - - - - - - {{ - MONTH_NAME[ - startTime.month + 1 > 11 - ? startTime.month + 1 - 12 - : startTime.month + 1 - ] - }} - - - - - - - - {{ - dayjs() - .hour(endTime.hms.hh) - .minute(endTime.hms.mm) - .second(endTime.hms.ss) - .format("HH:mm:ss") - }} - - - - - - - - - - - - - - - {{ dayjs(startTime.day).format("YYYY-MM-DD") }} - - {{ - dayjs() - .hour(startTime.hms.hh) - .minute(startTime.hms.mm) - .second(startTime.hms.ss) - .format("HH:mm:ss") - }} - - {{ datePicker.rangeSeparator }} - - {{ dayjs(endTime.day).format("YYYY-MM-DD") }} - - {{ - dayjs() - .hour(endTime.hms.hh) - .minute(endTime.hms.mm) - .second(endTime.hms.ss) - .format("HH:mm:ss") - }} - - - -- - - - - - - diff --git a/packages/component/component/datePicker/components/MonthPanel.vue b/packages/component/component/datePicker/components/MonthPanel.vue deleted file mode 100644 index a58042a5fee757f9fc4c19817b6892be311e7049..0000000000000000000000000000000000000000 --- a/packages/component/component/datePicker/components/MonthPanel.vue +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - {{ - typeof Month !== "string" - ? MONTH_NAME[Month] - : t("datePicker.selectMonth") - }} - - - - - - - {{ item.slice(0, 3) }} - - - - - {{ t("datePicker.selectYear") }} - - - - - diff --git a/packages/component/component/datePicker/components/MonthRange.vue b/packages/component/component/datePicker/components/MonthRange.vue deleted file mode 100644 index 04fdaec08ed7ee5e3f4b73d988af288e78874196..0000000000000000000000000000000000000000 --- a/packages/component/component/datePicker/components/MonthRange.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - - - - - - - {{ startTime.year || "--" }} {{ t("datePicker.year") }} - - - - - - - - - - {{ item.slice(0, 3) }} - - - - - - - - - {{ startTime.year + 1 }} {{ t("datePicker.year") }} - - - - - - - - - - - {{ item.slice(0, 3) }} - - - - - - - - {{ dayjs(startTime.unix).format("YYYY-MM") }} - {{ datePicker.rangeSeparator }} - - {{ dayjs(endTime.unix).format("YYYY-MM") }} - - -- - - - - - - diff --git a/packages/component/component/datePicker/components/TimePanel.vue b/packages/component/component/datePicker/components/TimePanel.vue deleted file mode 100644 index 16b7f37df7d41d08896e9f9f6f2ce0bfceb1ec91..0000000000000000000000000000000000000000 --- a/packages/component/component/datePicker/components/TimePanel.vue +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - {{ - t("datePicker.selectTime") - }} - - - - - - - - {{ index.toString().padStart(2, "0") }} - - - - - - - - {{ t("datePicker.selectDate") }} - - {{ - dayjs().hour(hms.hh).minute(hms.mm).second(hms.ss).format("HH:mm:ss") - }} - - - - - - diff --git a/packages/component/component/datePicker/components/TimeRange.vue b/packages/component/component/datePicker/components/TimeRange.vue deleted file mode 100644 index c55413c0555130f3d044c456dc709a1332f6d73e..0000000000000000000000000000000000000000 --- a/packages/component/component/datePicker/components/TimeRange.vue +++ /dev/null @@ -1,291 +0,0 @@ - - - - - - {{ t("datePicker.startTime") }} - - - - - - - - {{ index.toString().padStart(2, "0") }} - - - - - - - - - - {{ t("datePicker.endTime") }} - - - - - - - - {{ index.toString().padStart(2, "0") }} - - - - - - - - - {{ - dayjs() - .hour(startHms.hh) - .minute(startHms.mm) - .second(startHms.ss) - .format("HH:mm:ss") - }} - - 至 - - {{ - dayjs() - .hour(endHms.hh) - .minute(endHms.mm) - .second(endHms.ss) - .format("HH:mm:ss") - }} - - - - - - diff --git a/packages/component/component/datePicker/components/YearPanel.vue b/packages/component/component/datePicker/components/YearPanel.vue deleted file mode 100644 index adc9e6a1db30579863688209e5e21433342bcf40..0000000000000000000000000000000000000000 --- a/packages/component/component/datePicker/components/YearPanel.vue +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - - - {{ yearRange.join(" - ") }} - - - - - - - - - {{ item }} - - - - - {{ t("datePicker.selectMonth") }} - {{ Year }} - - - - - diff --git a/packages/component/component/datePicker/font/iconfont.eot b/packages/component/component/datePicker/font/iconfont.eot deleted file mode 100644 index c861caa8e2c1e5bfff43ca36bf79fe3195545148..0000000000000000000000000000000000000000 Binary files a/packages/component/component/datePicker/font/iconfont.eot and /dev/null differ diff --git a/packages/component/component/datePicker/font/iconfont.svg b/packages/component/component/datePicker/font/iconfont.svg deleted file mode 100644 index 788ecc69785fe12104a5130b5da5439c82b1ceef..0000000000000000000000000000000000000000 --- a/packages/component/component/datePicker/font/iconfont.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - -Created by iconfont - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/component/component/datePicker/font/iconfont.ttf b/packages/component/component/datePicker/font/iconfont.ttf deleted file mode 100644 index 0bd6c4a88dd899aea3926e49bdfb37e81004bcb9..0000000000000000000000000000000000000000 Binary files a/packages/component/component/datePicker/font/iconfont.ttf and /dev/null differ diff --git a/packages/component/component/datePicker/font/iconfont.woff b/packages/component/component/datePicker/font/iconfont.woff deleted file mode 100644 index bfe5599671f9441ceebdc8afeac71d4f65a741c7..0000000000000000000000000000000000000000 Binary files a/packages/component/component/datePicker/font/iconfont.woff and /dev/null differ diff --git a/packages/component/component/datePicker/hook/useBaseDatePicker.ts b/packages/component/component/datePicker/hook/useBaseDatePicker.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f3e792d5c08cbe712b9f1c82f4b5aa2d28ab47f --- /dev/null +++ b/packages/component/component/datePicker/hook/useBaseDatePicker.ts @@ -0,0 +1,20 @@ +import dayjs from "dayjs"; + +import type { + DatePickerContextType, + DatePickerModelValueSingleType, +} from "../interface"; + +export const useBaseDatePicker = (props: DatePickerContextType) => { + const getDefaultValue = () => { + const _defaultValue = dayjs( + props.defaultValue as DatePickerModelValueSingleType + ); + + return _defaultValue.isValid() + ? _defaultValue.startOf("day") + : dayjs().startOf("day"); + }; + + return { getDefaultValue }; +}; diff --git a/packages/component/component/datePicker/hook/useDatePicker.ts b/packages/component/component/datePicker/hook/useDatePicker.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0badc1083a47560e46e0ca40fd5e4b570ef48c4 --- /dev/null +++ b/packages/component/component/datePicker/hook/useDatePicker.ts @@ -0,0 +1,106 @@ +import type { + // DatePickerProps, + RequiredDatePickerProps, + TypeMap, + DatePickerModelValueSingleType, + DatePickerContextType, +} from "../interface"; +import { type Dayjs } from "dayjs"; + +import { computed, reactive, toRefs } from "vue"; +import DatePicker from "../component/DatePicker.vue"; +import DateRange from "../component/DateRange.vue"; +import TimeRange from "../component/TimeRange.vue"; +import MonthRange from "../component/MonthRange.vue"; +import YearRange from "../component/YearRange.vue"; + +import { normalizeDayjsValue } from "../util"; +import { isArray, isNumber } from "../../../utils"; + +export const useDatePicker = (props: RequiredDatePickerProps) => { + const initDate = computed | null>(() => { + if (props.range) { + const modelValue = isArray(props.modelValue) ? props.modelValue : []; + + return modelValue.map((date: DatePickerModelValueSingleType) => { + return normalizeDayjsValue(date, _format.value); + }) as Array; + } else { + let value = props.modelValue; + + // 兼容之前的 Year | Month 类型 modelValue可传 `2024` | `11` number类型 + if ( + ["year", "month"].includes(props.type!) && + isNumber(props.modelValue) && + `${props.modelValue}`.length <= 4 + ) { + value += ""; + } + + return normalizeDayjsValue( + value as DatePickerModelValueSingleType, + _format.value + ); + } + }); + + const typeMap = computed(() => { + return TYPE_MAP[props.type]; + }); + + const _format = computed(() => { + return props.format || typeMap.value.format; + }); + + const _inputFormat = computed(() => { + return props.inputFormat ?? typeMap.value.format; + }); + + // 类型映射 + const TYPE_MAP: TypeMap = { + year: { component: props.range ? YearRange : DatePicker, format: "YYYY" }, + month: { component: props.range ? MonthRange : DatePicker, format: "M" }, + datetime: { + component: props.range ? DateRange : DatePicker, + format: "YYYY-MM-DD HH:mm:ss", + }, + date: { + component: props.range ? DateRange : DatePicker, + format: "YYYY-MM-DD", + }, + yearmonth: { + component: props.range ? MonthRange : DatePicker, + format: "YYYY-MM", + }, + time: { + component: props.range ? TimeRange : DatePicker, + format: "HH:mm:ss", + }, + }; + + const renderComponentProps = computed(() => { + return { + ...props, + modelValue: initDate.value, + format: _format.value, + inputFormat: _inputFormat.value, + }; + }); + + const datePickerContext = reactive({ + ...toRefs(props), + modelValue: initDate, + format: _format, + inputFormat: _inputFormat, + }); + + const RenderComponent = computed(() => { + return typeMap.value.component; + }); + + return { + RenderComponent, + renderComponentProps, + datePickerContext, + }; +}; diff --git a/packages/component/component/datePicker/index.hooks.ts b/packages/component/component/datePicker/hook/useProps.ts similarity index 57% rename from packages/component/component/datePicker/index.hooks.ts rename to packages/component/component/datePicker/hook/useProps.ts index e11947c2c998d9e8a83e6357fa6f4a036d09ce38..3094fa915e319f14405ad69c1d50f87cf3fa1675 100644 --- a/packages/component/component/datePicker/index.hooks.ts +++ b/packages/component/component/datePicker/hook/useProps.ts @@ -1,7 +1,9 @@ -import { LayFormContext } from "../../types"; +import type { LayFormContext } from "../../../types"; import { computed, inject } from "vue"; -export default function useProps(props: any) { +import type { DatePickerProps } from "../interface"; + +export function useProps(props: DatePickerProps) { const size = computed(() => { const formContext = inject("LayForm", {} as LayFormContext); diff --git a/packages/component/component/datePicker/hook/useShortcutsRange.ts b/packages/component/component/datePicker/hook/useShortcutsRange.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcf2aa8c1f0620bf481011b282e5dad057d73eae --- /dev/null +++ b/packages/component/component/datePicker/hook/useShortcutsRange.ts @@ -0,0 +1,27 @@ +import dayjs, { type Dayjs } from "dayjs"; +import { inject } from "vue"; +import { + DATE_PICKER_CONTEXT, + type Shortcuts, + type DatePickerModelValueSingleType, +} from "../interface"; + +import { normalizeDayjsValue } from "../util"; +import { isFunction } from "../../../utils"; + +export const useShortcutsRange = () => { + const DatePickerContext = inject(DATE_PICKER_CONTEXT)!; + + const handleChangeShortcut = (shortcuts: Shortcuts): Array => { + const dates = ( + isFunction(shortcuts.value) ? shortcuts.value() : shortcuts.value + ) as Array; + + return [ + normalizeDayjsValue(dates[0], DatePickerContext.format!), + normalizeDayjsValue(dates[1], DatePickerContext.format!), + ].filter(Boolean) as Array; + }; + + return handleChangeShortcut; +}; diff --git a/packages/component/component/datePicker/index.less b/packages/component/component/datePicker/index.less index 09c43cdc094cbbe97970502567d4407ec9c3f69e..2a0fb580a5403e074059b1704a6943eba195a3c1 100644 --- a/packages/component/component/datePicker/index.less +++ b/packages/component/component/datePicker/index.less @@ -1,647 +1,701 @@ -@import "../dropdown/index.less"; -@import "../input/index.less"; - -@lg: 44px; -@md: 38px; -@sm: 32px; -@xs: 26px; -@lg-width: 260px; -@md-width: 220px; -@sm-width: 180px; -@xs-width: 140px; -@lg-range-width: 520px; -@md-range-width: 440px; -@sm-range-width: 360px; -@xs-range-width: 280px; - -.set-size(@size,@width) { - & { - width: @width; - height: @size; - .layui-input { - height: @size; - line-height: @size; - } - } -} - -.layui-date-picker { - &[size="lg"] { - .set-size(@lg,@lg-width); - } - &[size="md"] { - .set-size(@md,@md-width); - } - &[size="sm"] { - .set-size(@sm,@sm-width); - } - &[size="xs"] { - .set-size(@xs,@xs-width); - } -} -.layui-date-range-picker { - &[size="lg"] { - .set-size(@lg,@lg-range-width); - } - &[size="md"] { - .set-size(@md,@md-range-width); - } - &[size="sm"] { - .set-size(@sm,@sm-range-width); - } - &[size="xs"] { - .set-size(@xs,@xs-range-width); - } -} - -@font-face { - font-family: "laydate-icon"; - src: url("./font/iconfont.eot"); - src: url("./font/iconfont.eot#iefix") format("embedded-opentype"), - url("./font/iconfont.svg#iconfont") format("svg"), - url("./font/iconfont.woff") format("woff"), - url("./font/iconfont.ttf") format("truetype"); -} - -.laydate-icon { - font-family: "laydate-icon" !important; - font-size: 16px; - font-style: normal; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -html #layuicss-laydate { - display: none; - position: absolute; - width: 1989px; -} - -/* 初始化 */ -.layui-laydate * { - margin: 0; - padding: 0; -} - -/* 主体结构 */ -.layui-laydate, -.layui-laydate * { - box-sizing: border-box; -} -.layui-laydate { - z-index: 66666666; - border-radius: var(--global-border-radius); - font-size: 14px; - -webkit-animation-duration: 0.2s; - animation-duration: 0.2s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; - - .layui-laydate-year-control { - padding: 4px; - } -} -.layui-laydate-main { - width: 272px; -} -.layui-laydate-header *, -.layui-laydate-content td, -.layui-laydate-list li { - transition-duration: 0.3s; - -webkit-transition-duration: 0.3s; -} - -/* 微微往下滑入 */ -@keyframes laydate-downbit { - 0% { - opacity: 0.3; - transform: translate3d(0, -5px, 0); - } - 100% { - opacity: 1; - transform: translate3d(0, 0, 0); - } -} - -.layui-laydate { - animation-name: laydate-downbit; -} -.layui-laydate-static { - position: relative; - z-index: 0; - display: inline-block; - margin: 0; - -webkit-animation: none; - animation: none; -} - -/* 展开年月列表时 */ -.laydate-ym-show .laydate-prev-m, -.laydate-ym-show .laydate-next-m { - display: none !important; -} -.laydate-ym-show .laydate-prev-y, -.laydate-ym-show .laydate-next-y { - display: inline-block !important; -} -.laydate-ym-show .laydate-set-ym span[lay-type="month"] { - display: none !important; -} - -/* 展开时间列表时 */ -.laydate-time-show .layui-laydate-header .layui-icon, -.laydate-time-show .laydate-set-ym span[lay-type="year"], -.laydate-time-show .laydate-set-ym span[lay-type="month"] { - display: none !important; -} - -/* 头部结构 */ -.layui-laydate-header { - position: relative; - line-height: 30px; - padding: 10px 70px 5px; -} -.layui-laydate-header * { - vertical-align: bottom; -} -.layui-laydate-header i { - position: absolute; - top: 10px; - padding: 0 5px; - color: #999; - font-size: 12px; - cursor: pointer; - user-select: none; -} -.layui-laydate-header i.laydate-prev-y { - left: 15px; -} -.layui-laydate-header i.laydate-prev-m { - left: 45px; -} -.layui-laydate-header i.laydate-next-y { - right: 15px; -} -.layui-laydate-header i.laydate-next-m { - right: 45px; -} -.layui-laydate-year-panel-header { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - padding: 10px 14px 5px; - line-height: 30px; - border-bottom: 1px solid #e2e2e2; - - .laydate-set-ym { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - - i { - cursor: pointer; - user-select: none; - } - } -} -.laydate-set-ym { - width: 100%; - text-align: center; - box-sizing: border-box; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} -.laydate-set-ym span { - padding: 0 10px; - cursor: pointer; -} -.laydate-time-text { - cursor: default !important; -} - -/* 主体结构 */ -.layui-laydate-content { - position: relative; - padding: 10px; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} -.layui-laydate-content table { - border-collapse: collapse; - border-spacing: 0; -} -.layui-laydate-content th, -.layui-laydate-content td { - width: 36px; - height: 30px; - padding: 5px; - text-align: center !important; -} -.layui-laydate-content th { - font-weight: 400; -} -.layui-laydate-content td { - position: relative; - cursor: pointer; -} -.laydate-day-mark { - position: absolute; - left: 0; - top: 0; - width: 100%; - line-height: 30px; - font-size: 12px; - overflow: hidden; -} -.laydate-day-mark::after { - position: absolute; - content: ""; - right: 2px; - top: 2px; - width: 5px; - height: 5px; - border-radius: 50%; -} - -/* 底部结构 */ -.layui-laydate-footer { - position: relative; - height: 46px; - line-height: 26px; - padding: 10px; -} - -.layui-laydate-footer .laydate-footer-btns span { - border-radius: 0px; -} - -.layui-laydate-footer .laydate-footer-btns span:first-child { - border-top-left-radius: var(--global-border-radius); - border-bottom-left-radius: var(--global-border-radius); -} - -.layui-laydate-footer .laydate-footer-btns span:last-child { - border-top-right-radius: var(--global-border-radius); - border-bottom-right-radius: var(--global-border-radius); -} - -.layui-laydate-footer span { - display: inline-block; - vertical-align: top; - height: 26px; - line-height: 24px; - padding: 0 10px; - border: 1px solid #c9c9c9; - border-radius: 2px; - background-color: #fff; - font-size: 12px; - cursor: pointer; - white-space: nowrap; - transition: all 0.3s; -} -.layui-laydate-footer span:hover { - color: var(--global-primary-color); -} -.layui-laydate-footer span.layui-laydate-preview { - cursor: default; - border-color: transparent !important; -} -.layui-laydate-footer span.layui-laydate-preview:hover { - color: #666; -} -.layui-laydate-footer span:first-child.layui-laydate-preview { - padding-left: 0; -} -.laydate-footer-btns { - position: absolute; - right: 10px; - top: 10px; -} -.laydate-footer-btns span { - margin: 0 0 0 -1px; -} - -/* 年月列表 */ -.layui-laydate-list { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - padding: 10px; - box-sizing: border-box; - background-color: #fff; -} -.layui-laydate-list > li { - position: relative; - display: inline-block; - width: 33.3%; - height: 36px; - line-height: 36px; - margin: 3px 0; - vertical-align: middle; - text-align: center; - cursor: pointer; -} -.laydate-month-list > li { - width: 25%; - margin: 17px 0; -} -.laydate-time-list > li { - height: 100%; - margin: 0; - line-height: normal; - cursor: default; -} -.laydate-time-list p { - position: relative; - top: -4px; - line-height: 29px; -} -.laydate-time-list ol { - height: 181px; - overflow: hidden; -} -.laydate-time-list > li:hover ol { - overflow-y: auto; -} - -.laydate-time-list ol li { - width: 130%; - padding-left: 4px; - height: 30px; - line-height: 30px; - text-align: left; - cursor: pointer; -} - -/* 提示 */ -.layui-laydate-hint { - top: 115px; - left: 50%; - width: 250px; - margin-left: -125px; - line-height: 20px; - padding: 15px; - text-align: center; - font-size: 12px; - color: #ff5722; -} - -/* 双日历 */ -.layui-laydate-range { - min-width: 546px; -} -.layui-laydate-range .layui-laydate-main { - display: inline-block; - vertical-align: middle; -} -.layui-laydate-range .laydate-main-list-1 .layui-laydate-header, -.layui-laydate-range .laydate-main-list-1 .layui-laydate-content { - border-left: 1px solid #e2e2e2; -} - -/* 默认简约主题 */ -.layui-laydate, -.layui-laydate-hint { - background-color: #fff; - color: #666; -} -.layui-laydate-header { - border-bottom: 1px solid #e2e2e2; -} -.layui-laydate-header i:hover, -.layui-laydate-header span:hover { - color: #5fb878; -} -.layui-laydate-content { - border-top: none 0; - border-bottom: none 0; -} -.layui-laydate-content th { - color: #333; -} -.layui-laydate-content td { - color: #666; -} -.layui-laydate-content td.laydate-selected { - background-color: #b5fff8; -} -.laydate-selected:hover { - background-color: #00f7de !important; -} -.layui-laydate-content td:hover, -.layui-laydate-list li:hover { - background-color: #eee; - color: #333; -} -.laydate-time-list li ol { - margin: 0; - padding: 0; - border: 1px solid #e2e2e2; -} -.laydate-time-list li:first-child ol { - border-left-width: 1px; -} -.laydate-time-list > li:hover { - background: none; -} -.layui-laydate-content .laydate-day-prev, -.layui-laydate-content .laydate-day-next { - color: #d2d2d2; -} -.laydate-selected.laydate-day-prev, -.laydate-selected.laydate-day-next { - background-color: #f8f8f8 !important; -} -.layui-laydate-footer { - border-top: 1px solid #e2e2e2; -} -.layui-laydate-hint { - color: #ff5722; -} -.laydate-day-mark::after { - background-color: #5fb878; -} -.layui-laydate-content td.layui-this .laydate-day-mark::after { - display: none; -} -.layui-laydate-footer span[lay-type="date"] { - color: #5fb878; -} -.layui-laydate .layui-this { - background-color: var(--global-primary-color) !important; - color: #fff !important; -} - -.layui-laydate .laydate-disabled, -.layui-laydate .laydate-disabled:hover { - background: none !important; - color: #d2d2d2 !important; - cursor: not-allowed !important; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -/* 墨绿/自定义背景色主题 */ -.laydate-theme-molv { - border: none; -} -.laydate-theme-molv.layui-laydate-range { - width: 548px; -} -.laydate-theme-molv .layui-laydate-main { - width: 274px; -} -.laydate-theme-molv .layui-laydate-header { - border: none; - background-color: #009688; -} -.laydate-theme-molv .layui-laydate-header i, -.laydate-theme-molv .layui-laydate-header span { - color: #f6f6f6; -} -.laydate-theme-molv .layui-laydate-header i:hover, -.laydate-theme-molv .layui-laydate-header span:hover { - color: #fff; -} -.laydate-theme-molv .layui-laydate-content { - border: 1px solid #e2e2e2; - border-top: none; - border-bottom: none; -} -.laydate-theme-molv .laydate-main-list-1 .layui-laydate-content { - border-left: none; -} -.laydate-theme-molv .layui-laydate-footer { - border: 1px solid #e2e2e2; -} - -/* 格子主题 */ -.laydate-theme-grid .layui-laydate-content td, -.laydate-theme-grid .layui-laydate-content thead, -.laydate-theme-grid .laydate-year-list > li, -.laydate-theme-grid .laydate-month-list > li { - border: 1px solid #e2e2e2; -} -.laydate-theme-grid .laydate-selected, -.laydate-theme-grid .laydate-selected:hover { - background-color: #f2f2f2 !important; - color: #009688 !important; -} -.laydate-theme-grid .laydate-selected.laydate-day-prev, -.laydate-theme-grid .laydate-selected.laydate-day-next { - color: #d2d2d2 !important; -} -.laydate-theme-grid .laydate-year-list, -.laydate-theme-grid .laydate-month-list { - margin: 1px 0 0 1px; -} -.laydate-theme-grid .laydate-year-list > li, -.laydate-theme-grid .laydate-month-list > li { - margin: 0 -1px -1px 0; -} -.laydate-theme-grid .laydate-year-list > li { - height: 43px; - line-height: 43px; -} -.laydate-theme-grid .laydate-month-list > li { - height: 71px; - line-height: 71px; -} -.laydate-range-hover { - background-color: var(--global-neutral-color-2) !important; -} -.layui-laydate-content .layui-disabled:hover { - background-color: transparent !important; -} - -.laydate-range-inputs { - display: flex; - align-items: center; - height: 100%; - border-width: 1px; - box-sizing: border-box; - width: 100%; - border-style: solid; - overflow: hidden; - display: inline-flex; - border-color: var(--input-border-color); - border-radius: var(--input-border-radius); - &:hover { - border-color: #d2d2d2; - } - .range-separator { - margin: 0 5px; - color: var(--global-neutral-color-8); - background-color: transparent; - } - .layui-input-wrapper { - border: none; - box-sizing: border-box; - input { - text-align: center; - padding: 0; - } - } - .layui-input { - border: none; - } -} -.layui-laydate-range { - .laydate-set-ym { - overflow: visible; - white-space: nowrap; - } - .laydate-set-ym .layui-dropdown { - width: auto !important; - } - - .time-panel { - .layui-laydate-main { - width: 272px; - display: unset !important; - } - .layui-laydate-preview { - display: none; - } - } - .layui-laydate-content { - .laydate-year-list { - display: flex; - flex-wrap: wrap; - } - } - .layui-laydate-list { - display: flex; - flex-wrap: wrap; - } -} -.layui-laydate-range-datetime { - .layui-laydate-main { - width: 340px; - } -} -.layui-laydate-current { - background-color: var(--global-neutral-color-3); -} - -.laydate-time-list > li:hover ol::-webkit-scrollbar { - width: 0px; -} -.layui-laydate-content::-webkit-scrollbar { - width: 8px; - height: 8px; -} -.layui-laydate-content::-webkit-scrollbar-thumb { - border-radius: 10px; - background-color: #eeeeee; -} -.layui-laydate-content::-webkit-scrollbar-thumb:hover { - background-color: #dddddd; -} +@import "../dropdown/index.less"; +@import "../input/index.less"; + +@lg: 44px; +@md: 38px; +@sm: 32px; +@xs: 26px; +@lg-width: 260px; +@md-width: 220px; +@sm-width: 180px; +@xs-width: 140px; +@lg-range-width: 520px; +@md-range-width: 440px; +@sm-range-width: 360px; +@xs-range-width: 280px; + +.set-size(@size,@width) { + & { + width: @width; + height: @size; + .layui-input { + height: @size; + line-height: @size; + } + } +} + +.layui-date-picker { + &[size="lg"] { + .set-size(@lg,@lg-width); + } + &[size="md"] { + .set-size(@md,@md-width); + } + &[size="sm"] { + .set-size(@sm,@sm-width); + } + &[size="xs"] { + .set-size(@xs,@xs-width); + } +} +.layui-date-range-picker { + &[size="lg"] { + .set-size(@lg,@lg-range-width); + } + &[size="md"] { + .set-size(@md,@md-range-width); + } + &[size="sm"] { + .set-size(@sm,@sm-range-width); + } + &[size="xs"] { + .set-size(@xs,@xs-range-width); + } +} + +html #layuicss-laydate { + display: none; + position: absolute; + width: 1989px; +} + +/* 初始化 */ +.layui-laydate * { + margin: 0; + padding: 0; +} + +/* 主体结构 */ +.layui-laydate, +.layui-laydate * { + box-sizing: border-box; +} +.layui-laydate { + z-index: 66666666; + border-radius: var(--global-border-radius); + font-size: 14px; + -webkit-animation-duration: 0.2s; + animation-duration: 0.2s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + + .layui-laydate-year-control { + padding: 4px; + } +} +.layui-laydate-main { + width: 272px; +} +.layui-laydate-header *, +.layui-laydate-content td, +.layui-laydate-list li { + transition-duration: 0.3s; + -webkit-transition-duration: 0.3s; +} + +/* 微微往下滑入 */ +@keyframes laydate-downbit { + 0% { + opacity: 0.3; + transform: translate3d(0, -5px, 0); + } + 100% { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +.layui-laydate { + animation-name: laydate-downbit; +} +// .layui-laydate-static { +// position: relative; +// z-index: 0; +// display: inline-block; +// margin: 0; +// -webkit-animation: none; +// animation: none; +// } + +/* 展开年月列表时 */ +.laydate-ym-show .laydate-prev-m, +.laydate-ym-show .laydate-next-m { + display: none !important; +} +.laydate-ym-show .laydate-prev-y, +.laydate-ym-show .laydate-next-y { + display: inline-block !important; +} +.laydate-ym-show .laydate-set-ym span[lay-type="month"] { + display: none !important; +} + +/* 展开时间列表时 */ +.laydate-time-show .layui-laydate-header .layui-icon, +.laydate-time-show .laydate-set-ym span[lay-type="year"], +.laydate-time-show .laydate-set-ym span[lay-type="month"] { + display: none !important; +} + +/* 头部结构 */ +.layui-laydate-header { + position: relative; + height: 46px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10px; + // line-height: 30px; + // padding: 10px 70px 5px; +} +.layui-laydate-header * { + vertical-align: bottom; +} +.layui-laydate-header i { + // position: absolute; + // top: 10px; + padding: 0 5px; + color: #999; + font-size: 12px; + cursor: pointer; + user-select: none; +} +.layui-laydate-header .layui-icon-prev { + left: 15px; +} +.layui-laydate-header .layui-icon-left { + left: 45px; +} +.layui-laydate-header .layui-icon-next { + right: 15px; +} +.layui-laydate-header .layui-icon-right { + right: 45px; +} +.layui-laydate-year-panel-header { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 10px 14px 5px; + line-height: 30px; + border-bottom: 1px solid #e2e2e2; + + .laydate-set-ym { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + + i { + cursor: pointer; + user-select: none; + } + } +} +.laydate-set-ym { + width: 100%; + text-align: center; + box-sizing: border-box; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} +.laydate-set-ym span { + padding: 0 10px; + cursor: pointer; +} +.laydate-time-text { + cursor: default !important; +} + +/* 主体结构 */ +.layui-laydate-content { + position: relative; + padding: 10px; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} +.layui-laydate-content table { + border-collapse: collapse; + border-spacing: 0; +} +.layui-laydate-content th, +.layui-laydate-content td { + width: 36px; + height: 30px; + padding: 5px; + text-align: center !important; +} +.layui-laydate-content th { + font-weight: 400; +} +.layui-laydate-content td { + position: relative; + cursor: pointer; +} +.laydate-day-mark { + position: absolute; + left: 0; + top: 0; + width: 100%; + line-height: 30px; + font-size: 12px; + overflow: hidden; +} +.laydate-day-mark::after { + position: absolute; + content: ""; + right: 2px; + top: 2px; + width: 5px; + height: 5px; + border-radius: 50%; +} + +/* 底部结构 */ +.layui-laydate-footer { + // position: relative; + // height: 46px; + display: flex; + line-height: 26px; + padding: 10px; +} + +.layui-laydate-footer .layui-btn { + padding: 0 10px; + white-space: nowrap; + transition: all 0.3s; +} + +.layui-laydate-footer .laydate-footer-btns .layui-btn { + border-radius: 0; +} + +.layui-laydate-footer .laydate-footer-btns .layui-btn:first-child { + border-top-left-radius: var(--global-border-radius); + border-bottom-left-radius: var(--global-border-radius); +} + +.layui-laydate-footer .laydate-footer-btns .layui-btn:last-child { + border-top-right-radius: var(--global-border-radius); + border-bottom-right-radius: var(--global-border-radius); + margin-left: -1px; +} + +.layui-laydate-footer .layui-btn + .layui-btn { + margin-left: 0; +} +.layui-laydate-footer { + .laydate-footer-btns { + .layui-btn:not(.layui-btn-disabled) { + span:hover { + color: var(--global-primary-color); + } + } + } +} +.layui-laydate-footer span.layui-laydate-preview { + cursor: default; + border-color: transparent !important; +} +.layui-laydate-footer span.layui-laydate-preview:hover { + color: #666; +} +.layui-laydate-footer span:first-child.layui-laydate-preview { + padding-left: 0; +} +.laydate-footer-btns { + margin-left: auto; + // position: absolute; + // right: 10px; + // top: 10px; +} + +.layui-laydate-footer .type-time { + color: var(--global-primary-color); +} + +/* 年月列表 */ +.layui-laydate-list { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + padding: 10px; + box-sizing: border-box; + background-color: #fff; +} +.layui-laydate-list > li { + position: relative; + display: inline-block; + width: 33.3%; + height: 36px; + line-height: 36px; + margin: 3px 0; + vertical-align: middle; + text-align: center; + cursor: pointer; +} +.laydate-month-list > li { + width: 25%; + margin: 17px 0; +} +.laydate-time-list > li { + height: 100%; + margin: 0; + line-height: normal; + cursor: default; +} +.laydate-time-list p { + position: relative; + top: -4px; + line-height: 29px; +} +.laydate-time-list ol { + height: 181px; + overflow: hidden; +} +.laydate-time-list > li:hover ol { + overflow-y: auto; +} + +.laydate-time-list ol li { + width: 130%; + padding-left: 4px; + height: 30px; + line-height: 30px; + text-align: left; + cursor: pointer; +} + +/* 提示 */ +.layui-laydate-hint { + top: 115px; + left: 50%; + width: 250px; + margin-left: -125px; + line-height: 20px; + padding: 15px; + text-align: center; + font-size: 12px; + color: #ff5722; +} + +/* 双日历 */ +.layui-laydate-range { + min-width: 546px; +} +.layui-laydate-range .layui-laydate-main { + display: inline-block; + vertical-align: middle; +} +.layui-laydate-range .laydate-main-list-1 .layui-laydate-header, +.layui-laydate-range .laydate-main-list-1 .layui-laydate-content { + border-left: 1px solid #e2e2e2; +} + +/* 默认简约主题 */ +.layui-laydate, +.layui-laydate-hint { + background-color: #fff; + color: #666; +} +.layui-laydate-header { + border-bottom: 1px solid #e2e2e2; +} +.layui-laydate-header i:hover, +.layui-laydate-header span:hover { + color: #5fb878; +} +.layui-laydate-content { + border-top: none 0; + border-bottom: none 0; +} +.layui-laydate-content th { + color: #333; +} +.layui-laydate-content td { + color: #666; +} +.layui-laydate-content td.laydate-selected { + background-color: #b5fff8; +} +.laydate-selected:hover { + background-color: #00f7de !important; +} +.layui-laydate-content td:hover, +.layui-laydate-list li:hover { + background-color: #eee; + color: #333; +} +.laydate-time-list li ol { + margin: 0; + padding: 0; + border: 1px solid #e2e2e2; +} +.laydate-time-list li:first-child ol { + border-left-width: 1px; +} +.laydate-time-list > li:hover { + background: none; +} +.layui-laydate-content .laydate-day-prev, +.layui-laydate-content .laydate-day-next { + color: #d2d2d2; +} +.laydate-selected.laydate-day-prev, +.laydate-selected.laydate-day-next { + background-color: #f8f8f8 !important; +} +.layui-laydate-footer { + border-top: 1px solid #e2e2e2; +} +.layui-laydate-hint { + color: #ff5722; +} +.laydate-day-mark::after { + background-color: #5fb878; +} +.layui-laydate-content td.layui-this .laydate-day-mark::after { + display: none; +} +.layui-laydate-footer span[lay-type="date"] { + color: #5fb878; +} +.layui-laydate .layui-this { + background-color: var(--global-primary-color) !important; + color: #fff !important; +} + +.layui-laydate .laydate-disabled, +.layui-laydate .laydate-disabled:hover { + background: none !important; + color: #d2d2d2 !important; + cursor: not-allowed !important; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +/* 墨绿/自定义背景色主题 */ +.laydate-theme-molv { + border: none; +} +.laydate-theme-molv.layui-laydate-range { + width: 548px; +} +.laydate-theme-molv .layui-laydate-main { + width: 274px; +} +.laydate-theme-molv .layui-laydate-header { + border: none; + background-color: #009688; +} +.laydate-theme-molv .layui-laydate-header i, +.laydate-theme-molv .layui-laydate-header span { + color: #f6f6f6; +} +.laydate-theme-molv .layui-laydate-header i:hover, +.laydate-theme-molv .layui-laydate-header span:hover { + color: #fff; +} +.laydate-theme-molv .layui-laydate-content { + border: 1px solid #e2e2e2; + border-top: none; + border-bottom: none; +} +.laydate-theme-molv .laydate-main-list-1 .layui-laydate-content { + border-left: none; +} +.laydate-theme-molv .layui-laydate-footer { + border: 1px solid #e2e2e2; +} + +/* 格子主题 */ +.laydate-theme-grid .layui-laydate-content td, +.laydate-theme-grid .layui-laydate-content thead, +.laydate-theme-grid .laydate-year-list > li, +.laydate-theme-grid .laydate-month-list > li { + border: 1px solid #e2e2e2; +} +.laydate-theme-grid .laydate-selected, +.laydate-theme-grid .laydate-selected:hover { + background-color: #f2f2f2 !important; + color: #009688 !important; +} +.laydate-theme-grid .laydate-selected.laydate-day-prev, +.laydate-theme-grid .laydate-selected.laydate-day-next { + color: #d2d2d2 !important; +} +.laydate-theme-grid .laydate-year-list, +.laydate-theme-grid .laydate-month-list { + margin: 1px 0 0 1px; +} +.laydate-theme-grid .laydate-year-list > li, +.laydate-theme-grid .laydate-month-list > li { + margin: 0 -1px -1px 0; +} +.laydate-theme-grid .laydate-year-list > li { + height: 43px; + line-height: 43px; +} +.laydate-theme-grid .laydate-month-list > li { + height: 71px; + line-height: 71px; +} +.laydate-range-hover { + background-color: var(--global-neutral-color-2) !important; +} +.layui-laydate-content + .layui-disabled:not(.layui-laydate-current, .layui-this):hover { + background-color: transparent !important; +} + +.laydate-range-inputs { + display: flex; + align-items: center; + height: 100%; + border-width: 1px; + box-sizing: border-box; + width: 100%; + border-style: solid; + overflow: hidden; + display: inline-flex; + border-color: var(--input-border-color); + border-radius: var(--input-border-radius); + &:hover { + border-color: #d2d2d2; + } + .range-separator { + margin: 0 5px; + color: var(--global-neutral-color-8); + background-color: transparent; + } + .layui-input-wrapper { + border: none; + box-sizing: border-box; + input { + text-align: center; + padding: 0; + } + } + .layui-input { + border: none; + } +} +.layui-laydate-range { + &-main { + display: flex; + } + .laydate-set-ym { + overflow: visible; + white-space: nowrap; + line-height: 46px; + } + .laydate-set-ym .layui-dropdown { + width: auto !important; + } + + .time-panel { + .layui-laydate-main { + width: 272px; + display: unset !important; + } + .layui-laydate-preview { + display: none; + } + } + .layui-laydate-content { + .laydate-year-list { + display: flex; + flex-wrap: wrap; + } + } + .layui-laydate-list { + display: flex; + flex-wrap: wrap; + } +} +.layui-laydate-range-datetime { + .layui-laydate-main { + width: 340px; + } +} +.layui-laydate-current { + background-color: var(--global-neutral-color-3); +} + +.laydate-time-list > li:hover ol::-webkit-scrollbar { + width: 0px; +} +.layui-laydate-content::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.layui-laydate-content::-webkit-scrollbar-thumb { + border-radius: 10px; + background-color: #eeeeee; +} +.layui-laydate-content::-webkit-scrollbar-thumb:hover { + background-color: #dddddd; +} + +.layui-date-range-picker { + .end-input { + .layui-input-clear { + opacity: 0; + visibility: hidden; + display: block; + padding: 0 10px; + } + } + + .laydate-range-inputs { + &:hover { + .end-input { + .layui-input-clear { + opacity: 1; + visibility: visible; + } + } + } + } +} + +.layui-laydate-shortcut { + width: 80px; + padding: 6px 0; + display: inline-block; + vertical-align: top; + overflow: auto; + max-height: 276px; + text-align: center; +} + +.layui-laydate-shortcut + .layui-laydate-main { + display: inline-block; + border-left: 1px solid #e2e2e2; +} + +.layui-laydate-shortcut > li { + padding: 5px 8px; + cursor: pointer; + line-height: 18px; + user-select: none; + + &:hover { + background-color: #eee; + color: #333; + transition: all 0.3s; + } +} +/* 静态面板 */ +// .layui-date-picker-static { +// width: auto !important; +// height: 100% !important; +// border: 1px solid var(--input-border-color); +// } diff --git a/packages/component/component/datePicker/index.vue b/packages/component/component/datePicker/index.vue index e804fd94c4c4d92a407615fc0ec83d81acc833eb..a40d5f255732c54b13950de476310d3a06745087 100644 --- a/packages/component/component/datePicker/index.vue +++ b/packages/component/component/datePicker/index.vue @@ -1,136 +1,32 @@ - - - - - - - - - {{ rangeSeparator }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/packages/component/component/datePicker/interface.ts b/packages/component/component/datePicker/interface.ts index 3087b1ac1344b3485a1896b055aca6dd2470ff63..5e02c6f201e602347fdb5576390c239aa8c9f2b5 100644 --- a/packages/component/component/datePicker/interface.ts +++ b/packages/component/component/datePicker/interface.ts @@ -1,57 +1,109 @@ -import type { Ref, StyleValue } from "vue"; - -export type DatePickerType = "date" | "datetime" | "year" | "time" | "month"; - -export type provideType = { - currentYear: Ref; - currentMonth: Ref; - currentDay: Ref; - dateValue: Ref; - hms: Ref; - type: string; - showPanel: Ref; - clear: Function; - now: Function; - ok: Function; - range: boolean; - rangeValue: { - first: string; - last: string; - hover: string; - firstTime: { hh: number; mm: number; ss: number }; - lastTime: { hh: number; mm: number; ss: number }; +import type { Dayjs, ConfigType } from "dayjs"; +import type { StyleValue, Component, InjectionKey } from "vue"; +import type { CommonSize, CommonClass } from "../../types/common"; + +export type DatePickerType = + | "date" + | "datetime" + | "year" + | "month" + | "time" + | "yearmonth"; + +export type TypeMap = { + [k in DatePickerType]: { + component: Component; + format: string; }; - rangeSeparator: string; - simple: boolean; - timestamp: boolean; - max: Ref; - min: Ref; - defaultTime: string | string[]; - yearPage: Ref; - yearStep: Ref; }; +export type DatePickerModelValueSingleType = ConfigType; + export interface DatePickerProps { name?: string; - modelValue?: string | number | string[]; - type?: "date" | "datetime" | "year" | "month" | "time" | "yearmonth"; + modelValue?: + | DatePickerModelValueSingleType + | Array; + type?: DatePickerType; disabled?: boolean; readonly?: boolean; - placeholder?: string | string[]; + placeholder?: string | Array; allowClear?: boolean; simple?: boolean; max?: string; min?: string; range?: boolean; rangeSeparator?: string; - size?: "lg" | "md" | "sm" | "xs"; + size?: CommonSize; prefixIcon?: string; suffixIcon?: string; timestamp?: boolean; format?: string; - defaultTime?: string | string[]; + inputFormat?: string; + defaultValue?: + | DatePickerModelValueSingleType + | Array; + defaultTime?: + | DatePickerModelValueSingleType + | Array; contentStyle?: StyleValue; - contentClass?: string | Array | object; + contentClass?: CommonClass; + disabledDate?: (date: Date) => boolean; + /** + * 年份分页 + * @version 2.19.0 + */ yearPage?: number; - yearStep?: number; + // yearStep?: number; + /** + * 快捷选项 + * @version 2.19.0 + */ + shortcuts?: Array; + // /** + // * 静态面板 + // * @version 2.19.0 + // */ + // static?: boolean; +} + +export interface RequiredDatePickerProps extends DatePickerProps { + size: CommonSize; + type: DatePickerType; + disabled: boolean; + readonly: boolean; + allowClear: boolean; + simple: boolean; + range: boolean; + rangeSeparator: string; + prefixIcon: string; + suffixIcon: string; + timestamp: boolean; + yearPage: number; +} + +export type DatePickerEmits = { + (e: "update:modelValue", value: string | Array): void; + (e: "change", value: string | Array): void; + (e: "blur", event: Event): void; + (e: "focus", event: Event): void; + (e: "clear"): void; +}; + +export type DatePickerValue = Dayjs | Array | null | undefined; + +export interface DatePickerContextType extends RequiredDatePickerProps { + modelValue: DatePickerValue; +} + +export const DATE_PICKER_CONTEXT: InjectionKey = + Symbol("LayDatePicker"); + +export interface Shortcuts { + text: string | number; + value: + | DatePickerModelValueSingleType + | Array + | (() => DatePickerModelValueSingleType) + | (() => Array); } diff --git a/packages/component/component/datePicker/day.ts b/packages/component/component/datePicker/util.ts similarity index 52% rename from packages/component/component/datePicker/day.ts rename to packages/component/component/datePicker/util.ts index 9cadb13ab0cb4b04c1f1e4f8a39ec577ccf98a73..8ed6848e749f60437185012eaea0806b6b3e3f0e 100644 --- a/packages/component/component/datePicker/day.ts +++ b/packages/component/component/datePicker/util.ts @@ -1,24 +1,66 @@ +import dayjs, { type Dayjs } from "dayjs"; +import type { DatePickerModelValueSingleType } from "./interface"; +import { DateContentSingleDateObject } from "./component/interface"; + +export const normalizeDayjsValue = ( + value: DatePickerModelValueSingleType, + format: string | undefined +): Dayjs | null => { + const date = dayjs(value, format); + + return dayjs(value).isValid() ? dayjs(value) : date.isValid() ? date : null; +}; + +export const dayjsToString = ( + value: DatePickerModelValueSingleType, + format: string +) => { + const date = dayjs(value, format); + + return dayjs(value).isValid() + ? dayjs(value).format(format) + : date.isValid() + ? date.format(format) + : ""; +}; + +export const checkRangeValue = (values: Array) => { + const [start, end] = values; + if (!start || !end) return false; + return start.isValid() && end.isValid(); +}; + /** * 获取年份列表 * @param {Date | number} date 时间 * @param {Number} page 页数 * @param {Number} step 步进 */ -const getYears = (date?: Date | number, page = 15, step = 1) => { +export const getYears = (date?: Date | number, page = 15, step = 1) => { const years = []; const y = typeof date === "number" ? date : date?.getFullYear() ?? 1970; - console.log(y, page, step); - const r = (page % 2 ? page - 1 : page) / 2; - for (let i = y - r; i <= y + r; i += step) { + const currentIndex = getPosition(y, page); + + for ( + let i = y - (currentIndex - 1); + i <= y + page - currentIndex; + i += step + ) { years.push(i); } return years; }; +const getPosition = (year: number, length: number): number => { + if (year === 0) return length; + + return ((((year - 1) % length) + length) % length) + 1; +}; + /** * 获取当前日期 */ -const getDate = (val = "") => { +export const getDate = (val = "") => { if (val) { return new Date(val); } else { @@ -29,18 +71,18 @@ const getDate = (val = "") => { /** * 获取当前年份 */ -const getYear = (val = "") => { +export const getYear = (val = "") => { return getDate(val).getFullYear(); }; /** * 获取当前月份 */ -const getMonth = (val = "") => { +export const getMonth = (val = "") => { return getDate(val).getMonth(); }; -const getDay = (val = "") => { +export const getDay = (val = "") => { if (val) { return new Date(getDate(val).toDateString()).getTime(); } else { @@ -54,16 +96,20 @@ const getDay = (val = "") => { * @param year * @param month */ -const getDayLength = (year: number, month: number): number => { +export const getDayLength = (year: number, month: number): number => { return new Date(year, month + 1, 0).getDate(); }; // 设置日期列表 -const setDateList = (year: number, month: number) => { +export const setDateList = ( + year: number, + month: number +): Array => { const curDays = getDayLength(year, month); // 当月天数 const prevDays = getDayLength(year, month - 1); // 上月天数 const curFirstDayWeek = new Date(year, month, 1).getDay(); // 当月第一天星期几 - const list: any[] = []; + const list: Array = []; + // 填充上月天数 for (let i = prevDays - curFirstDayWeek + 1; i <= prevDays; i++) { list.push({ @@ -99,13 +145,3 @@ const setDateList = (year: number, month: number) => { } return list; }; - -export { - getDayLength, - getYears, - getDate, - getMonth, - getYear, - getDay, - setDateList, -}; diff --git a/packages/component/types/common.ts b/packages/component/types/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf9c011cddb04387035f6cf3d6cf00d2ab6ee0ac --- /dev/null +++ b/packages/component/types/common.ts @@ -0,0 +1,3 @@ +export type CommonSize = "lg" | "md" | "sm" | "xs"; + +export type CommonClass = string | Array | object; diff --git a/packages/component/utils/index.ts b/packages/component/utils/index.ts index 7417170b0abdf3a08727b25f0a14fb81abbe1881..532c313eb179e11ed12b968d008a73f06f2649ec 100644 --- a/packages/component/utils/index.ts +++ b/packages/component/utils/index.ts @@ -4,3 +4,4 @@ export * from "./arrayUtil"; export * from "./vueUtil"; export * from "./treeUtil"; export * from "./uuidUtil"; +export * from "./type"; diff --git a/packages/component/utils/type.ts b/packages/component/utils/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d39039ffe8b63ff1a7344a83c92bdacacdcead3 --- /dev/null +++ b/packages/component/utils/type.ts @@ -0,0 +1,23 @@ +export const isFunction = (val: unknown): val is Function => + typeof val === "function"; + +export const isUndefined = (val: any): val is undefined => val === undefined; + +export const isNumber = (val: any): val is number => typeof val === "number"; + +export const isString = (val: any): val is string => typeof val === "string"; +export const isDate = (val: Date): boolean => + val instanceof Date && !isNaN(val.valueOf()); + +/** + * 过滤出非undefined | null | '' 的集合 + * @param {any[]} val 数据源 + * @returns {any[]} 过滤后数据 + */ +export const normalizeValue = (val: any[]): any => { + return val.filter((v) => v !== undefined && v !== null && v !== ""); +}; + +export const isNil = (val: any): boolean => { + return val === undefined || val === null; +}; diff --git a/packages/component/utils/vueUtil.ts b/packages/component/utils/vueUtil.ts index f644fb5c11aba154532e97c89682c1642c81aad9..59c636c95ef07853baeb4788df51fa32d0380ac5 100644 --- a/packages/component/utils/vueUtil.ts +++ b/packages/component/utils/vueUtil.ts @@ -55,27 +55,3 @@ export function kebabCase(key: string) { const result = key.replace(/([A-Z])/g, " $1").trim(); return result.split(" ").join("-").toLowerCase(); } - -export const isFunction = (val: unknown): val is Function => - typeof val === "function"; - -export const isUndefined = (val: any): val is undefined => val === undefined; - -export const isNumber = (val: any): val is number => typeof val === "number"; - -export const isString = (val: any): val is string => typeof val === "string"; -export const isDate = (val: Date): boolean => - val instanceof Date && !isNaN(val.valueOf()); - -/** - * 过滤出非undefined | null | '' 的集合 - * @param {any[]} val 数据源 - * @returns {any[]} 过滤后数据 - */ -export const normalizeValue = (val: any[]): any => { - return val.filter((v) => v !== undefined && v !== null && v !== ""); -}; - -export const isNil = (val: any): boolean => { - return val === undefined || val === null; -}; diff --git a/packages/json-schema-form/src/component/__tests__/form.test.tsx b/packages/json-schema-form/src/component/__tests__/form.test.tsx index 6416b3beea1bc90e2db1d5ce98df67957e40effa..41dbbb8adafa4324fac72fc1ac4fd1e606169267 100644 --- a/packages/json-schema-form/src/component/__tests__/form.test.tsx +++ b/packages/json-schema-form/src/component/__tests__/form.test.tsx @@ -190,6 +190,7 @@ describe("LayJsonSchemaForm", () => { inputDom.setValue("456"); dateDom.setValue("2024-03-05"); await nextTick(); + await nextTick(); expect(inputDom.element.value).toBe("456"); expect(dateDom.element.value).toBe("2024-03-05");