feat(Calendar): 新增公共日历组件 BaseCalendar

This commit is contained in:
zdl
2025-12-23 17:44:20 +08:00
parent 068d59634b
commit 39fb70a1eb
4 changed files with 541 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
/**
* BaseCalendar - 基础日历组件
* 封装 Ant Design Calendar提供统一的黑金主题和接口
*/
import React, { useCallback } from 'react';
import { Calendar, ConfigProvider, Button } from 'antd';
import type { CalendarProps } from 'antd';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import zhCN from 'antd/locale/zh_CN';
import { Box, HStack, Text } from '@chakra-ui/react';
import { CALENDAR_THEME, CALENDAR_COLORS, CALENDAR_STYLES } from './theme';
dayjs.locale('zh-cn');
/**
* 单元格渲染信息
*/
export interface CellRenderInfo {
type: 'date' | 'month';
isToday: boolean;
isCurrentMonth: boolean;
}
/**
* BaseCalendar Props
*/
export interface BaseCalendarProps {
/** 当前选中日期 */
value?: Dayjs;
/** 日期变化回调(月份切换等) */
onChange?: (date: Dayjs) => void;
/** 日期选择回调(点击日期) */
onSelect?: (date: Dayjs) => void;
/** 自定义单元格内容渲染 */
cellRender?: (date: Dayjs, info: CellRenderInfo) => React.ReactNode;
/** 日历高度 */
height?: string | number;
/** 是否显示工具栏 */
showToolbar?: boolean;
/** 工具栏标题格式 */
titleFormat?: string;
/** 额外的 className */
className?: string;
}
/**
* 默认工具栏组件
*/
const CalendarToolbar: React.FC<{
value: Dayjs;
onChange: (date: Dayjs) => void;
titleFormat?: string;
}> = ({ value, onChange, titleFormat = 'YYYY年M月' }) => {
const handlePrev = () => onChange(value.subtract(1, 'month'));
const handleNext = () => onChange(value.add(1, 'month'));
const handleToday = () => onChange(dayjs());
return (
<HStack justify="flex-start" mb={4} px={2} spacing={4}>
<HStack spacing={2}>
<Button
type="primary"
icon={<LeftOutlined />}
onClick={handlePrev}
style={{
backgroundColor: CALENDAR_STYLES.toolbar.buttonBg,
borderColor: CALENDAR_STYLES.toolbar.buttonBg,
color: CALENDAR_STYLES.toolbar.buttonColor,
}}
/>
<Button
type="primary"
icon={<RightOutlined />}
onClick={handleNext}
style={{
backgroundColor: CALENDAR_STYLES.toolbar.buttonBg,
borderColor: CALENDAR_STYLES.toolbar.buttonBg,
color: CALENDAR_STYLES.toolbar.buttonColor,
}}
/>
<Button
type="primary"
onClick={handleToday}
style={{
backgroundColor: CALENDAR_STYLES.toolbar.buttonBg,
borderColor: CALENDAR_STYLES.toolbar.buttonBg,
color: CALENDAR_STYLES.toolbar.buttonColor,
}}
>
</Button>
</HStack>
<Text
fontSize={{ base: 'lg', md: 'xl' }}
fontWeight="700"
bgGradient={`linear(135deg, ${CALENDAR_COLORS.gold.primary} 0%, #F5E6A3 100%)`}
bgClip="text"
>
{value.format(titleFormat)}
</Text>
</HStack>
);
};
/**
* BaseCalendar 组件
*/
export const BaseCalendar: React.FC<BaseCalendarProps> = ({
value,
onChange,
onSelect,
cellRender,
height = '100%',
showToolbar = true,
titleFormat = 'YYYY年M月',
className,
}) => {
const [currentValue, setCurrentValue] = React.useState<Dayjs>(value || dayjs());
// 同步外部 value
React.useEffect(() => {
if (value) {
setCurrentValue(value);
}
}, [value]);
// 处理日期变化
const handleChange = useCallback((date: Dayjs) => {
setCurrentValue(date);
onChange?.(date);
}, [onChange]);
// 处理日期选择(只在点击日期时触发,不在切换面板时触发)
const handleSelect: CalendarProps<Dayjs>['onSelect'] = useCallback((date: Dayjs, selectInfo) => {
// selectInfo.source: 'date' 表示点击日期,'month' 表示切换月份面板
// 只在点击日期时触发 onSelect
if (selectInfo.source === 'date') {
setCurrentValue(date);
onSelect?.(date);
}
}, [onSelect]);
// 自定义单元格渲染
const fullCellRender = useCallback((date: Dayjs) => {
const isToday = date.isSame(dayjs(), 'day');
const isCurrentMonth = date.isSame(currentValue, 'month');
const info: CellRenderInfo = {
type: 'date',
isToday,
isCurrentMonth,
};
// 基础日期单元格样式
const cellStyle: React.CSSProperties = {
minHeight: CALENDAR_STYLES.cell.minHeight,
padding: CALENDAR_STYLES.cell.padding,
borderRadius: '8px',
transition: 'all 0.2s ease',
cursor: 'pointer',
...(isToday ? {
backgroundColor: CALENDAR_STYLES.today.bg,
border: CALENDAR_STYLES.today.border,
} : {}),
};
return (
<div
style={cellStyle}
className="base-calendar-cell"
>
{/* 日期数字 */}
<div
style={{
textAlign: 'center',
fontSize: '14px',
fontWeight: isToday ? 700 : 600,
color: isToday
? CALENDAR_COLORS.gold.primary
: isCurrentMonth
? '#FFFFFF' // 纯白色,更亮
: 'rgba(255, 255, 255, 0.4)',
marginBottom: '4px',
}}
>
{date.date()}
</div>
{/* 自定义内容 */}
{cellRender?.(date, info)}
</div>
);
}, [currentValue, cellRender]);
// 隐藏默认 header
const headerRender = useCallback((): React.ReactNode => null, []);
return (
<Box height={height} className={className}>
<ConfigProvider theme={CALENDAR_THEME} locale={zhCN}>
{showToolbar && (
<CalendarToolbar
value={currentValue}
onChange={handleChange}
titleFormat={titleFormat}
/>
)}
<Box
height={showToolbar ? 'calc(100% - 60px)' : '100%'}
sx={{
// 日历整体样式
'.ant-picker-calendar': {
bg: 'transparent',
},
'.ant-picker-panel': {
bg: 'transparent',
border: 'none',
},
// 星期头 - 居中显示
'.ant-picker-content thead th': {
color: `${CALENDAR_COLORS.gold.primary} !important`,
fontWeight: '600 !important',
fontSize: '14px',
padding: '8px 0',
textAlign: 'center !important',
},
// 日期单元格
'.ant-picker-cell': {
padding: '2px',
},
'.ant-picker-cell-inner': {
width: '100%',
height: '100%',
},
// 非当前月份
'.ant-picker-cell-in-view': {
opacity: 1,
},
'.ant-picker-cell:not(.ant-picker-cell-in-view)': {
opacity: 0.5,
},
// hover 效果
'.base-calendar-cell:hover': {
bg: 'rgba(212, 175, 55, 0.1)',
},
// 选中状态
'.ant-picker-cell-selected .base-calendar-cell': {
bg: 'rgba(212, 175, 55, 0.15)',
},
}}
>
<Calendar
fullscreen={true}
value={currentValue}
onChange={handleChange}
onSelect={handleSelect}
fullCellRender={fullCellRender}
headerRender={headerRender}
/>
</Box>
</ConfigProvider>
</Box>
);
};
export default BaseCalendar;

