refactor(HeroPanel): 提取表格列定义到 columns 目录

- 新增 renderers.js 通用渲染函数
- 新增 stockColumns.js 事件关联股票列
- 新增 sectorColumns.js 涨停板块列
- 新增 ztStockColumns.js 涨停个股列
- 新增 eventColumns.js 未来事件列
- 使用工厂函数模式,支持 useMemo 缓存

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2026-01-13 19:00:42 +08:00
parent 9c1ec403f0
commit e070df5d62
6 changed files with 1207 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
// 事件表格列定义
// 用于未来事件 Tab
import React from "react";
import { Button, Space, Tooltip, Typography } from "antd";
import {
ClockCircleOutlined,
LinkOutlined,
RobotOutlined,
StockOutlined,
} from "@ant-design/icons";
import { StarFilled } from "@ant-design/icons";
import dayjs from "dayjs";
const { Text: AntText } = Typography;
// 渲染星级评分
const renderStars = (star) => {
const level = parseInt(star, 10) || 0;
return (
<span>
{[1, 2, 3, 4, 5].map((i) => (
<StarFilled
key={i}
style={{
color: i <= level ? "#faad14" : "#d9d9d9",
fontSize: "14px",
}}
/>
))}
</span>
);
};
/**
* 创建事件表格列
* @param {Object} options - 配置选项
* @param {Function} options.showContentDetail - 显示内容详情
* @param {Function} options.showRelatedStocks - 显示相关股票
*/
export const createEventColumns = ({ showContentDetail, showRelatedStocks }) => [
{
title: "时间",
dataIndex: "calendar_time",
key: "time",
width: 80,
render: (time) => (
<Space>
<ClockCircleOutlined />
<AntText>{dayjs(time).format("HH:mm")}</AntText>
</Space>
),
},
{
title: "重要度",
dataIndex: "star",
key: "star",
width: 120,
render: renderStars,
},
{
title: "标题",
dataIndex: "title",
key: "title",
ellipsis: true,
render: (text) => (
<Tooltip title={text}>
<AntText strong style={{ fontSize: "14px" }}>
{text}
</AntText>
</Tooltip>
),
},
{
title: "背景",
dataIndex: "former",
key: "former",
width: 80,
render: (text) => (
<Button
type="link"
size="small"
icon={<LinkOutlined />}
onClick={() => showContentDetail(text, "事件背景")}
disabled={!text}
>
查看
</Button>
),
},
{
title: "未来推演",
dataIndex: "forecast",
key: "forecast",
width: 90,
render: (text) => (
<Button
type="link"
size="small"
icon={<RobotOutlined />}
onClick={() => showContentDetail(text, "未来推演")}
disabled={!text}
>
{text ? "查看" : "无"}
</Button>
),
},
{
title: "相关股票",
dataIndex: "related_stocks",
key: "stocks",
width: 120,
render: (stocks, record) => {
const hasStocks = stocks && stocks.length > 0;
if (!hasStocks) {
return <AntText type="secondary"></AntText>;
}
return (
<Button
type="link"
size="small"
icon={<StockOutlined />}
onClick={() =>
showRelatedStocks(stocks, record.calendar_time, record.title)
}
>
{stocks.length}
</Button>
);
},
},
];

View File

@@ -0,0 +1,6 @@
// HeroPanel 表格列相关导出
export * from './renderers';
export { createStockColumns } from './stockColumns';
export { createSectorColumns } from './sectorColumns';
export { createZtStockColumns } from './ztStockColumns';
export { createEventColumns } from './eventColumns';

View File

