refactor(HeroPanel): 提取 hooks/constants/utils/styles 基础模块

- hooks/useDetailModalState: 整合弹窗 17 个状态为单一 Hook
- constants: 主题色配置、热度级别常量、日期常量
- utils: 交易时间判断、热度颜色、日期/股票代码格式化
- styles/animations.css: 深色主题覆盖、动画、滚动条样式

🤖 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:24 +08:00
parent 2948f14904
commit 1aea8dcb6c
5 changed files with 510 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
// HeroPanel 常量配置
// 主题色配置
export const goldColors = {
primary: "#D4AF37",
light: "#F4D03F",
dark: "#B8860B",
glow: "rgba(212, 175, 55, 0.4)",
};
export const textColors = {
primary: "#ffffff",
secondary: "rgba(255, 255, 255, 0.85)",
muted: "rgba(255, 255, 255, 0.5)",
};
// 热度级别配置
export const HEAT_LEVELS = [
{
key: "high",
threshold: 80,
colors: {
bg: "rgba(147, 51, 234, 0.55)",
text: "#d8b4fe",
border: "rgba(147, 51, 234, 0.65)",
},
},
{
key: "medium",
threshold: 60,
colors: {
bg: "rgba(239, 68, 68, 0.50)",
text: "#fca5a5",
border: "rgba(239, 68, 68, 0.60)",
},
},
{
key: "low",
threshold: 40,
colors: {
bg: "rgba(251, 146, 60, 0.45)",
text: "#fed7aa",
border: "rgba(251, 146, 60, 0.55)",
},
},
{
key: "cold",
threshold: 0,
colors: {
bg: "rgba(59, 130, 246, 0.35)",
text: "#93c5fd",
border: "rgba(59, 130, 246, 0.45)",
},
},
];
export const DEFAULT_HEAT_COLORS = {
bg: "rgba(60, 60, 70, 0.12)",
text: textColors.muted,
border: "transparent",
};
// 日期常量
export const WEEK_DAYS = ["日", "一", "二", "三", "四", "五", "六"];
export const MONTH_NAMES = [
"1月", "2月", "3月", "4月", "5月", "6月",
"7月", "8月", "9月", "10月", "11月", "12月",
];

View File

@@ -0,0 +1,2 @@
// HeroPanel Hooks 导出
export { useDetailModalState } from "./useDetailModalState";

View File

