feat(Calendar): 新增公共日历组件 BaseCalendar
This commit is contained in:
269
src/components/Calendar/BaseCalendar.tsx
Normal file
269
src/components/Calendar/BaseCalendar.tsx
Normal 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;
|
||||
142
src/components/Calendar/CalendarEventBlock.tsx
Normal file
142
src/components/Calendar/CalendarEventBlock.tsx
Normal 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;
|
||||
19
src/components/Calendar/index.ts
Normal file
19
src/components/Calendar/index.ts
Normal 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';
|
||||
111
src/components/Calendar/theme.ts
Normal file
111
src/components/Calendar/theme.ts
Normal 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;
|
||||
Reference in New Issue
Block a user