refactor(HeroPanel): 提取日历和统计卡片组件

- CalendarCell: 日历单元格,memo 优化渲染
- CombinedCalendar: 综合日历组件,懒加载 DetailModal
- HotKeywordsCloud: 热门关键词云,涨停分析 Tab 使用
- ZTStatsCards: 涨停统计卡片(连板分布、封板时间、公告驱动)
- InfoModal: 使用说明弹窗
- index.js: 组件统一导出

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2026-01-15 11:42:32 +08:00
parent 1aea8dcb6c
commit 635abfc1ab
6 changed files with 991 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
// HeroPanel - 日历单元格组件
import React, { memo } from "react";
import {
Box,
VStack,
HStack,
Text,
Badge,
Tooltip,
Icon,
} from "@chakra-ui/react";
import { Flame, FileText, TrendingUp, TrendingDown } from "lucide-react";
import { goldColors, textColors } from "../constants";
import { getHeatColor } from "../utils";
/**
* 趋势图标
*/
const TrendIcon = memo(({ current, previous }) => {
if (!current || !previous) return null;
const diff = current - previous;
if (diff === 0) return null;
const isUp = diff > 0;
return (
<Icon
as={isUp ? TrendingUp : TrendingDown}
boxSize={3}
color={isUp ? "#22c55e" : "#ef4444"}
/>
);
});
TrendIcon.displayName = "TrendIcon";
/**
* 日历单元格 - 显示涨停数和事件数(加大尺寸)
* 新增连续概念连接展示connectLeft/connectRight 表示与左右格子是否同一概念)
*/
const CalendarCell = memo(
({
date,
ztData,
eventCount,
previousZtData,
isSelected,
isToday,
isWeekend,
onClick,
connectLeft,
connectRight,
}) => {
if (!date) {
return <Box minH="75px" />;
}
const hasZtData = !!ztData;
const hasEventData = eventCount > 0;
const ztCount = ztData?.count || 0;
const heatColors = getHeatColor(ztCount);
const topSector = ztData?.top_sector || "";
// 是否有连接线(连续概念)
const hasConnection = connectLeft || connectRight;
// 周末无数据显示"休市"
if (isWeekend && !hasZtData && !hasEventData) {
return (
<Box
p={2}
borderRadius="10px"
bg="rgba(30, 30, 40, 0.3)"
border="1px solid rgba(255, 255, 255, 0.03)"
textAlign="center"
minH="75px"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
>
<Text
fontSize="md"
fontWeight="400"
color="rgba(255, 255, 255, 0.25)"
>
{date.getDate()}
</Text>
<Text fontSize="xs" color="rgba(255, 255, 255, 0.2)">
休市
</Text>
</Box>
);
}
// 正常日期
return (
<Tooltip
label={
<VStack spacing={1} align="start" p={1}>
<Text fontWeight="bold" fontSize="md">{`${
date.getMonth() + 1
}${date.getDate()}`}</Text>
{hasZtData && (
<Text>
涨停: {ztCount} {topSector && `| ${topSector}`}
</Text>
)}
{hasEventData && <Text>未来事件: {eventCount}</Text>}
{!hasZtData && !hasEventData && (
<Text color="gray.400">暂无数据</Text>
)}
</VStack>
}
placement="top"
hasArrow
bg="rgba(15, 15, 22, 0.95)"
border="1px solid rgba(212, 175, 55, 0.3)"
borderRadius="10px"
>
<Box
as="button"
p={2}
borderRadius="10px"
bg={
hasZtData
? heatColors.bg
: hasEventData
? "rgba(34, 197, 94, 0.2)"
: "rgba(40, 40, 50, 0.3)"
}
border={
isSelected
? `2px solid ${goldColors.primary}`
: isToday
? `2px solid ${goldColors.light}`
: hasZtData
? `1px solid ${heatColors.border}`
: hasEventData
? "1px solid rgba(34, 197, 94, 0.4)"
: "1px solid rgba(255, 255, 255, 0.08)"
}
boxShadow={
isSelected
? `0 0 15px ${goldColors.glow}`
: isToday
? `0 0 10px ${goldColors.glow}`
: "none"
}
position="relative"
cursor="pointer"
transition="all 0.2s"
_hover={{
transform: "scale(1.05)",
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
borderColor: goldColors.primary,
}}
onClick={() => onClick && onClick(date)}
w="full"
minH="75px"
>
{/* 今天标记 */}
{isToday && (
<Badge
position="absolute"
top="2px"
right="2px"
bg="rgba(239, 68, 68, 0.9)"
color="white"
fontSize="9px"
px={1}
borderRadius="sm"
>
今天
</Badge>
)}
<VStack spacing={0.5} align="center">
{/* 日期 */}
<Text
fontSize="lg"
fontWeight={isSelected || isToday ? "bold" : "600"}
color={
isSelected
? goldColors.primary
: isToday
? goldColors.light
: textColors.primary
}
>
{date.getDate()}
</Text>
{/* 涨停数 + 趋势 */}
{hasZtData && (
<HStack spacing={1} justify="center">
<Icon as={Flame} boxSize={3} color={heatColors.text} />
<Text fontSize="sm" fontWeight="bold" color={heatColors.text}>
{ztCount}
</Text>
<TrendIcon current={ztCount} previous={previousZtData?.count} />
</HStack>
)}
{/* 事件数 */}
{hasEventData && (
<HStack spacing={1} justify="center">
<Icon as={FileText} boxSize={3} color="#22c55e" />
<Text fontSize="sm" fontWeight="bold" color="#22c55e">
{eventCount}
</Text>
</HStack>
)}
{/* 主要板块 - 连续概念用连接样式 */}
{hasZtData && topSector && (
<Box
position="relative"
w="full"
display="flex"
justifyContent="center"
alignItems="center"
>
{/* 左连接线 */}
{connectLeft && (
<Box
position="absolute"
left="-12px"
top="50%"
transform="translateY(-50%)"
w="12px"
h="2px"
bgGradient="linear(to-r, rgba(212,175,55,0.6), rgba(212,175,55,0.3))"
/>
)}
<Text
fontSize="xs"
color={
hasConnection ? goldColors.primary : textColors.secondary
}
fontWeight={hasConnection ? "bold" : "normal"}
noOfLines={1}
maxW="70px"
bg={hasConnection ? "rgba(212,175,55,0.15)" : "transparent"}
px={hasConnection ? 1.5 : 0}
py={hasConnection ? 0.5 : 0}
borderRadius={hasConnection ? "full" : "none"}
border={
hasConnection ? "1px solid rgba(212,175,55,0.3)" : "none"
}
>
{topSector}
</Text>
{/* 右连接线 */}
{connectRight && (
<Box
position="absolute"
right="-12px"
top="50%"
transform="translateY(-50%)"
w="12px"
h="2px"
bgGradient="linear(to-l, rgba(212,175,55,0.6), rgba(212,175,55,0.3))"
/>
)}
</Box>
)}
</VStack>
</Box>
</Tooltip>
);
}
);
CalendarCell.displayName = "CalendarCell";
export default CalendarCell;