View File

@@ -0,0 +1,142 @@
/**
* CalendarEventBlock - 日历事件块组件
* 用于在日历单元格中显示事件列表,支持多种事件类型和 "更多" 折叠
*/
import React, { useMemo } from 'react';
import { HStack, Text, Badge, VStack, Box } from '@chakra-ui/react';
import { CALENDAR_COLORS } from './theme';
/**
* 事件类型定义
*/
export type EventType = 'news' | 'report' | 'plan' | 'review' | 'system' | 'priceUp' | 'priceDown';
/**
* 日历事件接口
*/
export interface CalendarEvent {
id: string | number;
type: EventType;
title: string;
date: string;
count?: number;
data?: unknown;
}
/**
* 事件块 Props
*/
interface CalendarEventBlockProps {
events: CalendarEvent[];
maxDisplay?: number;
onEventClick?: (event: CalendarEvent) => void;
onMoreClick?: (events: CalendarEvent[]) => void;
compact?: boolean;
}
/**
* 事件类型配置
*/
const EVENT_CONFIG: Record<EventType, { label: string; color: string; emoji?: string }> = {
news: { label: '新闻', color: CALENDAR_COLORS.events.news, emoji: '📰' },
report: { label: '研报', color: CALENDAR_COLORS.events.report, emoji: '📊' },
plan: { label: '计划', color: CALENDAR_COLORS.events.plan },
review: { label: '复盘', color: CALENDAR_COLORS.events.review },
system: { label: '系统', color: CALENDAR_COLORS.events.system },
priceUp: { label: '涨', color: CALENDAR_COLORS.events.priceUp, emoji: '🔥' },
priceDown: { label: '跌', color: CALENDAR_COLORS.events.priceDown },
};
/**
* 单个事件行组件
*/
const EventLine: React.FC<{
event: CalendarEvent;
compact?: boolean;
onClick?: () => void;
}> = ({ event, compact, onClick }) => {
const config = EVENT_CONFIG[event.type] || { label: event.type, color: '#888' };
return (
<Box
fontSize={compact ? '9px' : '10px'}
color={config.color}
cursor="pointer"
px={1}
py={0.5}
borderRadius="sm"
_hover={{ bg: 'rgba(255, 255, 255, 0.1)' }}
w="100%"
overflow="hidden"
textAlign="left"
onClick={(e) => {
e.stopPropagation();
onClick?.();
}}
>
{/* 格式计划年末布局XX+1 */}
<Text fontWeight="600" fontSize={compact ? '9px' : '10px'} isTruncated>
{config.emoji ? `${config.emoji} ` : ''}{config.label}{event.title || ''}
{(event.count ?? 0) > 1 && <Text as="span" color={config.color}>+{(event.count ?? 1) - 1}</Text>}
</Text>
</Box>
);
};
/**
* 日历事件块组件
*/
export const CalendarEventBlock: React.FC<CalendarEventBlockProps> = ({
events,
maxDisplay = 3,
onEventClick,
onMoreClick,
compact = false,
}) => {
// 计算显示的事件和剩余事件
const { displayEvents, remainingCount, remainingEvents } = useMemo(() => {
if (events.length <= maxDisplay) {
return { displayEvents: events, remainingCount: 0, remainingEvents: [] };
}
return {
displayEvents: events.slice(0, maxDisplay),
remainingCount: events.length - maxDisplay,
remainingEvents: events.slice(maxDisplay),
};
}, [events, maxDisplay]);
if (events.length === 0) return null;
return (
<VStack spacing={0} align="stretch" w="100%">
{displayEvents.map((event) => (
<EventLine
key={event.id}
event={event}
compact={compact}
onClick={() => onEventClick?.(event)}
/>
))}
{remainingCount > 0 && (
<Box
fontSize="9px"
color={CALENDAR_COLORS.text.secondary}
cursor="pointer"
px={1}
py={0.5}
borderRadius="sm"
_hover={{ bg: 'rgba(255, 255, 255, 0.1)' }}
onClick={(e) => {
e.stopPropagation();
onMoreClick?.(remainingEvents);
}}
>
<Text>+{remainingCount} </Text>
</Box>
)}
</VStack>
);
};
export default CalendarEventBlock;

