community增加事件详情

This commit is contained in:
2026-01-07 15:01:27 +08:00
parent b5b6122a17
commit 91d57b5823
4 changed files with 528 additions and 204 deletions

View File

@@ -56,6 +56,7 @@ import { getApiBase } from '@utils/apiConfig';
import ReactMarkdown from 'react-markdown';
import dayjs from 'dayjs';
import KLineChartModal from '@components/StockChart/KLineChartModal';
import { FullCalendarPro } from '@components/Calendar';
const { TabPane } = Tabs;
const { Text: AntText } = Typography;
@@ -2309,7 +2310,7 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
};
/**
* 综合日历组件(无左侧面板,点击弹窗)
* 综合日历组件 - 使用 FullCalendarPro 实现跨天事件条效果
*/
const CombinedCalendar = () => {
const [currentMonth, setCurrentMonth] = useState(new Date());
@@ -2324,7 +2325,6 @@ const CombinedCalendar = () => {
const [eventCounts, setEventCounts] = useState([]);
const [selectedEvents, setSelectedEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
@@ -2356,27 +2356,21 @@ const CombinedCalendar = () => {
}
} catch (error) {
console.error('Failed to load event counts:', error);
} finally {
setLoading(false);
}
};
loadEventCounts();
}, [currentMonth]);
// 获取当月涨停板块详情
// 获取涨停板块详情(加载所有数据,不限于当月)
useEffect(() => {
const loadMonthZtDetails = async () => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const loadZtDetails = async () => {
const details = {};
const promises = [];
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}${String(month + 1).padStart(2, '0')}${String(day).padStart(2, '0')}`;
const hasData = ztDatesData.some(d => d.date === dateStr);
if (hasData && !ztDailyDetails[dateStr]) {
// 加载所有有数据的日期
ztDatesData.forEach(d => {
const dateStr = d.date;
if (!ztDailyDetails[dateStr]) {
promises.push(
fetch(`/data/zt/daily/${dateStr}.json`)
.then(res => res.ok ? res.json() : null)
@@ -2385,10 +2379,8 @@ const CombinedCalendar = () => {
// 优先使用词云图最高频词fallback 到板块数据
let topWord = '';
if (data.word_freq_data && data.word_freq_data.length > 0) {
// word_freq_data 已按频率排序,第一个就是最高频
topWord = data.word_freq_data[0].name;
} else if (data.sector_data) {
// fallback: 使用板块数据中最高的
let maxCount = 0;
Object.entries(data.sector_data).forEach(([sector, info]) => {
if (info.count > maxCount) {
@@ -2403,7 +2395,7 @@ const CombinedCalendar = () => {
.catch(() => null)
);
}
}
});
if (promises.length > 0) {
await Promise.all(promises);
@@ -2412,114 +2404,24 @@ const CombinedCalendar = () => {
};
if (ztDatesData.length > 0) {
loadMonthZtDetails();
loadZtDetails();
}
}, [currentMonth, ztDatesData]);
}, [ztDatesData]);
// 构建日期数据映射
const ztDataMap = useMemo(() => {
const map = new Map();
ztDatesData.forEach(d => {
// 构建 FullCalendarPro 所需的数据格式
const calendarData = useMemo(() => {
return ztDatesData.map(d => {
const detail = ztDailyDetails[d.date] || {};
map.set(d.date, {
const eventDateStr = `${d.date.slice(0,4)}-${d.date.slice(4,6)}-${d.date.slice(6,8)}`;
const eventCount = eventCounts.find(e => e.date === eventDateStr)?.count || 0;
return {
date: d.date,
count: d.count,
top_sector: detail.top_sector,
});
});
return map;
}, [ztDatesData, ztDailyDetails]);
const eventCountMap = useMemo(() => {
const map = new Map();
eventCounts.forEach(d => {
map.set(d.date, d.count);
});
return map;
}, [eventCounts]);
// 生成日历天数
const days = useMemo(() => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
const result = [];
for (let i = 0; i < startingDayOfWeek; i++) {
result.push(null);
}
for (let i = 1; i <= daysInMonth; i++) {
result.push(new Date(year, month, i));
}
return result;
}, [currentMonth]);
// 预计算日历格子数据(含连续概念检测)
const calendarCellsData = useMemo(() => {
const today = new Date();
const todayStr = today.toDateString();
const selectedDateStr = selectedDate?.toDateString();
// 第一遍:构建基础数据
const baseData = days.map((date, index) => {
if (!date) {
return { key: `empty-${index}`, date: null, topSector: null };
}
const ztDateStr = formatDateStr(date);
const eventDateStr = dayjs(date).format('YYYY-MM-DD');
const ztData = ztDataMap.get(ztDateStr) || null;
const eventCount = eventCountMap.get(eventDateStr) || 0;
const dayOfWeek = date.getDay();
const prevDate = new Date(date);
prevDate.setDate(prevDate.getDate() - 1);
const previousZtData = ztDataMap.get(formatDateStr(prevDate)) || null;
return {
key: ztDateStr || `day-${index}`,
date,
ztData,
topSector: detail.top_sector || '',
eventCount,
previousZtData,
isSelected: date.toDateString() === selectedDateStr,
isToday: date.toDateString() === todayStr,
isWeekend: dayOfWeek === 0 || dayOfWeek === 6,
topSector: ztData?.top_sector || null,
dayOfWeek,
};
});
// 第二遍:检测连续概念(同一行内,排除周末和跨行)
return baseData.map((cellData, index) => {
if (!cellData.date || !cellData.topSector) {
return { ...cellData, connectLeft: false, connectRight: false };
}
// 检查左边(前一格)
let connectLeft = false;
if (index > 0 && cellData.dayOfWeek !== 0) { // 不是周日(行首)
const prevCell = baseData[index - 1];
if (prevCell?.date && prevCell.topSector === cellData.topSector && !prevCell.isWeekend) {
connectLeft = true;
}
}
// 检查右边(后一格)
let connectRight = false;
if (index < baseData.length - 1 && cellData.dayOfWeek !== 6) { // 不是周六(行尾)
const nextCell = baseData[index + 1];
if (nextCell?.date && nextCell.topSector === cellData.topSector && !nextCell.isWeekend) {
connectRight = true;
}
}
return { ...cellData, connectLeft, connectRight };
});
}, [days, ztDataMap, eventCountMap, selectedDate]);
}, [ztDatesData, ztDailyDetails, eventCounts]);
// 处理日期点击 - 打开弹窗
const handleDateClick = useCallback(async (date) => {
@@ -2567,13 +2469,9 @@ const CombinedCalendar = () => {
setDetailLoading(false);
}, [ztDailyDetails]);
// 月份导航
const handlePrevMonth = useCallback(() => {
setCurrentMonth(prev => new Date(prev.getFullYear(), prev.getMonth() - 1));
}, []);
const handleNextMonth = useCallback(() => {
setCurrentMonth(prev => new Date(prev.getFullYear(), prev.getMonth() + 1));
// 月份变化回调
const handleMonthChange = useCallback((year, month) => {
setCurrentMonth(new Date(year, month - 1, 1));
}, []);
return (
@@ -2599,94 +2497,32 @@ const CombinedCalendar = () => {
backgroundSize="200% 100%"
/>
{/* 月份导航 */}
<HStack justify="space-between" mb={4}>
<IconButton
icon={<ChevronLeft size={22} />}
variant="ghost"
size="md"
color={textColors.secondary}
_hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }}
onClick={handlePrevMonth}
aria-label="上个月"
/>
<HStack spacing={3}>
<Icon as={Calendar} boxSize={5} color={goldColors.primary} />
<Text fontSize="xl" fontWeight="bold" color={textColors.primary}>
{currentMonth.getFullYear()}{MONTH_NAMES[currentMonth.getMonth()]}
</Text>
<Text fontSize="sm" color={textColors.muted}>
综合日历
</Text>
</HStack>
<IconButton
icon={<ChevronRight size={22} />}
variant="ghost"
size="md"
color={textColors.secondary}
_hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }}
onClick={handleNextMonth}
aria-label="下个月"
/>
</HStack>
{/* FullCalendar Pro - 炫酷跨天事件条日历 */}
<FullCalendarPro
data={calendarData}
currentMonth={currentMonth}
onDateClick={handleDateClick}
onMonthChange={handleMonthChange}
height="650px"
/>
{/* 星期标题 */}
<SimpleGrid columns={7} spacing={2} mb={3}>
{WEEK_DAYS.map((day, idx) => (
<Text
key={day}
textAlign="center"
fontSize="md"
fontWeight="600"
color={idx === 0 || idx === 6 ? 'orange.300' : textColors.secondary}
>
{day}
</Text>
))}
</SimpleGrid>
{/* 日历格子 */}
{loading ? (
<Center h="300px">
<Spinner size="xl" color={goldColors.primary} thickness="3px" />
</Center>
) : (
<SimpleGrid columns={7} spacing={2}>
{calendarCellsData.map((cellData) => (
<CalendarCell
key={cellData.key}
date={cellData.date}
ztData={cellData.ztData}
eventCount={cellData.eventCount}
previousZtData={cellData.previousZtData}
isSelected={cellData.isSelected}
isToday={cellData.isToday}
isWeekend={cellData.isWeekend}
onClick={handleDateClick}
connectLeft={cellData.connectLeft}
connectRight={cellData.connectRight}
/>
))}
</SimpleGrid>
)}
{/* 图例 */}
<HStack spacing={5} mt={4} justify="center" flexWrap="wrap">
{/* 图例说明 */}
<HStack spacing={4} mt={4} justify="center" flexWrap="wrap">
<HStack spacing={2}>
<Icon as={Flame} boxSize={4} color="purple.400" />
<Text fontSize="sm" color={textColors.muted}>涨停80</Text>
<Box w="20px" h="10px" borderRadius="md" bgGradient="linear(135deg, #FFD700 0%, #FFA500 100%)" />
<Text fontSize="xs" color={textColors.muted}>连续热门概念</Text>
</HStack>
<HStack spacing={2}>
<Icon as={Flame} boxSize={4} color="red.400" />
<Text fontSize="sm" color={textColors.muted}>涨停60</Text>
<Icon as={Flame} boxSize={3} color="#EF4444" />
<Text fontSize="xs" color={textColors.muted}>涨停60</Text>
</HStack>
<HStack spacing={2}>
<Icon as={Flame} boxSize={4} color="orange.400" />
<Text fontSize="sm" color={textColors.muted}>涨停40</Text>
<Icon as={Flame} boxSize={3} color="#F59E0B" />
<Text fontSize="xs" color={textColors.muted}>涨停&lt;60</Text>
</HStack>
<HStack spacing={2}>
<Icon as={FileText} boxSize={4} color="#22c55e" />
<Text fontSize="sm" color={textColors.muted}>未来事件</Text>
<Box w="6px" h="6px" borderRadius="full" bg="#22C55E" />
<Text fontSize="xs" color={textColors.muted}>未来事件</Text>
</HStack>
</HStack>
</Box>