@@ -0,0 +1,197 @@
// HeroPanel - DetailModal 状态管理 Hook
// 整合 DetailModal 组件的所有状态,使主组件更简洁
import { useState, useCallback } from "react";
/**
* DetailModal 状态管理 Hook
* 将 17 个 useState 整合为一个自定义 hook
*/
export const useDetailModalState = () => {
// ========== UI 状态 ==========
// 视图模式:板块视图 | 个股视图
const [ztViewMode, setZtViewMode] = useState("sector"); // 'sector' | 'stock'
// 板块筛选(个股视图时使用)
const [selectedSectorFilter, setSelectedSectorFilter] = useState(null);
// 展开的涨停原因
const [expandedReasons, setExpandedReasons] = useState({});
// ========== 弹窗/抽屉状态 ==========
// 内容详情抽屉
const [detailDrawerVisible, setDetailDrawerVisible] = useState(false);
const [selectedContent, setSelectedContent] = useState(null);
// 板块股票弹窗
const [sectorStocksModalVisible, setSectorStocksModalVisible] = useState(false);
const [selectedSectorInfo, setSelectedSectorInfo] = useState(null);
// 事件关联股票抽屉
const [stocksDrawerVisible, setStocksDrawerVisible] = useState(false);
const [selectedEventStocks, setSelectedEventStocks] = useState([]);
const [selectedEventTime, setSelectedEventTime] = useState(null);
const [selectedEventTitle, setSelectedEventTitle] = useState("");
// K线弹窗
const [klineModalVisible, setKlineModalVisible] = useState(false);
const [selectedKlineStock, setSelectedKlineStock] = useState(null);
// 关联事件弹窗
const [relatedEventsModalVisible, setRelatedEventsModalVisible] = useState(false);
const [selectedRelatedEvents, setSelectedRelatedEvents] = useState({
sectorName: "",
events: [],
});
// ========== 数据加载状态 ==========
const [stockQuotes, setStockQuotes] = useState({});
const [stockQuotesLoading, setStockQuotesLoading] = useState(false);
// ========== 操作方法 ==========
// 打开内容详情
const openContentDetail = useCallback((content) => {
setSelectedContent(content);
setDetailDrawerVisible(true);
}, []);
// 关闭内容详情
const closeContentDetail = useCallback(() => {
setDetailDrawerVisible(false);
setSelectedContent(null);
}, []);
// 打开板块股票弹窗
const openSectorStocks = useCallback((sectorInfo) => {
setSelectedSectorInfo(sectorInfo);
setSectorStocksModalVisible(true);
}, []);
// 关闭板块股票弹窗
const closeSectorStocks = useCallback(() => {
setSectorStocksModalVisible(false);
setSelectedSectorInfo(null);
}, []);
// 打开事件关联股票
const openEventStocks = useCallback((stocks, time, title) => {
setSelectedEventStocks(stocks);
setSelectedEventTime(time);
setSelectedEventTitle(title);
setStocksDrawerVisible(true);
}, []);
// 关闭事件关联股票
const closeEventStocks = useCallback(() => {
setStocksDrawerVisible(false);
setSelectedEventStocks([]);
setSelectedEventTime(null);
setSelectedEventTitle("");
}, []);
// 打开 K 线弹窗
const openKlineModal = useCallback((stock) => {
setSelectedKlineStock(stock);
setKlineModalVisible(true);
}, []);
// 关闭 K 线弹窗
const closeKlineModal = useCallback(() => {
setKlineModalVisible(false);
setSelectedKlineStock(null);
}, []);
// 打开关联事件弹窗
const openRelatedEvents = useCallback((sectorName, events) => {
setSelectedRelatedEvents({ sectorName, events });
setRelatedEventsModalVisible(true);
}, []);
// 关闭关联事件弹窗
const closeRelatedEvents = useCallback(() => {
setRelatedEventsModalVisible(false);
setSelectedRelatedEvents({ sectorName: "", events: [] });
}, []);
// 切换展开原因
const toggleExpandedReason = useCallback((stockCode) => {
setExpandedReasons((prev) => ({
...prev,
[stockCode]: !prev[stockCode],
}));
}, []);
// 重置所有状态(用于关闭弹窗时)
const resetAllState = useCallback(() => {
setZtViewMode("sector");
setSelectedSectorFilter(null);
setExpandedReasons({});
setDetailDrawerVisible(false);
setSelectedContent(null);
setSectorStocksModalVisible(false);
setSelectedSectorInfo(null);
setStocksDrawerVisible(false);
setSelectedEventStocks([]);
setSelectedEventTime(null);
setSelectedEventTitle("");
setKlineModalVisible(false);
setSelectedKlineStock(null);
setRelatedEventsModalVisible(false);
setSelectedRelatedEvents({ sectorName: "", events: [] });
setStockQuotes({});
setStockQuotesLoading(false);
}, []);
return {
// UI 状态 + setters
ztViewMode,
setZtViewMode,
selectedSectorFilter,
setSelectedSectorFilter,
expandedReasons,
setExpandedReasons,
// 弹窗状态 + setters
detailDrawerVisible,
setDetailDrawerVisible,
selectedContent,
setSelectedContent,
sectorStocksModalVisible,
setSectorStocksModalVisible,
selectedSectorInfo,
setSelectedSectorInfo,
stocksDrawerVisible,
setStocksDrawerVisible,
selectedEventStocks,
setSelectedEventStocks,
selectedEventTime,
setSelectedEventTime,
selectedEventTitle,
setSelectedEventTitle,
klineModalVisible,
setKlineModalVisible,
selectedKlineStock,
setSelectedKlineStock,
relatedEventsModalVisible,
setRelatedEventsModalVisible,
selectedRelatedEvents,
setSelectedRelatedEvents,
// 数据状态 + setters
stockQuotes,
setStockQuotes,
stockQuotesLoading,
setStockQuotesLoading,
// 操作方法(高级封装)
openContentDetail,
closeContentDetail,
openSectorStocks,
closeSectorStocks,
openEventStocks,
closeEventStocks,
openKlineModal,
closeKlineModal,
openRelatedEvents,
closeRelatedEvents,
toggleExpandedReason,
resetAllState,
};
};
export default useDetailModalState;

