Skip to content

Commit

Permalink
Merge pull request #112 from valcosmos/dev
Browse files Browse the repository at this point in the history
feat: add calendar
  • Loading branch information
valcosmos authored Feb 11, 2024
2 parents be9d8af + b1bc14d commit 25c47c9
Show file tree
Hide file tree
Showing 13 changed files with 469 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
77 changes: 77 additions & 0 deletions src/components/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -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<Dayjs>(value)

const [curMonth, setCurMonth] = useState<Dayjs>(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 (
<LocaleContext.Provider
value={{
locale: locale || navigator.language,
}}
>
<div className={classNames} style={style}>
<Header
curMonth={curMonth}
prevMonthHandler={prevMonthHandler}
nextMonthHandler={nextMonthHandler}
todayHandler={todayHandler}
/>
<MonthCalendar
{...props}
value={curValue}
curMonth={curMonth}
selectHandler={selectHandler}
/>
</div>
</LocaleContext.Provider>
)
}

export default Calendar
35 changes: 35 additions & 0 deletions src/components/Calendar/Header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="calendar-header">
<div className="calendar-header-left">
<div className="calendar-header-icon" onClick={prevMonthHandler}>
&lt;
</div>
<div className="calendar-header-value">{curMonth.format(CalendarContext.formatMonth)}</div>
<div className="calendar-header-icon" onClick={nextMonthHandler}>
&gt;
</div>
<button className="calendar-header-btn" onClick={todayHandler}>
{CalendarContext.today}
</button>
</div>
</div>
)
}

export default Header
11 changes: 11 additions & 0 deletions src/components/Calendar/LocaleContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext } from 'react'

export interface LocaleContextType {
locale: string
}

const LocaleContext = createContext<LocaleContextType>({
locale: 'zh-CN',
})

export default LocaleContext
108 changes: 108 additions & 0 deletions src/components/Calendar/MonthCalendar.tsx
Original file line number Diff line number Diff line change
@@ -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] = (
<div
className={
'calendar-month-body-cell ' +
(item.currentMonth ? 'calendar-month-body-cell-current' : '')
}
onClick={() => selectHandler?.(item.date)}
>
{dateRender ? (
dateRender(item.date)
) : (
<div className="calendar-month-body-cell-date">
<div
className={cs(
'calendar-month-body-cell-date-value',
value.format('YYYY-MM-DD') === item.date.format('YYYY-MM-DD')
? 'calendar-month-body-cell-date-selected'
: '',
)}
>
{item.date.date()}
</div>
<div className="calendar-month-cell-body-date-content">
{dateInnerContent?.(item.date)}
</div>
</div>
)}
</div>
)
}
rows.push(row)
}
return rows.map((row, index) => (
<div className="calendar-month-body-row" key={index}>
{row}
</div>
))
}

return (
<div className="calendar-month">
<div className="calendar-month-week-list">
{weekList.map(week => (
<div className="calendar-month-week-list-item" key={week}>
{CalendarLocale.week[week]}
</div>
))}
</div>
<div className="calendar-month-body">{renderDays(allDays)}</div>
</div>
)
}

export default MonthCalendar
27 changes: 27 additions & 0 deletions src/components/Calendar/calendar.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 => (
<div style={{ padding: '20px' }}>
<Story />
</div>
),
],
} satisfies Meta<typeof Calendar>

export default meta

const Template: StoryFn<typeof Calendar> = args => <Calendar {...args} />

export const BasicCalendar = Template.bind({})
BasicCalendar.args = {
value: dayjs('2024-02-11'),
}
105 changes: 105 additions & 0 deletions src/components/Calendar/index.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}

}
}
3 changes: 3 additions & 0 deletions src/components/Calendar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Calendar from './Calendar'

export default Calendar
Loading

0 comments on commit 25c47c9

Please sign in to comment.