From 39fb70a1ebb5a417498100f0dbef3374563c82f8 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 23 Dec 2025 17:44:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(Calendar):=20=E6=96=B0=E5=A2=9E=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E6=97=A5=E5=8E=86=E7=BB=84=E4=BB=B6=20BaseCalendar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Calendar/BaseCalendar.tsx | 269 ++++++++++++++++++ .../Calendar/CalendarEventBlock.tsx | 142 +++++++++ src/components/Calendar/index.ts | 19 ++ src/components/Calendar/theme.ts | 111 ++++++++ 4 files changed, 541 insertions(+) create mode 100644 src/components/Calendar/BaseCalendar.tsx create mode 100644 src/components/Calendar/CalendarEventBlock.tsx create mode 100644 src/components/Calendar/index.ts create mode 100644 src/components/Calendar/theme.ts diff --git a/src/components/Calendar/BaseCalendar.tsx b/src/components/Calendar/BaseCalendar.tsx new file mode 100644 index 00000000..02b349df --- /dev/null +++ b/src/components/Calendar/BaseCalendar.tsx @@ -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 ( + + + } + onClick={handlePrev} + style={{ + backgroundColor: CALENDAR_STYLES.toolbar.buttonBg, + borderColor: CALENDAR_STYLES.toolbar.buttonBg, + color: CALENDAR_STYLES.toolbar.buttonColor, + }} + /> + } + onClick={handleNext} + style={{ + backgroundColor: CALENDAR_STYLES.toolbar.buttonBg, + borderColor: CALENDAR_STYLES.toolbar.buttonBg, + color: CALENDAR_STYLES.toolbar.buttonColor, + }} + /> + + 今天 + + + + {value.format(titleFormat)} + + + ); +}; + +/** + * BaseCalendar 组件 + */ +export const BaseCalendar: React.FC = ({ + value, + onChange, + onSelect, + cellRender, + height = '100%', + showToolbar = true, + titleFormat = 'YYYY年M月', + className, +}) => { + const [currentValue, setCurrentValue] = React.useState(value || dayjs()); + + // 同步外部 value + React.useEffect(() => { + if (value) { + setCurrentValue(value); + } + }, [value]); + + // 处理日期变化 + const handleChange = useCallback((date: Dayjs) => { + setCurrentValue(date); + onChange?.(date); + }, [onChange]); + + // 处理日期选择(只在点击日期时触发,不在切换面板时触发) + const handleSelect: CalendarProps['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 ( + + {/* 日期数字 */} + + {date.date()} + + {/* 自定义内容 */} + {cellRender?.(date, info)} + + ); + }, [currentValue, cellRender]); + + // 隐藏默认 header + const headerRender = useCallback((): React.ReactNode => null, []); + + return ( + + + {showToolbar && ( + + )} + + + + + + ); +}; + +export default BaseCalendar; diff --git a/src/components/Calendar/CalendarEventBlock.tsx b/src/components/Calendar/CalendarEventBlock.tsx new file mode 100644 index 00000000..4f0ba6be --- /dev/null +++ b/src/components/Calendar/CalendarEventBlock.tsx @@ -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 = { + 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 ( + { + e.stopPropagation(); + onClick?.(); + }} + > + {/* 格式:计划:年末布局XX+1 */} + + {config.emoji ? `${config.emoji} ` : ''}{config.label}:{event.title || ''} + {(event.count ?? 0) > 1 && +{(event.count ?? 1) - 1}} + + + ); +}; + +/** + * 日历事件块组件 + */ +export const CalendarEventBlock: React.FC = ({ + 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 ( + + {displayEvents.map((event) => ( + onEventClick?.(event)} + /> + ))} + {remainingCount > 0 && ( + { + e.stopPropagation(); + onMoreClick?.(remainingEvents); + }} + > + +{remainingCount} 更多 + + )} + + ); +}; + +export default CalendarEventBlock; diff --git a/src/components/Calendar/index.ts b/src/components/Calendar/index.ts new file mode 100644 index 00000000..c6666b58 --- /dev/null +++ b/src/components/Calendar/index.ts @@ -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'; diff --git a/src/components/Calendar/theme.ts b/src/components/Calendar/theme.ts new file mode 100644 index 00000000..0201c04a --- /dev/null +++ b/src/components/Calendar/theme.ts @@ -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;