View File

@@ -0,0 +1,159 @@
/* HeroPanel 动画和深色主题样式 */
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.1); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Ant Design 深色主题覆盖 - 弹窗专用 */
.hero-panel-modal .ant-tabs {
color: rgba(255, 255, 255, 0.85);
}
.hero-panel-modal .ant-tabs-nav::before {
border-color: rgba(255, 215, 0, 0.2) !important;
}
.hero-panel-modal .ant-tabs-tab {
color: rgba(255, 255, 255, 0.65) !important;
font-size: 15px !important;
}
.hero-panel-modal .ant-tabs-tab:hover {
color: #FFD700 !important;
}
.hero-panel-modal .ant-tabs-tab-active .ant-tabs-tab-btn {
color: #FFD700 !important;
}
.hero-panel-modal .ant-tabs-ink-bar {
background: linear-gradient(90deg, #FFD700, #FFA500) !important;
}
/* 表格深色主题 */
.hero-panel-modal .ant-table {
background: transparent !important;
color: rgba(255, 255, 255, 0.85) !important;
}
.hero-panel-modal .ant-table-thead > tr > th {
background: rgba(255, 215, 0, 0.1) !important;
color: #FFD700 !important;
border-bottom: 1px solid rgba(255, 215, 0, 0.2) !important;
font-weight: 600 !important;
font-size: 14px !important;
}
.hero-panel-modal .ant-table-tbody > tr > td {
background: transparent !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
color: rgba(255, 255, 255, 0.85) !important;
font-size: 14px !important;
}
.hero-panel-modal .ant-table-tbody > tr:hover > td {
background: rgba(255, 215, 0, 0.08) !important;
}
.hero-panel-modal .ant-table-tbody > tr.ant-table-row:hover > td {
background: rgba(255, 215, 0, 0.1) !important;
}
.hero-panel-modal .ant-table-cell-row-hover {
background: rgba(255, 215, 0, 0.08) !important;
}
.hero-panel-modal .ant-table-placeholder {
background: transparent !important;
}
.hero-panel-modal .ant-empty-description {
color: rgba(255, 255, 255, 0.45) !important;
}
/* 滚动条样式 */
.hero-panel-modal .ant-table-body::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.hero-panel-modal .ant-table-body::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
.hero-panel-modal .ant-table-body::-webkit-scrollbar-thumb {
background: rgba(255, 215, 0, 0.3);
border-radius: 3px;
}
.hero-panel-modal .ant-table-body::-webkit-scrollbar-thumb:hover {
background: rgba(255, 215, 0, 0.5);
}
/* 板块股票表格滚动 - 针对 Ant Design 5.x */
.sector-stocks-table-wrapper {
max-height: 450px;
overflow: hidden;
}
.sector-stocks-table-wrapper .ant-table-wrapper,
.sector-stocks-table-wrapper .ant-table,
.sector-stocks-table-wrapper .ant-table-container {
max-height: 100%;
}
.sector-stocks-table-wrapper .ant-table-body {
max-height: 380px !important;
overflow-y: auto !important;
scrollbar-width: thin;
scrollbar-color: rgba(255, 215, 0, 0.4) rgba(255, 255, 255, 0.05);
}
/* 相关股票表格滚动 */
.related-stocks-table-wrapper .ant-table-body {
scrollbar-width: thin;
scrollbar-color: rgba(255, 215, 0, 0.4) rgba(255, 255, 255, 0.05);
}
/* Tag 样式优化 */
.hero-panel-modal .ant-tag {
border-radius: 4px !important;
}
/* Button link 样式 */
.hero-panel-modal .ant-btn-link {
color: #FFD700 !important;
}
.hero-panel-modal .ant-btn-link:hover {
color: #FFA500 !important;
}
.hero-panel-modal .ant-btn-link:disabled {
color: rgba(255, 255, 255, 0.25) !important;
}
/* Typography 样式 */
.hero-panel-modal .ant-typography {
color: rgba(255, 255, 255, 0.85) !important;
}
.hero-panel-modal .ant-typography-secondary {
color: rgba(255, 255, 255, 0.45) !important;
}
/* Spin 加载样式 */
.hero-panel-modal .ant-spin-text {
color: #FFD700 !important;
}
.hero-panel-modal .ant-spin-dot-item {
background-color: #FFD700 !important;
}

View File

@@ -0,0 +1,84 @@
// HeroPanel 工具函数
import { HEAT_LEVELS, DEFAULT_HEAT_COLORS } from '../constants';
/**
* 判断当前是否在交易时间内 (9:30-15:00)
*/
export const isInTradingTime = () => {
const now = new Date();
const timeInMinutes = now.getHours() * 60 + now.getMinutes();
return timeInMinutes >= 570 && timeInMinutes <= 900;
};
/**
* 根据涨停数获取热度颜色
*/
export const getHeatColor = (count) => {
if (!count) return DEFAULT_HEAT_COLORS;
const level = HEAT_LEVELS.find((l) => count >= l.threshold);
return level?.colors || DEFAULT_HEAT_COLORS;
};
/**
* 日期格式化为 YYYYMMDD
*/
export const formatDateStr = (date) => {
if (!date) return "";
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}${month}${day}`;
};
/**
* 获取六位股票代码(去掉后缀)
*/
export const getSixDigitCode = (code) => {
if (!code) return code;
return code.split(".")[0];
};
/**
* 添加交易所后缀
*/
export const addExchangeSuffix = (code) => {
const sixDigitCode = getSixDigitCode(code);
if (code.includes(".")) return code;
if (sixDigitCode.startsWith("6")) {
return `${sixDigitCode}.SH`;
} else if (sixDigitCode.startsWith("0") || sixDigitCode.startsWith("3")) {
return `${sixDigitCode}.SZ`;
}
return sixDigitCode;
};
/**
* 解析连板天数
*/
export const parseContinuousDays = (text) => {
if (!text || text === "首板") return 1;
const match = text.match(/(\d+)/);
return match ? parseInt(match[1]) : 1;
};
/**
* 获取涨停时间样式
*/
export const getTimeStyle = (time) => {
if (time <= "09:30:00") return { bg: "#ff4d4f", text: "#fff", label: "秒板" };
if (time <= "09:35:00") return { bg: "#fa541c", text: "#fff", label: "早板" };
if (time <= "10:00:00") return { bg: "#fa8c16", text: "#fff", label: "盘初" };
if (time <= "11:00:00") return { bg: "#52c41a", text: "#fff", label: "盘中" };
return { bg: "rgba(255,255,255,0.1)", text: "#888", label: "尾盘" };
};
/**
* 获取连板天数样式
*/
export const getDaysStyle = (days) => {
if (days >= 5) return { bg: "linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)", text: "#fff" };
if (days >= 3) return { bg: "linear-gradient(135deg, #fa541c 0%, #ff7a45 100%)", text: "#fff" };
if (days >= 2) return { bg: "linear-gradient(135deg, #fa8c16 0%, #ffc53d 100%)", text: "#fff" };
return { bg: "rgba(255,255,255,0.1)", text: "#888" };
};