@@ -0,0 +1,184 @@
// HeroPanel 表格列渲染器
import { Tag, Space, Button, Typography, Tooltip } from "antd";
import {
ClockCircleOutlined,
LinkOutlined,
RobotOutlined,
StockOutlined,
StarFilled,
StarOutlined,
LineChartOutlined,
} from "@ant-design/icons";
import dayjs from "dayjs";
const { Text: AntText } = Typography;
/**
* 渲染星级评分
*/
export const renderStars = (star) => {
const level = parseInt(star, 10) || 0;
return (
<span>
{[1, 2, 3, 4, 5].map((i) => (
<span
key={i}
style={{
color: i <= level ? "#faad14" : "#d9d9d9",
fontSize: "14px",
}}
>
</span>
))}
</span>
);
};
/**
* 渲染涨跌幅
*/
export const renderChangePercent = (val) => {
if (val === null || val === undefined) return "-";
const num = parseFloat(val);
const color = num > 0 ? "#ff4d4f" : num < 0 ? "#52c41a" : "#888";
const prefix = num > 0 ? "+" : "";
return (
<span style={{ color, fontWeight: 600 }}>
{prefix}{num.toFixed(2)}%
</span>
);
};
/**
* 渲染时间
*/
export const renderTime = (time) => (
<Space>
<ClockCircleOutlined />
<AntText>{dayjs(time).format("HH:mm")}</AntText>
</Space>
);
/**
* 渲染标题带tooltip
*/
export const renderTitle = (text) => (
<Tooltip title={text}>
<AntText strong style={{ fontSize: "14px" }}>
{text}
</AntText>
</Tooltip>
);
/**
* 渲染排名样式
*/
export const getRankStyle = (index) => {
if (index === 0) {
return {
background: "linear-gradient(135deg, #FFD700 0%, #FFA500 100%)",
color: "#000",
};
}
if (index === 1) {
return {
background: "linear-gradient(135deg, #C0C0C0 0%, #A9A9A9 100%)",
color: "#000",
};
}
if (index === 2) {
return {
background: "linear-gradient(135deg, #CD7F32 0%, #8B4513 100%)",
color: "#fff",
};
}
return {
background: "rgba(255, 255, 255, 0.08)",
color: "#888",
};
};
/**
* 渲染排名徽章
*/
export const renderRankBadge = (index) => {
const style = getRankStyle(index);
return (
<div
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 24,
height: 24,
borderRadius: "50%",
fontSize: 12,
fontWeight: "bold",
...style,
}}
>
{index + 1}
</div>
);
};
/**
* 创建查看按钮渲染器
*/
export const createViewButtonRenderer = (onClick, iconType = "link") => {
const icons = {
link: <LinkOutlined />,
robot: <RobotOutlined />,
stock: <StockOutlined />,
chart: <LineChartOutlined />,
};
return (text, record) => (
<Button
type="link"
size="small"
icon={icons[iconType] || icons.link}
onClick={() => onClick(text, record)}
disabled={!text}
>
{text ? "查看" : "无"}
</Button>
);
};
/**
* 创建自选按钮渲染器
*/
export const createWatchlistButtonRenderer = (isInWatchlist, onAdd) => {
return (_, record) => {
const inWatchlist = isInWatchlist(record.code);
return (
<Button
type={inWatchlist ? "primary" : "default"}
size="small"
icon={inWatchlist ? <StarFilled /> : <StarOutlined />}
onClick={() => onAdd(record)}
disabled={inWatchlist}
>
{inWatchlist ? "已添加" : "加自选"}
</Button>
);
};
};
/**
* 创建K线按钮渲染器
*/
export const createKlineButtonRenderer = (showKline) => {
return (_, record) => (
<Button
type="primary"
size="small"
icon={<LineChartOutlined />}
onClick={() => showKline(record)}
>
查看
</Button>
);
};

View File