View File

@@ -0,0 +1,263 @@
// HeroPanel - 综合日历组件
import React, { useState, useEffect, useCallback, Suspense, lazy } from "react";
import {
Box,
HStack,
VStack,
Text,
Icon,
Center,
Spinner,
} from "@chakra-ui/react";
import { Flame } from "lucide-react";
import dayjs from "dayjs";
import { GLASS_BLUR } from "@/constants/glassConfig";
import { eventService } from "@services/eventService";
import { getApiBase } from "@utils/apiConfig";
import { textColors } from "../constants";
import { formatDateStr } from "../utils";
// 懒加载 FullCalendar
const FullCalendarPro = lazy(() =>
import("@components/Calendar").then((module) => ({
default: module.FullCalendarPro,
}))
);
/**
* 综合日历组件 - 使用 FullCalendarPro 实现跨天事件条效果
* @param {Object} props
* @param {React.ComponentType} props.DetailModal - 详情弹窗组件
*/
const CombinedCalendar = ({ DetailModal }) => {
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(null);
// 日历综合数据(涨停 + 事件 + 上证涨跌幅)- 使用新的综合 API
const [calendarData, setCalendarData] = useState([]);
const [ztDailyDetails, setZtDailyDetails] = useState({});
const [selectedZtDetail, setSelectedZtDetail] = useState(null);
const [selectedEvents, setSelectedEvents] = useState([]);
const [detailLoading, setDetailLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
// 加载日历综合数据(一次 API 调用获取所有数据)
useEffect(() => {
const loadCalendarCombinedData = async () => {
try {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1;
const response = await fetch(
`${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}`
);
if (response.ok) {
const result = await response.json();
if (result.success && result.data) {
// 转换为 FullCalendarPro 需要的格式
const formattedData = result.data.map((item) => ({
date: item.date,
count: item.zt_count || 0,
topSector: item.top_sector || "",
eventCount: item.event_count || 0,
indexChange: item.index_change,
}));
console.log(
"[HeroPanel] 加载日历综合数据成功,数据条数:",
formattedData.length
);
setCalendarData(formattedData);
}
}
} catch (error) {
console.error("Failed to load calendar combined data:", error);
}
};
loadCalendarCombinedData();
}, [currentMonth]);
// 处理日期点击 - 打开弹窗
const handleDateClick = useCallback(
async (date) => {
setSelectedDate(date);
setModalOpen(true);
setDetailLoading(true);
const ztDateStr = formatDateStr(date);
const eventDateStr = dayjs(date).format("YYYY-MM-DD");
// 加载涨停详情
const detail = ztDailyDetails[ztDateStr];
if (detail?.fullData) {
setSelectedZtDetail(detail.fullData);
} else {
try {
const response = await fetch(`/data/zt/daily/${ztDateStr}.json`);
if (response.ok) {
const data = await response.json();
setSelectedZtDetail(data);
setZtDailyDetails((prev) => ({
...prev,
[ztDateStr]: { ...prev[ztDateStr], fullData: data },
}));
} else {
setSelectedZtDetail(null);
}
} catch {
setSelectedZtDetail(null);
}
}
// 加载事件详情
try {
const response = await eventService.calendar.getEventsForDate(
eventDateStr
);
if (response.success) {
setSelectedEvents(response.data || []);
} else {
setSelectedEvents([]);
}
} catch {
setSelectedEvents([]);
}
setDetailLoading(false);
},
[ztDailyDetails]
);
// 月份变化回调
const handleMonthChange = useCallback((year, month) => {
setCurrentMonth(new Date(year, month - 1, 1));
}, []);
return (
<>
<Box
bg="rgba(15, 15, 22, 0.6)"
backdropFilter={GLASS_BLUR.md}
borderRadius="xl"
pt={5}
px={5}
pb={1}
position="relative"
overflow="hidden"
>
{/* 顶部装饰条 */}
<Box
position="absolute"
top={0}
left={0}
right={0}
h="3px"
bgGradient="linear(to-r, transparent, #D4AF37, #F4D03F, #D4AF37, transparent)"
animation="shimmer 3s linear infinite"
backgroundSize="200% 100%"
/>
{/* 图例说明 - 右上角 */}
<HStack
spacing={3}
position="absolute"
top={4}
right={4}
flexWrap="wrap"
justify="flex-end"
zIndex={1}
>
<HStack spacing={1}>
<Box
w="16px"
h="8px"
borderRadius="sm"
bgGradient="linear(135deg, #FFD700 0%, #FFA500 100%)"
/>
<Text fontSize="2xs" color={textColors.muted}>
热门概念
</Text>
</HStack>
<HStack spacing={1}>
<Icon as={Flame} boxSize={2.5} color="#EF4444" />
<Text fontSize="2xs" color={textColors.muted}>
60
</Text>
</HStack>
<HStack spacing={1}>
<Icon as={Flame} boxSize={2.5} color="#F59E0B" />
<Text fontSize="2xs" color={textColors.muted}>
&lt;60
</Text>
</HStack>
<HStack spacing={1}>
<Box
w="12px"
h="12px"
borderRadius="full"
bg="linear-gradient(135deg, #22C55E 0%, #16A34A 100%)"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text fontSize="6px" fontWeight="bold" color="white">
N
</Text>
</Box>
<Text fontSize="2xs" color={textColors.muted}>
事件
</Text>
</HStack>
<HStack spacing={0.5}>
<Text fontSize="2xs" fontWeight="600" color="#EF4444">
+
</Text>
<Text fontSize="2xs" color={textColors.muted}>
/
</Text>
<Text fontSize="2xs" fontWeight="600" color="#22C55E">
-
</Text>
<Text fontSize="2xs" color={textColors.muted}>
上证
</Text>
</HStack>
</HStack>
{/* FullCalendar Pro - 炫酷跨天事件条日历(懒加载) */}
<Suspense
fallback={
<Center h="300px">
<VStack spacing={4}>
<Spinner size="xl" color="gold" thickness="3px" />
<Text color="whiteAlpha.600" fontSize="sm">
加载日历组件...
</Text>
</VStack>
</Center>
}
>
<FullCalendarPro
data={calendarData}
currentMonth={currentMonth}
onDateClick={handleDateClick}
onMonthChange={handleMonthChange}
/>
</Suspense>
</Box>
{/* 详情弹窗 */}
{DetailModal && (
<DetailModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
selectedDate={selectedDate}
ztDetail={selectedZtDetail}
events={selectedEvents}
loading={detailLoading}
/>
)}
</>
);
};
export default CombinedCalendar;

View File

@@ -0,0 +1,122 @@
// 热门关键词云组件
// 用于涨停分析 Tab 显示今日热词
import React from "react";
import { FireOutlined } from "@ant-design/icons";
/**
* 获取关键词样式(根据排名)
*/
const getKeywordStyle = (index) => {
if (index < 3) {
return {
fontSize: "15px",
fontWeight: "bold",
background:
"linear-gradient(135deg, rgba(255,215,0,0.3) 0%, rgba(255,165,0,0.2) 100%)",
border: "1px solid rgba(255,215,0,0.5)",
color: "#FFD700",
padding: "6px 12px",
};
}
if (index < 6) {
return {
fontSize: "14px",
fontWeight: "600",
background: "rgba(255,215,0,0.15)",
border: "1px solid rgba(255,215,0,0.3)",
color: "#D4A84B",
padding: "4px 10px",
};
}
return {
fontSize: "13px",
fontWeight: "normal",
background: "rgba(255,255,255,0.08)",
border: "1px solid rgba(255,255,255,0.15)",
color: "#888",
padding: "2px 8px",
};
};
/**
* 热门关键词云组件
* @param {Object} props
* @param {Array} props.keywords - 关键词数组 [{ name: string }]
*/
const HotKeywordsCloud = ({ keywords }) => {
if (!keywords || keywords.length === 0) {
return null;
}
return (
<div
style={{
padding: "16px",
background:
"linear-gradient(135deg, rgba(255, 215, 0, 0.08) 0%, rgba(255, 140, 0, 0.05) 100%)",
borderRadius: "12px",
border: "1px solid rgba(255, 215, 0, 0.2)",
position: "relative",
overflow: "hidden",
}}
>
{/* 装饰线 */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "2px",
background:
"linear-gradient(to right, transparent, #FFD700, #FF8C00, #FFD700, transparent)",
}}
/>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginBottom: "12px",
}}
>
<div
style={{
padding: "6px",
background: "rgba(255,215,0,0.2)",
borderRadius: "6px",
}}
>
<FireOutlined style={{ color: "#FFD700", fontSize: "16px" }} />
</div>
<span style={{ fontSize: "16px", fontWeight: "bold", color: "gold" }}>
今日热词
</span>
<span style={{ fontSize: "12px", color: "rgba(255,255,255,0.5)" }}>
词频越高排名越前
</span>
</div>
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
{keywords.map((kw, idx) => {
const style = getKeywordStyle(idx);
return (
<span
key={kw.name}
style={{
...style,
borderRadius: "9999px",
transition: "all 0.2s",
cursor: "default",
}}
>
{kw.name}
</span>
);
})}
</div>
</div>
);
};
export default HotKeywordsCloud;

