From b1bc14d18f9bd4b5192cb819be5f0f962ba7325e Mon Sep 17 00:00:00 2001 From: Cupid Valentine Date: Sun, 11 Feb 2024 22:39:24 +0800 Subject: [PATCH] feat: add calendar --- package.json | 1 + src/components/Calendar/Calendar.tsx | 77 +++++++++++++ src/components/Calendar/Header.tsx | 35 ++++++ src/components/Calendar/LocaleContext.tsx | 11 ++ src/components/Calendar/MonthCalendar.tsx | 108 +++++++++++++++++++ src/components/Calendar/calendar.stories.tsx | 27 +++++ src/components/Calendar/index.scss | 105 ++++++++++++++++++ src/components/Calendar/index.tsx | 3 + src/components/Calendar/locale/en-US.ts | 32 ++++++ src/components/Calendar/locale/index.ts | 10 ++ src/components/Calendar/locale/interface.ts | 28 +++++ src/components/Calendar/locale/zh-CN.ts | 32 ++++++ vitest-setup.ts | 1 - 13 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 src/components/Calendar/Calendar.tsx create mode 100644 src/components/Calendar/Header.tsx create mode 100644 src/components/Calendar/LocaleContext.tsx create mode 100644 src/components/Calendar/MonthCalendar.tsx create mode 100644 src/components/Calendar/calendar.stories.tsx create mode 100644 src/components/Calendar/index.scss create mode 100644 src/components/Calendar/index.tsx create mode 100644 src/components/Calendar/locale/en-US.ts create mode 100644 src/components/Calendar/locale/index.ts create mode 100644 src/components/Calendar/locale/interface.ts create mode 100644 src/components/Calendar/locale/zh-CN.ts diff --git a/package.json b/package.json index 2de311a..0baae7c 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "async-validator": "^4.2.5", "axios": "^1.6.7", "classnames": "^2.5.1", + "dayjs": "^1.11.10", "lodash-es": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx new file mode 100644 index 0000000..160f0a6 --- /dev/null +++ b/src/components/Calendar/Calendar.tsx @@ -0,0 +1,77 @@ +import dayjs, { Dayjs } from 'dayjs' +import MonthCalendar from './MonthCalendar' +import './index.scss' +import Header from './Header' +import { CSSProperties, ReactNode, useState } from 'react' +import cs from 'classnames' +import LocaleContext from './LocaleContext' + +export interface CalendarProps { + value: Dayjs + style?: CSSProperties + className?: string | string[] + // 定制日期显示,会完全覆盖日期单元格 + dateRender?: (currentDate: Dayjs) => ReactNode + // 定制日期单元格,内容会被添加到单元格内,只在全屏日历模式下生效。 + dateInnerContent?: (currentDate: Dayjs) => ReactNode + // 国际化相关 + locale?: string + onChange?: (date: Dayjs) => void +} + +export function Calendar(props: CalendarProps) { + const { value, style, className, locale, onChange } = props + + const [curValue, setCurValue] = useState(value) + + const [curMonth, setCurMonth] = useState(value) + + const classNames = cs('calendar', className) + + function selectHandler(date: Dayjs) { + setCurValue(date) + setCurMonth(date) + onChange?.(date) + } + + function prevMonthHandler() { + setCurMonth(curMonth.subtract(1, 'month')) + } + + function nextMonthHandler() { + setCurMonth(curMonth.add(1, 'month')) + } + + function todayHandler() { + const date = dayjs(Date.now()) + + setCurValue(date) + setCurMonth(date) + onChange?.(date) + } + + return ( + +
+
+ +
+
+ ) +} + +export default Calendar diff --git a/src/components/Calendar/Header.tsx b/src/components/Calendar/Header.tsx new file mode 100644 index 0000000..3a60ef1 --- /dev/null +++ b/src/components/Calendar/Header.tsx @@ -0,0 +1,35 @@ +import { Dayjs } from 'dayjs' +import { useContext } from 'react' +import LocaleContext from './LocaleContext' +import allLocales from './locale' +interface HeaderProps { + curMonth: Dayjs + prevMonthHandler: () => void + nextMonthHandler: () => void + todayHandler: () => void +} +function Header(props: HeaderProps) { + const { curMonth, prevMonthHandler, nextMonthHandler, todayHandler } = props + + const localeContext = useContext(LocaleContext) + const CalendarContext = allLocales[localeContext.locale] + + return ( +
+
+
+ < +
+
{curMonth.format(CalendarContext.formatMonth)}
+
+ > +
+ +
+
+ ) +} + +export default Header diff --git a/src/components/Calendar/LocaleContext.tsx b/src/components/Calendar/LocaleContext.tsx new file mode 100644 index 0000000..37c4d74 --- /dev/null +++ b/src/components/Calendar/LocaleContext.tsx @@ -0,0 +1,11 @@ +import { createContext } from 'react' + +export interface LocaleContextType { + locale: string +} + +const LocaleContext = createContext({ + locale: 'zh-CN', +}) + +export default LocaleContext diff --git a/src/components/Calendar/MonthCalendar.tsx b/src/components/Calendar/MonthCalendar.tsx new file mode 100644 index 0000000..6e266bd --- /dev/null +++ b/src/components/Calendar/MonthCalendar.tsx @@ -0,0 +1,108 @@ +import { Dayjs } from 'dayjs' +import { CalendarProps } from '.' +import LocaleContext from './LocaleContext' +import { useContext } from 'react' +import allLocales from './locale' +import cs from 'classnames' + +interface MonthCalendarProps extends CalendarProps { + selectHandler?: (date: Dayjs) => void + curMonth: Dayjs +} + +function getAllDays(date: Dayjs) { + const startDate = date.startOf('month') + const day = startDate.day() + + const daysInfo: Array<{ date: Dayjs; currentMonth: boolean }> = new Array(6 * 7) + + for (let i = 0; i < day; i++) { + daysInfo[i] = { + date: startDate.subtract(day - i, 'day'), + currentMonth: false, + } + } + + for (let i = day; i < daysInfo.length; i++) { + const calcDate = startDate.add(i - day, 'day') + + daysInfo[i] = { + date: calcDate, + currentMonth: calcDate.month() === date.month(), + } + } + + return daysInfo +} + +function MonthCalendar(props: MonthCalendarProps) { + const localeContext = useContext(LocaleContext) + + const { value, curMonth, dateRender, dateInnerContent, selectHandler } = props + + const CalendarLocale = allLocales[localeContext.locale] + + const weekList = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] + + const allDays = getAllDays(curMonth) + + function renderDays(days: Array<{ date: Dayjs; currentMonth: boolean }>) { + const rows = [] + for (let i = 0; i < 6; i++) { + const row = [] + for (let j = 0; j < 7; j++) { + const item = days[i * 7 + j] + row[j] = ( +
selectHandler?.(item.date)} + > + {dateRender ? ( + dateRender(item.date) + ) : ( +
+
+ {item.date.date()} +
+
+ {dateInnerContent?.(item.date)} +
+
+ )} +
+ ) + } + rows.push(row) + } + return rows.map((row, index) => ( +
+ {row} +
+ )) + } + + return ( +
+
+ {weekList.map(week => ( +
+ {CalendarLocale.week[week]} +
+ ))} +
+
{renderDays(allDays)}
+
+ ) +} + +export default MonthCalendar diff --git a/src/components/Calendar/calendar.stories.tsx b/src/components/Calendar/calendar.stories.tsx new file mode 100644 index 0000000..b8e3372 --- /dev/null +++ b/src/components/Calendar/calendar.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { Meta, StoryFn } from '@storybook/react' + +import Calendar from '.' +import dayjs from 'dayjs' + +const meta = { + title: 'Data Display/Calendar 组件', + id: 'calendar', + component: Calendar, + decorators: [ + Story => ( +
+ +
+ ), + ], +} satisfies Meta + +export default meta + +const Template: StoryFn = args => + +export const BasicCalendar = Template.bind({}) +BasicCalendar.args = { + value: dayjs('2024-02-11'), +} diff --git a/src/components/Calendar/index.scss b/src/components/Calendar/index.scss new file mode 100644 index 0000000..a0687b5 --- /dev/null +++ b/src/components/Calendar/index.scss @@ -0,0 +1,105 @@ +.calendar { + width: 100%; +} + +.calendar-header { + &-left { + display: flex; + align-items: center; + + height: 28px; + line-height: 28px; + } + + &-value { + font-size: 20px; + } + + &-btn { + background: #eee; + cursor: pointer; + border: 0; + padding: 0 15px; + line-height: 28px; + + &:hover { + background: #ccc; + } + } + + &-icon { + width: 28px; + height: 28px; + + line-height: 28px; + + border-radius: 50%; + text-align: center; + font-size: 12px; + + user-select: none; + cursor: pointer; + + margin-right: 12px; + + &:not(:first-child) { + margin: 0 12px; + } + + &:hover { + background: #ccc; + } + } + +} + +.calendar-month { + &-week-list { + display: flex; + padding: 0; + width: 100%; + box-sizing: border-box; + border-bottom: 1px solid #eee; + + &-item { + padding: 20px 16px; + text-align: left; + color: #7d7d7f; + flex: 1; + } + } + + &-body { + &-row { + height: 100px; + display: flex; + } + + &-cell { + flex: 1; + border: 1px solid #eee; + color: #ccc; + overflow: hidden; + + &-current { + color: #000; + } + + &-date { + padding: 10px; + + &-selected { + background: blue; + width: 28px; + height: 28px; + line-height: 28px; + text-align: center; + color: #fff; + border-radius: 50%; + cursor: pointer; + } + } + } + + } +} \ No newline at end of file diff --git a/src/components/Calendar/index.tsx b/src/components/Calendar/index.tsx new file mode 100644 index 0000000..f7eddf6 --- /dev/null +++ b/src/components/Calendar/index.tsx @@ -0,0 +1,3 @@ +import Calendar from './Calendar' + +export default Calendar diff --git a/src/components/Calendar/locale/en-US.ts b/src/components/Calendar/locale/en-US.ts new file mode 100644 index 0000000..12106a3 --- /dev/null +++ b/src/components/Calendar/locale/en-US.ts @@ -0,0 +1,32 @@ +import { CalendarType } from './interface' + +const CalendarLocale: CalendarType = { + formatYear: 'YYYY', + formatMonth: 'MMM YYYY', + today: 'Today', + month: { + January: 'January', + February: 'February', + March: 'March', + April: 'April', + May: 'May', + June: 'June', + July: 'July', + August: 'August', + September: 'September', + October: 'October', + November: 'November', + December: 'December', + }, + week: { + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday', + sunday: 'Sunday', + }, +} + +export default CalendarLocale diff --git a/src/components/Calendar/locale/index.ts b/src/components/Calendar/locale/index.ts new file mode 100644 index 0000000..48126c5 --- /dev/null +++ b/src/components/Calendar/locale/index.ts @@ -0,0 +1,10 @@ +import zhCN from './zh-CN' +import enUS from './en-US' +import { CalendarType } from './interface' + +const allLocales: Record = { + 'zh-CN': zhCN, + 'en-US': enUS, +} + +export default allLocales diff --git a/src/components/Calendar/locale/interface.ts b/src/components/Calendar/locale/interface.ts new file mode 100644 index 0000000..dfbd738 --- /dev/null +++ b/src/components/Calendar/locale/interface.ts @@ -0,0 +1,28 @@ +export interface CalendarType { + formatYear: string + formatMonth: string + today: string + month: { + January: string + February: string + March: string + April: string + May: string + June: string + July: string + August: string + September: string + October: string + November: string + December: string + } & Record + week: { + monday: string + tuesday: string + wednesday: string + thursday: string + friday: string + saturday: string + sunday: string + } & Record +} diff --git a/src/components/Calendar/locale/zh-CN.ts b/src/components/Calendar/locale/zh-CN.ts new file mode 100644 index 0000000..e827aab --- /dev/null +++ b/src/components/Calendar/locale/zh-CN.ts @@ -0,0 +1,32 @@ +import { CalendarType } from './interface' + +const CalendarLocale: CalendarType = { + formatYear: 'YYYY 年', + formatMonth: 'YYYY 年 MM 月', + today: '今天', + month: { + January: '一月', + February: '二月', + March: '三月', + April: '四月', + May: '五月', + June: '六月', + July: '七月', + August: '八月', + September: '九月', + October: '十月', + November: '十一月', + December: '十二月', + }, + week: { + monday: '周一', + tuesday: '周二', + wednesday: '周三', + thursday: '周四', + friday: '周五', + saturday: '周六', + sunday: '周日', + }, +} + +export default CalendarLocale diff --git a/vitest-setup.ts b/vitest-setup.ts index d29477e..3e91332 100644 --- a/vitest-setup.ts +++ b/vitest-setup.ts @@ -11,4 +11,3 @@ // }) import '@testing-library/jest-dom/vitest' -