View File

@@ -0,0 +1,19 @@
/**
* Calendar 公共组件库
* 统一的日历组件,基于 Ant Design Calendar + 黑金主题
*/
// 基础日历组件
export { BaseCalendar } from './BaseCalendar';
export type { BaseCalendarProps, CellRenderInfo } from './BaseCalendar';
// 事件块组件
export { CalendarEventBlock } from './CalendarEventBlock';
export type { CalendarEvent, EventType } from './CalendarEventBlock';
// 主题配置
export {
CALENDAR_THEME,
CALENDAR_COLORS,
CALENDAR_STYLES,
} from './theme';

View File

@@ -0,0 +1,111 @@
/**
* Calendar 黑金主题配置
* 统一的 Ant Design Calendar 主题,用于所有日历组件
*/
import type { ThemeConfig } from 'antd';
// 黑金主题色值
export const CALENDAR_COLORS = {
// 主色
gold: {
primary: '#D4AF37',
secondary: '#B8960C',
gradient: 'linear-gradient(135deg, #D4AF37 0%, #F5E6A3 100%)',
},
// 背景色
bg: {
deep: '#0A0A14',
primary: '#0F0F1A',
elevated: '#1A1A2E',
surface: '#252540',
},
// 边框色
border: {
subtle: 'rgba(212, 175, 55, 0.1)',
default: 'rgba(212, 175, 55, 0.2)',
emphasis: 'rgba(212, 175, 55, 0.4)',
},
// 文字色
text: {
primary: 'rgba(255, 255, 255, 0.95)',
secondary: 'rgba(255, 255, 255, 0.6)',
muted: 'rgba(255, 255, 255, 0.4)',
},
// 事件类型颜色
events: {
news: '#9F7AEA', // 紫色 - 新闻
report: '#805AD5', // 深紫 - 研报
plan: '#D4AF37', // 金色 - 计划
review: '#10B981', // 绿色 - 复盘
system: '#3B82F6', // 蓝色 - 系统事件
priceUp: '#FC8181', // 红色 - 上涨
priceDown: '#68D391', // 绿色 - 下跌
},
} as const;
/**
* Ant Design Calendar 黑金主题配置
*/
export const CALENDAR_THEME: ThemeConfig = {
token: {
// 基础色
colorBgContainer: 'transparent',
colorBgElevated: CALENDAR_COLORS.bg.elevated,
colorText: CALENDAR_COLORS.text.primary,
colorTextSecondary: CALENDAR_COLORS.text.secondary,
colorTextTertiary: CALENDAR_COLORS.text.muted,
colorTextHeading: CALENDAR_COLORS.gold.primary,
// 边框
colorBorder: CALENDAR_COLORS.border.default,
colorBorderSecondary: CALENDAR_COLORS.border.subtle,
// 主色
colorPrimary: CALENDAR_COLORS.gold.primary,
colorPrimaryHover: CALENDAR_COLORS.gold.secondary,
colorPrimaryActive: CALENDAR_COLORS.gold.secondary,
// 链接色
colorLink: CALENDAR_COLORS.gold.primary,
colorLinkHover: CALENDAR_COLORS.gold.secondary,
// 圆角
borderRadius: 8,
borderRadiusLG: 12,
},
components: {
Calendar: {
// 日历整体背景
fullBg: 'transparent',
fullPanelBg: 'transparent',
// 选中项背景
itemActiveBg: 'rgba(212, 175, 55, 0.15)',
},
},
};
/**
* 日历样式常量(用于内联样式或 CSS-in-JS
*/
export const CALENDAR_STYLES = {
// 今天高亮
today: {
bg: 'rgba(212, 175, 55, 0.1)',
border: `2px solid ${CALENDAR_COLORS.gold.primary}`,
},
// 日期单元格
cell: {
minHeight: '85px',
padding: '4px',
},
// 工具栏
toolbar: {
buttonBg: CALENDAR_COLORS.gold.primary,
buttonColor: CALENDAR_COLORS.bg.deep,
buttonHoverBg: CALENDAR_COLORS.gold.secondary,
},
};
export default CALENDAR_THEME;