View File

@@ -0,0 +1,152 @@
// HeroPanel - 使用说明弹窗组件
import React, { useState } from "react";
import { HStack, Icon, Text } from "@chakra-ui/react";
import { Modal as AntModal, ConfigProvider, theme } from "antd";
import { Info } from "lucide-react";
import { GLASS_BLUR } from "@/constants/glassConfig";
/**
* 使用说明弹窗组件
*/
const InfoModal = () => {
const [isOpen, setIsOpen] = useState(false);
const onOpen = () => setIsOpen(true);
const onClose = () => setIsOpen(false);
return (
<>
<HStack
spacing={1.5}
px={3}
py={1.5}
bg="rgba(255,215,0,0.08)"
border="1px solid rgba(255,215,0,0.2)"
borderRadius="full"
cursor="pointer"
transition="all 0.2s"
_hover={{
bg: "rgba(255,215,0,0.15)",
borderColor: "rgba(255,215,0,0.4)",
transform: "scale(1.02)",
}}
onClick={onOpen}
>
<Icon as={Info} color="gold" boxSize={4} />
<Text fontSize="sm" color="gold" fontWeight="medium">
使用说明
</Text>
</HStack>
<ConfigProvider
theme={{
algorithm: theme.darkAlgorithm,
token: {
colorBgElevated: 'rgba(15,15,30,0.98)',
},
}}
>
<AntModal
open={isOpen}
onCancel={onClose}
footer={null}
width={550}
centered
title={
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div
style={{
padding: '8px',
background: 'rgba(255,215,0,0.15)',
borderRadius: '8px',
border: '1px solid rgba(255,215,0,0.3)',
display: 'flex',
}}
>
<Info size={20} color="gold" />
</div>
<span
style={{
fontSize: '20px',
fontWeight: 'bold',
background: 'linear-gradient(to right, #FFD700, #FFA500)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
事件中心使用指南
</span>
</div>
}
styles={{
header: {
background: 'rgba(25,25,50,0.98)',
borderBottom: '1px solid rgba(255,215,0,0.2)',
paddingBottom: '16px',
},
body: {
background: 'linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)',
padding: '24px',
},
content: {
background: 'linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)',
border: '1px solid rgba(255,215,0,0.3)',
borderRadius: '16px',
boxShadow: '0 25px 80px rgba(0,0,0,0.8)',
},
mask: {
background: 'rgba(0,0,0,0.7)',
backdropFilter: GLASS_BLUR.sm,
},
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: 'rgba(255,255,255,0.9)', marginBottom: '8px' }}>
📅 综合日历
</div>
<div style={{ fontSize: '16px', color: 'rgba(255,255,255,0.8)', lineHeight: '1.8' }}>
日历同时展示
<span style={{ color: '#d8b4fe', fontWeight: 'bold' }}>历史涨停数据</span>
<span style={{ color: '#86efac', fontWeight: 'bold' }}>未来事件</span>
点击日期查看详细信息
</div>
</div>
<div>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: 'rgba(255,255,255,0.9)', marginBottom: '8px' }}>
🔥 涨停板块
</div>
<div style={{ fontSize: '16px', color: 'rgba(255,255,255,0.8)', lineHeight: '1.8' }}>
点击历史日期查看当日涨停板块排行涨停数量涨停股票代码帮助理解市场主线
</div>
</div>
<div>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: 'rgba(255,255,255,0.9)', marginBottom: '8px' }}>
📊 未来事件
</div>
<div style={{ fontSize: '16px', color: 'rgba(255,255,255,0.8)', lineHeight: '1.8' }}>
点击未来日期查看事件详情包括
<span style={{ color: '#67e8f9', fontWeight: 'bold' }}>背景分析</span>
<span style={{ color: '#fdba74', fontWeight: 'bold' }}>未来推演</span>
<span style={{ color: '#86efac', fontWeight: 'bold' }}>相关股票</span>
</div>
</div>
<div style={{ paddingTop: '12px', borderTop: '1px solid rgba(255,215,0,0.2)' }}>
<div style={{ fontSize: '16px', color: '#fde047', textAlign: 'center', fontWeight: '500' }}>
💡 颜色越深表示涨停数越多 · 绿色标记表示有未来事件
</div>
</div>
</div>
</AntModal>
</ConfigProvider>
</>
);
};
export default InfoModal;

