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:
132
src/views/Community/components/HeroPanel/columns/eventColumns.js
Normal file
132
src/views/Community/components/HeroPanel/columns/eventColumns.js
Normal 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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
// HeroPanel 表格列相关导出
|
||||
export * from './renderers';
|
||||
export { createStockColumns } from './stockColumns';
|
||||
export { createSectorColumns } from './sectorColumns';
|
||||
export { createZtStockColumns } from './ztStockColumns';
|
||||
export { createEventColumns } from './eventColumns';
|
||||
184
src/views/Community/components/HeroPanel/columns/renderers.js
Normal file
184
src/views/Community/components/HeroPanel/columns/renderers.js
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
233
src/views/Community/components/HeroPanel/columns/stockColumns.js
Normal file
233
src/views/Community/components/HeroPanel/columns/stockColumns.js
Normal 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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user