@@ -0,0 +1,368 @@
// 涨停板块表格列定义
// 用于涨停分析板块视图
import React from "react";
import { Tag, Button, Tooltip, Typography } from "antd";
import { FireOutlined } from "@ant-design/icons";
import { Box, HStack, VStack } from "@chakra-ui/react";
import { FileText } from "lucide-react";
const { Text: AntText } = Typography;
// 获取排名样式
const getRankStyle = (index) => {
if (index === 0) {
return {
background: "linear-gradient(135deg, #FFD700 0%, #FFA500 100%)",
color: "#000",
fontWeight: "bold",
};
}
if (index === 1) {
return {
background: "linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%)",
color: "#000",
fontWeight: "bold",
};
}
if (index === 2) {
return {
background: "linear-gradient(135deg, #CD7F32 0%, #A0522D 100%)",
color: "#fff",
fontWeight: "bold",
};
}
return { background: "rgba(255,255,255,0.1)", color: "#888" };
};
// 获取涨停数颜色
const getCountColor = (count) => {
if (count >= 8) return { bg: "#ff4d4f", text: "#fff" };
if (count >= 5) return { bg: "#fa541c", text: "#fff" };
if (count >= 3) return { bg: "#fa8c16", text: "#fff" };
return { bg: "rgba(255,215,0,0.2)", text: "#FFD700" };
};
// 获取相关度颜色
const getRelevanceColor = (score) => {
if (score >= 80) return "#10B981";
if (score >= 60) return "#F59E0B";
return "#6B7280";
};
/**
* 创建涨停板块表格列
* @param {Object} options - 配置选项
* @param {Array} options.stockList - 股票列表数据
* @param {Function} options.setSelectedSectorInfo - 设置选中板块信息
* @param {Function} options.setSectorStocksModalVisible - 设置板块股票弹窗可见性
* @param {Function} options.setSelectedRelatedEvents - 设置关联事件
* @param {Function} options.setRelatedEventsModalVisible - 设置关联事件弹窗可见性
*/
export const createSectorColumns = ({
stockList,
setSelectedSectorInfo,
setSectorStocksModalVisible,
setSelectedRelatedEvents,
setRelatedEventsModalVisible,
}) => [
{
title: "排名",
key: "rank",
width: 60,
align: "center",
render: (_, __, index) => {
const style = getRankStyle(index);
return (
<div
style={{
width: 28,
height: 28,
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "14px",
margin: "0 auto",
...style,
}}
>
{index + 1}
</div>
);
},
},
{
title: "板块名称",
dataIndex: "name",
key: "name",
width: 130,
render: (name, record, index) => (
<HStack spacing={2}>
<Box
w="4px"
h="24px"
borderRadius="full"
bg={
index < 3
? "linear-gradient(180deg, #FFD700 0%, #FF8C00 100%)"
: "whiteAlpha.300"
}
/>
<AntText
strong
style={{
color: index < 3 ? "#FFD700" : "#E0E0E0",
fontSize: "14px",
}}
>
{name}
</AntText>
</HStack>
),
},
{
title: "涨停数",
dataIndex: "count",
key: "count",
width: 90,
align: "center",
render: (count) => {
const colors = getCountColor(count);
return (
<HStack justify="center" spacing={1}>
<Box
px={3}
py={1}
borderRadius="full"
bg={colors.bg}
display="flex"
alignItems="center"
gap={1}
>
<FireOutlined style={{ color: colors.text, fontSize: "12px" }} />
<span
style={{
color: colors.text,
fontWeight: "bold",
fontSize: "14px",
}}
>
{count}
</span>
</Box>
</HStack>
);
},
},
{
title: "涨停股票",
dataIndex: "stocks",
key: "stocks",
render: (stocks, record) => {
// 根据股票代码查找股票详情,并按连板天数排序
const getStockInfoList = () => {
return stocks
.map((code) => {
const stockInfo = stockList.find((s) => s.scode === code);
return stockInfo || { sname: code, scode: code, _continuousDays: 1 };
})
.sort((a, b) => (b._continuousDays || 1) - (a._continuousDays || 1));
};
const stockInfoList = getStockInfoList();
const displayStocks = stockInfoList.slice(0, 4);
const handleShowAll = (e) => {
e.stopPropagation();
setSelectedSectorInfo({
name: record.name,
count: record.count,
stocks: stockInfoList,
});
setSectorStocksModalVisible(true);
};
return (
<HStack spacing={1} flexWrap="wrap" align="center">
{displayStocks.map((info) => (
<Tooltip
key={info.scode}
title={
<Box>
<div style={{ fontWeight: "bold", marginBottom: 4 }}>
{info.sname}
</div>
<div style={{ fontSize: "12px", color: "#888" }}>
{info.scode}
</div>
{info.continuous_days && (
<div
style={{
fontSize: "12px",
marginTop: 4,
color: "#fa8c16",
}}
>
{info.continuous_days}
</div>
)}
</Box>
}
placement="top"
>
<Tag
style={{
cursor: "pointer",
margin: "2px",
background:
info._continuousDays >= 3
? "rgba(255, 77, 79, 0.2)"
: info._continuousDays >= 2
? "rgba(250, 140, 22, 0.2)"
: "rgba(59, 130, 246, 0.15)",
border:
info._continuousDays >= 3
? "1px solid rgba(255, 77, 79, 0.4)"
: info._continuousDays >= 2
? "1px solid rgba(250, 140, 22, 0.4)"
: "1px solid rgba(59, 130, 246, 0.3)",
borderRadius: "6px",
}}
>
<a
href={`https://valuefrontier.cn/company?scode=${info.scode}`}
target="_blank"
rel="noopener noreferrer"
style={{
color:
info._continuousDays >= 3
? "#ff4d4f"
: info._continuousDays >= 2
? "#fa8c16"
: "#60A5FA",
fontSize: "13px",
}}
>
{info.sname}
{info._continuousDays > 1 && (
<span style={{ fontSize: "10px", marginLeft: 2 }}>
({info._continuousDays})
</span>
)}
</a>
</Tag>
</Tooltip>
))}
{stocks.length > 4 && (
<Button
type="link"
size="small"
onClick={handleShowAll}
style={{
padding: "0 4px",
height: "auto",
fontSize: "12px",
color: "#FFD700",
}}
>
查看全部 {stocks.length}
</Button>
)}
</HStack>
);
},
},
{
title: "涨停归因",
dataIndex: "related_events",
key: "related_events",
width: 280,
render: (events, record) => {
if (!events || events.length === 0) {
return (
<AntText style={{ color: "#666", fontSize: "12px" }}>-</AntText>
);
}
// 取相关度最高的事件
const sortedEvents = [...events].sort(
(a, b) => (b.relevance_score || 0) - (a.relevance_score || 0)
);
const topEvent = sortedEvents[0];
// 点击打开事件详情弹窗
const handleClick = (e) => {
e.stopPropagation();
setSelectedRelatedEvents({
sectorName: record.name,
events: sortedEvents,
count: record.count,
});
setRelatedEventsModalVisible(true);
};
return (
<VStack align="start" spacing={1}>
<Box
cursor="pointer"
p={1.5}
borderRadius="md"
bg="rgba(96, 165, 250, 0.1)"
_hover={{
bg: "rgba(96, 165, 250, 0.2)",
transform: "translateY(-1px)",
}}
transition="all 0.2s"
maxW="260px"
onClick={handleClick}
>
<HStack spacing={1.5} align="start">
<FileText
size={14}
color="#60A5FA"
style={{ flexShrink: 0, marginTop: 2 }}
/>
<VStack align="start" spacing={0.5} flex={1}>
<AntText
style={{
color: "#E0E0E0",
fontSize: "12px",
lineHeight: "1.3",
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{topEvent.title}
</AntText>
<HStack spacing={2}>
<Tag
style={{
fontSize: "10px",
padding: "0 6px",
background: `${getRelevanceColor(
topEvent.relevance_score || 0
)}20`,
border: "none",
color: getRelevanceColor(topEvent.relevance_score || 0),
borderRadius: "4px",
}}
>
相关度 {topEvent.relevance_score || 0}
</Tag>
{events.length > 1 && (
<AntText style={{ fontSize: "10px", color: "#888" }}>
+{events.length - 1}
</AntText>
)}
</HStack>
</VStack>
</HStack>
</Box>
</VStack>
);
},
},
];

View File

@@ -0,0 +1,233 @@
// 相关股票表格列定义
// 用于事件关联股票弹窗
import React from "react";
import { Tag, Button, Tooltip, Typography } from "antd";
import { StarFilled, StarOutlined, LineChartOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import { getSixDigitCode } from "../utils";
const { Text: AntText } = Typography;
/**
* 创建相关股票表格列
* @param {Object} options - 配置选项
* @param {Object} options.stockQuotes - 股票行情数据
* @param {Object} options.expandedReasons - 展开状态
* @param {Function} options.setExpandedReasons - 设置展开状态
* @param {Function} options.showKline - 显示K线
* @param {Function} options.isStockInWatchlist - 检查是否在自选
* @param {Function} options.addSingleToWatchlist - 添加到自选
*/
export const createStockColumns = ({
stockQuotes,
expandedReasons,
setExpandedReasons,
showKline,
isStockInWatchlist,
addSingleToWatchlist,
}) => [
{
title: "代码",
dataIndex: "code",
key: "code",
width: 90,
render: (code) => {
const sixDigitCode = getSixDigitCode(code);
return (
<a
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: "#3B82F6" }}
>
{sixDigitCode}
</a>
);
},
},
{
title: "名称",
dataIndex: "name",
key: "name",
width: 100,
render: (name, record) => {
const sixDigitCode = getSixDigitCode(record.code);
return (
<a
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
target="_blank"
rel="noopener noreferrer"
>
<AntText strong>{name}</AntText>
</a>
);
},
},
{
title: "现价",
key: "price",
width: 80,
render: (_, record) => {
const quote = stockQuotes[record.code];
if (quote && quote.price !== undefined) {
return (
<AntText type={quote.change > 0 ? "danger" : "success"}>
{quote.price?.toFixed(2)}
</AntText>
);
}
return <AntText>-</AntText>;
},
},
{
title: "涨跌幅",
key: "change",
width: 100,
render: (_, record) => {
const quote = stockQuotes[record.code];
if (quote && quote.changePercent !== undefined) {
const changePercent = quote.changePercent || 0;
return (
<Tag
color={
changePercent > 0
? "red"
: changePercent < 0
? "green"
: "default"
}
>
{changePercent > 0 ? "+" : ""}
{changePercent.toFixed(2)}%
</Tag>
);
}
return <AntText>-</AntText>;
},
},
{
title: "关联理由",
dataIndex: "description",
key: "reason",
render: (description, record) => {
const stockCode = record.code;
const isExpanded = expandedReasons[stockCode] || false;
const reason = typeof description === "string" ? description : "";
const shouldTruncate = reason && reason.length > 80;
const toggleExpanded = () => {
setExpandedReasons((prev) => ({
...prev,
[stockCode]: !prev[stockCode],
}));
};
return (
<div>
<AntText style={{ fontSize: "13px", lineHeight: "1.6" }}>
{isExpanded || !shouldTruncate
? reason || "-"
: `${reason?.slice(0, 80)}...`}
</AntText>
{shouldTruncate && (
<Button
type="link"
size="small"
onClick={toggleExpanded}
style={{ padding: 0, marginLeft: 4, fontSize: "12px" }}
>
({isExpanded ? "收起" : "展开"})
</Button>
)}
{reason && (
<div style={{ marginTop: 4 }}>
<AntText type="secondary" style={{ fontSize: "11px" }}>
(AI合成)
</AntText>
</div>
)}
</div>
);
},
},
{
title: "研报引用",
dataIndex: "report",
key: "report",
width: 180,
render: (report) => {
if (!report || !report.title) {
return <AntText type="secondary">-</AntText>;
}
return (
<div style={{ fontSize: "12px" }}>
<Tooltip title={report.sentences || report.title}>
<div>
<AntText strong style={{ display: "block", marginBottom: 2 }}>
{report.title.length > 18
? `${report.title.slice(0, 18)}...`
: report.title}
</AntText>
{report.author && (
<AntText
type="secondary"
style={{ display: "block", fontSize: "11px" }}
>
{report.author}
</AntText>
)}
{report.declare_date && (
<AntText type="secondary" style={{ fontSize: "11px" }}>
{dayjs(report.declare_date).format("YYYY-MM-DD")}
</AntText>
)}
{report.match_score && (
<Tag
color={report.match_score === "好" ? "green" : "blue"}
style={{ marginLeft: 4, fontSize: "10px" }}
>
匹配度: {report.match_score}
</Tag>
)}
</div>
</Tooltip>
</div>
);
},
},
{
title: "K线图",
key: "kline",
width: 80,
render: (_, record) => (
<Button
type="primary"
size="small"
icon={<LineChartOutlined />}
onClick={() => showKline(record)}
>
查看
</Button>
),
},
{
title: "操作",
key: "action",
width: 90,
render: (_, record) => {
const inWatchlist = isStockInWatchlist(record.code);
return (
<Button
type={inWatchlist ? "primary" : "default"}
size="small"
icon={inWatchlist ? <StarFilled /> : <StarOutlined />}
onClick={() => addSingleToWatchlist(record)}
disabled={inWatchlist}
>
{inWatchlist ? "已添加" : "加自选"}
</Button>
);
},
},
];

View File

@@ -0,0 +1,284 @@
// 涨停股票详情表格列定义
// 用于涨停分析个股视图
import React from "react";
import { Tag, Button, Tooltip, Typography } from "antd";
import { StarFilled, StarOutlined, LineChartOutlined } from "@ant-design/icons";
import { Box, HStack, VStack } from "@chakra-ui/react";
import { getTimeStyle, getDaysStyle } from "../utils";
const { Text: AntText } = Typography;
/**
* 创建涨停股票详情表格列
* @param {Object} options - 配置选项
* @param {Function} options.showContentDetail - 显示内容详情
* @param {Function} options.setSelectedKlineStock - 设置K线股票
* @param {Function} options.setKlineModalVisible - 设置K线弹窗可见性
* @param {Function} options.isStockInWatchlist - 检查是否在自选
* @param {Function} options.addSingleToWatchlist - 添加到自选
*/
export const createZtStockColumns = ({
showContentDetail,
setSelectedKlineStock,
setKlineModalVisible,
isStockInWatchlist,
addSingleToWatchlist,
}) => [
{
title: "股票信息",
key: "stock",
width: 140,
fixed: "left",
render: (_, record) => (
<VStack align="start" spacing={0}>
<a
href={`https://valuefrontier.cn/company?scode=${record.scode}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: "#FFD700", fontWeight: "bold", fontSize: "14px" }}
>
{record.sname}
</a>
<AntText style={{ color: "#60A5FA", fontSize: "12px" }}>
{record.scode}
</AntText>
</VStack>
),
},
{
title: "涨停时间",
dataIndex: "formatted_time",
key: "time",
width: 90,
align: "center",
render: (time) => {
const style = getTimeStyle(time || "15:00:00");
return (
<VStack spacing={0}>
<Box
px={2}
py={0.5}
borderRadius="md"
bg={style.bg}
fontSize="13px"
fontWeight="bold"
color={style.text}
>
{time?.substring(0, 5) || "-"}
</Box>
<AntText style={{ fontSize: "10px", color: "#888" }}>
{style.label}
</AntText>
</VStack>
);
},
},
{
title: "连板",
dataIndex: "continuous_days",
key: "continuous",
width: 70,
align: "center",
render: (text) => {
if (!text || text === "首板") {
return (
<Box
px={2}
py={0.5}
borderRadius="md"
bg="rgba(255,255,255,0.1)"
fontSize="12px"
color="#888"
>
首板
</Box>
);
}
const match = text.match(/(\d+)/);
const days = match ? parseInt(match[1]) : 1;
const style = getDaysStyle(days);
return (
<Box
px={2}
py={0.5}
borderRadius="md"
bg={style.bg}
fontSize="13px"
fontWeight="bold"
color={style.text}
>
{text}
</Box>
);
},
},
{
title: "核心板块",
dataIndex: "core_sectors",
key: "sectors",
width: 200,
render: (sectors) => (
<HStack spacing={1} flexWrap="wrap">
{(sectors || []).slice(0, 3).map((sector, idx) => (
<Tag
key={idx}
style={{
margin: "2px",
background:
idx === 0
? "linear-gradient(135deg, rgba(255,215,0,0.25) 0%, rgba(255,165,0,0.15) 100%)"
: "rgba(255,215,0,0.1)",
border:
idx === 0
? "1px solid rgba(255,215,0,0.5)"
: "1px solid rgba(255,215,0,0.2)",
borderRadius: "6px",
color: idx === 0 ? "#FFD700" : "#D4A84B",
fontSize: "12px",
fontWeight: idx === 0 ? "bold" : "normal",
}}
>
{sector}
</Tag>
))}
</HStack>
),
},
{
title: "涨停简报",
dataIndex: "brief",
key: "brief",
width: 200,
render: (text, record) => {
if (!text) return <AntText type="secondary">-</AntText>;
// 移除HTML标签
const cleanText = text
.replace(/<br\s*\/?>/gi, " ")
.replace(/<[^>]+>/g, "");
return (
<Tooltip
title={
<Box maxW="400px" p={2}>
<div
style={{
fontWeight: "bold",
marginBottom: 8,
color: "#FFD700",
}}
>
{record.sname} 涨停简报
</div>
<div
style={{
fontSize: "13px",
lineHeight: 1.6,
whiteSpace: "pre-wrap",
}}
>
{cleanText}
</div>
</Box>
}
placement="topLeft"
overlayStyle={{ maxWidth: 450 }}
>
<Button
type="link"
size="small"
onClick={() =>
showContentDetail(
text.replace(/<br\s*\/?>/gi, "\n\n"),
`${record.sname} 涨停简报`
)
}
style={{
padding: 0,
height: "auto",
whiteSpace: "normal",
textAlign: "left",
color: "#60A5FA",
fontSize: "13px",
}}
>
{cleanText.length > 30
? cleanText.substring(0, 30) + "..."
: cleanText}
</Button>
</Tooltip>
);
},
},
{
title: "K线图",
key: "kline",
width: 80,
align: "center",
render: (_, record) => (
<Button
type="primary"
size="small"
icon={<LineChartOutlined />}
onClick={() => {
// 添加交易所后缀
const code = record.scode;
let stockCode = code;
if (!code.includes(".")) {
if (code.startsWith("6")) stockCode = `${code}.SH`;
else if (code.startsWith("0") || code.startsWith("3"))
stockCode = `${code}.SZ`;
else if (code.startsWith("688")) stockCode = `${code}.SH`;
}
setSelectedKlineStock({
stock_code: stockCode,
stock_name: record.sname,
});
setKlineModalVisible(true);
}}
style={{
background: "linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%)",
border: "none",
borderRadius: "6px",
}}
>
查看
</Button>
),
},
{
title: "操作",
key: "action",
width: 90,
align: "center",
render: (_, record) => {
const code = record.scode;
const inWatchlist = isStockInWatchlist(code);
return (
<Button
type={inWatchlist ? "primary" : "default"}
size="small"
icon={inWatchlist ? <StarFilled /> : <StarOutlined />}
onClick={() => addSingleToWatchlist({ code, name: record.sname })}
disabled={inWatchlist}
style={
inWatchlist
? {
background:
"linear-gradient(135deg, #faad14 0%, #fa8c16 100%)",
border: "none",
borderRadius: "6px",
}
: {
background: "rgba(255,255,255,0.1)",
border: "1px solid rgba(255,215,0,0.3)",
borderRadius: "6px",
color: "#FFD700",
}
}
>
{inWatchlist ? "已添加" : "加自选"}
</Button>
);
},
},
];