View File

@@ -0,0 +1,171 @@
// 涨停统计卡片组件
// 显示连板分布、封板时间、公告驱动统计
import React from "react";
/**
* 获取连板颜色
*/
const getContinuousColor = (key) => {
if (key === "4连板+") return "#ff4d4f";
if (key === "3连板") return "#fa541c";
if (key === "2连板") return "#fa8c16";
return "#52c41a";
};
/**
* 获取时间颜色
*/
const getTimeColor = (key) => {
if (key === "秒板") return "#ff4d4f";
if (key === "早盘") return "#fa8c16";
if (key === "盘中") return "#52c41a";
return "#888";
};
/**
* 统计卡片基础样式
*/
const cardStyle = {
flex: 1,
minWidth: "200px",
padding: "12px",
background: "rgba(255,255,255,0.03)",
borderRadius: "12px",
border: "1px solid rgba(255,255,255,0.08)",
};
/**
* 涨停统计卡片组件
* @param {Object} props
* @param {Object} props.stats - 统计数据
* @param {Object} props.stats.continuousStats - 连板分布
* @param {Object} props.stats.timeStats - 时间分布
* @param {number} props.stats.announcementCount - 公告驱动数
* @param {number} props.stats.announcementRatio - 公告驱动占比
*/
const ZTStatsCards = ({ stats }) => {
if (!stats) {
return null;
}
return (
<div style={{ display: "flex", gap: "16px", flexWrap: "wrap" }}>
{/* 连板分布 */}
<div style={cardStyle}>
<span
style={{
fontSize: "12px",
color: "rgba(255,255,255,0.5)",
display: "block",
marginBottom: "8px",
}}
>
连板分布
</span>
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
{Object.entries(stats.continuousStats).map(([key, value]) => (
<div
key={key}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<span
style={{
fontSize: "18px",
fontWeight: "bold",
color: getContinuousColor(key),
}}
>
{value}
</span>
<span
style={{ fontSize: "10px", color: "rgba(255,255,255,0.5)" }}
>
{key}
</span>
</div>
))}
</div>
</div>
{/* 涨停时间分布 */}
<div style={cardStyle}>
<span
style={{
fontSize: "12px",
color: "rgba(255,255,255,0.5)",
display: "block",
marginBottom: "8px",
}}
>
封板时间
</span>
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
{Object.entries(stats.timeStats).map(([key, value]) => (
<div
key={key}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<span
style={{
fontSize: "18px",
fontWeight: "bold",
color: getTimeColor(key),
}}
>
{value}
</span>
<span
style={{ fontSize: "10px", color: "rgba(255,255,255,0.5)" }}
>
{key}
</span>
</div>
))}
</div>
</div>
{/* 公告驱动 */}
<div
style={{
padding: "12px",
background: "rgba(255,255,255,0.03)",
borderRadius: "12px",
border: "1px solid rgba(255,255,255,0.08)",
minWidth: "120px",
}}
>
<span
style={{
fontSize: "12px",
color: "rgba(255,255,255,0.5)",
display: "block",
marginBottom: "8px",
}}
>
公告驱动
</span>
<div style={{ display: "flex", gap: "8px", alignItems: "baseline" }}>
<span
style={{ fontSize: "20px", fontWeight: "bold", color: "#A855F7" }}
>
{stats.announcementCount}
</span>
<span style={{ fontSize: "12px", color: "rgba(255,255,255,0.5)" }}>
({stats.announcementRatio}%)
</span>
</div>
</div>
</div>
);
};
export default ZTStatsCards;

View File

@@ -0,0 +1,7 @@
// HeroPanel 子组件导出
export * from "./DetailModal";
export { default as CalendarCell } from "./CalendarCell";
export { default as InfoModal } from "./InfoModal";
export { default as CombinedCalendar } from "./CombinedCalendar";
export { default as HotKeywordsCloud } from "./HotKeywordsCloud";
export { default as ZTStatsCards } from "./ZTStatsCards";