Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add calendar #112

Merged
merged 1 commit into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading