更新Company页面的UI为FUI风格
This commit is contained in:
@@ -24,12 +24,12 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { ChevronDownIcon, ChevronUpIcon, RepeatIcon } from "@chakra-ui/icons";
|
import { ChevronDownIcon, ChevronUpIcon, RepeatIcon } from "@chakra-ui/icons";
|
||||||
import { FiTrendingUp, FiZap, FiClock } from "react-icons/fi";
|
import { FiTrendingUp, FiZap } from "react-icons/fi";
|
||||||
import { FireOutlined } from "@ant-design/icons";
|
import { FireOutlined } from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Select } from "antd";
|
import { Select } from "antd";
|
||||||
import MiniEventCard from "../../EventCard/MiniEventCard";
|
|
||||||
import { getApiBase } from "@utils/apiConfig";
|
import { getApiBase } from "@utils/apiConfig";
|
||||||
|
import { getChangeColor } from "@utils/colorUtils";
|
||||||
import "../../SearchFilters/CompactSearchBox.css";
|
import "../../SearchFilters/CompactSearchBox.css";
|
||||||
|
|
||||||
// 固定深色主题颜色
|
// 固定深色主题颜色
|
||||||
@@ -50,7 +50,7 @@ const COLORS = {
|
|||||||
const EVENTS_PER_LOAD = 12;
|
const EVENTS_PER_LOAD = 12;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化时间显示
|
* 格式化时间显示 - 始终显示日期,避免跨天混淆
|
||||||
*/
|
*/
|
||||||
const formatEventTime = (dateStr) => {
|
const formatEventTime = (dateStr) => {
|
||||||
if (!dateStr) return "";
|
if (!dateStr) return "";
|
||||||
@@ -59,78 +59,164 @@ const formatEventTime = (dateStr) => {
|
|||||||
const isToday = date.isSame(now, "day");
|
const isToday = date.isSame(now, "day");
|
||||||
const isYesterday = date.isSame(now.subtract(1, "day"), "day");
|
const isYesterday = date.isSame(now.subtract(1, "day"), "day");
|
||||||
|
|
||||||
|
// 始终显示日期,用标签区分今天/昨天
|
||||||
if (isToday) {
|
if (isToday) {
|
||||||
return date.format("HH:mm");
|
return `今天 ${date.format("MM-DD HH:mm")}`;
|
||||||
} else if (isYesterday) {
|
} else if (isYesterday) {
|
||||||
return `昨天 ${date.format("HH:mm")}`;
|
return `昨天 ${date.format("MM-DD HH:mm")}`;
|
||||||
} else {
|
} else {
|
||||||
return date.format("MM-DD HH:mm");
|
return date.format("MM-DD HH:mm");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单个事件项组件 - 带时间轴
|
* 根据涨跌幅获取背景色
|
||||||
*/
|
*/
|
||||||
const TimelineEventItem = React.memo(({ event, isSelected, onEventClick, isHot }) => {
|
const getChangeBgColor = (value) => {
|
||||||
// 使用后端返回的字段:related_max_chg(最大涨幅)或 related_avg_chg(平均涨幅)
|
if (value == null || isNaN(value)) return "transparent";
|
||||||
const changePct = event.related_max_chg ?? event.related_avg_chg ?? event.max_change_pct ?? event.change_pct;
|
const absChange = Math.abs(value);
|
||||||
const hasChange = changePct != null;
|
if (value > 0) {
|
||||||
const isPositive = hasChange && changePct >= 0;
|
if (absChange >= 5) return "rgba(239, 68, 68, 0.12)";
|
||||||
|
if (absChange >= 3) return "rgba(239, 68, 68, 0.08)";
|
||||||
|
return "rgba(239, 68, 68, 0.05)";
|
||||||
|
} else if (value < 0) {
|
||||||
|
if (absChange >= 5) return "rgba(16, 185, 129, 0.12)";
|
||||||
|
if (absChange >= 3) return "rgba(16, 185, 129, 0.08)";
|
||||||
|
return "rgba(16, 185, 129, 0.05)";
|
||||||
|
}
|
||||||
|
return "transparent";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个事件项组件 - 卡片式布局
|
||||||
|
*/
|
||||||
|
const TimelineEventItem = React.memo(({ event, isSelected, onEventClick }) => {
|
||||||
|
// 使用 related_max_chg 作为主要涨幅显示
|
||||||
|
const maxChange = event.related_max_chg;
|
||||||
|
const avgChange = event.related_avg_chg;
|
||||||
|
const hasMaxChange = maxChange != null && !isNaN(maxChange);
|
||||||
|
const hasAvgChange = avgChange != null && !isNaN(avgChange);
|
||||||
|
|
||||||
|
// 用于背景色的涨幅(使用平均超额)
|
||||||
|
const bgValue = avgChange;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<Box
|
||||||
spacing={0}
|
|
||||||
align="flex-start"
|
|
||||||
w="100%"
|
w="100%"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={() => onEventClick?.(event)}
|
onClick={() => onEventClick?.(event)}
|
||||||
_hover={{ bg: "rgba(255, 255, 255, 0.06)" }}
|
bg={isSelected ? "rgba(66, 153, 225, 0.15)" : getChangeBgColor(bgValue)}
|
||||||
borderRadius="md"
|
borderWidth="1px"
|
||||||
transition="all 0.15s"
|
borderColor={isSelected ? "#4299e1" : COLORS.cardBorderColor}
|
||||||
bg={isSelected ? "rgba(66, 153, 225, 0.15)" : "transparent"}
|
borderRadius="lg"
|
||||||
py={2}
|
p={3}
|
||||||
px={1}
|
mb={2}
|
||||||
|
_hover={{
|
||||||
|
bg: isSelected ? "rgba(66, 153, 225, 0.2)" : "rgba(255, 255, 255, 0.06)",
|
||||||
|
borderColor: isSelected ? "#63b3ed" : "#5a6070",
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
}}
|
||||||
|
transition="all 0.2s ease"
|
||||||
>
|
>
|
||||||
{/* 左侧时间 */}
|
{/* 第一行:时间 */}
|
||||||
<Text
|
<Text
|
||||||
fontSize="sm"
|
fontSize="xs"
|
||||||
color={COLORS.secondaryTextColor}
|
color={COLORS.secondaryTextColor}
|
||||||
fontWeight="500"
|
mb={1.5}
|
||||||
w="58px"
|
|
||||||
flexShrink={0}
|
|
||||||
textAlign="right"
|
|
||||||
pr={3}
|
|
||||||
>
|
>
|
||||||
{formatEventTime(event.created_at || event.event_time)}
|
{formatEventTime(event.created_at || event.event_time)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* 右侧内容 */}
|
{/* 第二行:标题 */}
|
||||||
<Box flex={1} minW={0} pr={2}>
|
<Text
|
||||||
<Text
|
fontSize="sm"
|
||||||
fontSize="sm"
|
color="#63b3ed"
|
||||||
color={COLORS.textColor}
|
fontWeight="medium"
|
||||||
noOfLines={2}
|
noOfLines={2}
|
||||||
lineHeight="1.6"
|
lineHeight="1.5"
|
||||||
>
|
mb={2}
|
||||||
{event.title}
|
_hover={{ textDecoration: "underline" }}
|
||||||
</Text>
|
>
|
||||||
</Box>
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* 涨跌幅 */}
|
{/* 第三行:涨跌幅指标 */}
|
||||||
{hasChange && (
|
{(hasMaxChange || hasAvgChange) && (
|
||||||
<Text
|
<HStack spacing={2} flexWrap="wrap">
|
||||||
fontSize="sm"
|
{/* 最大超额 */}
|
||||||
fontWeight="bold"
|
{hasMaxChange && (
|
||||||
color={isPositive ? "#fc8181" : "#68d391"}
|
<Box
|
||||||
flexShrink={0}
|
bg={maxChange > 0 ? "rgba(239, 68, 68, 0.15)" : "rgba(16, 185, 129, 0.15)"}
|
||||||
minW="50px"
|
borderWidth="1px"
|
||||||
textAlign="right"
|
borderColor={maxChange > 0 ? "rgba(239, 68, 68, 0.3)" : "rgba(16, 185, 129, 0.3)"}
|
||||||
>
|
borderRadius="md"
|
||||||
{isPositive ? "+" : ""}
|
px={2}
|
||||||
{changePct.toFixed(1)}%
|
py={1}
|
||||||
</Text>
|
>
|
||||||
|
<Text fontSize="xs" color={COLORS.secondaryTextColor} mb={0.5}>
|
||||||
|
最大超额
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={maxChange > 0 ? "#fc8181" : "#68d391"}
|
||||||
|
>
|
||||||
|
{maxChange > 0 ? "+" : ""}{maxChange.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 平均超额 */}
|
||||||
|
{hasAvgChange && (
|
||||||
|
<Box
|
||||||
|
bg={avgChange > 0 ? "rgba(239, 68, 68, 0.15)" : "rgba(16, 185, 129, 0.15)"}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={avgChange > 0 ? "rgba(239, 68, 68, 0.3)" : "rgba(16, 185, 129, 0.3)"}
|
||||||
|
borderRadius="md"
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
>
|
||||||
|
<Text fontSize="xs" color={COLORS.secondaryTextColor} mb={0.5}>
|
||||||
|
平均超额
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={avgChange > 0 ? "#fc8181" : "#68d391"}
|
||||||
|
>
|
||||||
|
{avgChange > 0 ? "+" : ""}{avgChange.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 超预期得分 */}
|
||||||
|
{event.expectation_surprise_score != null && (
|
||||||
|
<Box
|
||||||
|
bg={event.expectation_surprise_score >= 60 ? "rgba(239, 68, 68, 0.15)" :
|
||||||
|
event.expectation_surprise_score >= 40 ? "rgba(237, 137, 54, 0.15)" : "rgba(66, 153, 225, 0.15)"}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={event.expectation_surprise_score >= 60 ? "rgba(239, 68, 68, 0.3)" :
|
||||||
|
event.expectation_surprise_score >= 40 ? "rgba(237, 137, 54, 0.3)" : "rgba(66, 153, 225, 0.3)"}
|
||||||
|
borderRadius="md"
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
>
|
||||||
|
<Text fontSize="xs" color={COLORS.secondaryTextColor} mb={0.5}>
|
||||||
|
超预期
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={event.expectation_surprise_score >= 60 ? "#fc8181" :
|
||||||
|
event.expectation_surprise_score >= 40 ? "#ed8936" : "#63b3ed"}
|
||||||
|
>
|
||||||
|
{Math.round(event.expectation_surprise_score)}分
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,25 +245,23 @@ const MainlineCard = React.memo(
|
|||||||
}
|
}
|
||||||
}, [isExpanded]);
|
}, [isExpanded]);
|
||||||
|
|
||||||
// 找出涨幅最大的事件(HOT 事件)
|
// 找出最大超额涨幅最高的事件(HOT 事件)
|
||||||
const hotEvent = useMemo(() => {
|
const hotEvent = useMemo(() => {
|
||||||
if (!mainline.events || mainline.events.length === 0) return null;
|
if (!mainline.events || mainline.events.length === 0) return null;
|
||||||
let maxChange = -Infinity;
|
let maxChange = -Infinity;
|
||||||
let hot = null;
|
let hot = null;
|
||||||
mainline.events.forEach((event) => {
|
mainline.events.forEach((event) => {
|
||||||
// 使用后端返回的字段:related_max_chg(最大涨幅)
|
// 统一使用 related_max_chg(最大超额)
|
||||||
const change = event.related_max_chg ?? event.related_avg_chg ?? event.max_change_pct ?? event.change_pct ?? -Infinity;
|
const change = event.related_max_chg ?? -Infinity;
|
||||||
if (change > maxChange) {
|
if (change > maxChange) {
|
||||||
maxChange = change;
|
maxChange = change;
|
||||||
hot = event;
|
hot = event;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 只有当有正涨幅时才显示 HOT
|
// 只有当最大超额 > 0 时才显示 HOT
|
||||||
return maxChange > 0 ? hot : null;
|
return maxChange > 0 ? hot : null;
|
||||||
}, [mainline.events]);
|
}, [mainline.events]);
|
||||||
|
|
||||||
const hotEventId = hotEvent?.id;
|
|
||||||
|
|
||||||
// 当前显示的事件
|
// 当前显示的事件
|
||||||
const displayedEvents = useMemo(() => {
|
const displayedEvents = useMemo(() => {
|
||||||
return mainline.events.slice(0, displayCount);
|
return mainline.events.slice(0, displayCount);
|
||||||
@@ -292,8 +376,8 @@ const MainlineCard = React.memo(
|
|||||||
{hotEvent && (
|
{hotEvent && (
|
||||||
<Box
|
<Box
|
||||||
px={3}
|
px={3}
|
||||||
py={2.5}
|
py={3}
|
||||||
bg="rgba(245, 101, 101, 0.08)"
|
bg="rgba(245, 101, 101, 0.1)"
|
||||||
borderBottomWidth="1px"
|
borderBottomWidth="1px"
|
||||||
borderBottomColor={COLORS.cardBorderColor}
|
borderBottomColor={COLORS.cardBorderColor}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
@@ -301,11 +385,11 @@ const MainlineCard = React.memo(
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEventSelect?.(hotEvent);
|
onEventSelect?.(hotEvent);
|
||||||
}}
|
}}
|
||||||
_hover={{ bg: "rgba(245, 101, 101, 0.15)" }}
|
_hover={{ bg: "rgba(245, 101, 101, 0.18)" }}
|
||||||
transition="all 0.15s"
|
transition="all 0.15s"
|
||||||
>
|
>
|
||||||
<HStack spacing={2} align="start">
|
{/* 第一行:HOT 标签 + 最大超额 */}
|
||||||
{/* HOT 标签 */}
|
<HStack spacing={2} mb={1.5}>
|
||||||
<Badge
|
<Badge
|
||||||
bg="linear-gradient(135deg, #f56565 0%, #ed8936 100%)"
|
bg="linear-gradient(135deg, #f56565 0%, #ed8936 100%)"
|
||||||
color="white"
|
color="white"
|
||||||
@@ -313,7 +397,6 @@ const MainlineCard = React.memo(
|
|||||||
px={2}
|
px={2}
|
||||||
py={0.5}
|
py={0.5}
|
||||||
borderRadius="sm"
|
borderRadius="sm"
|
||||||
flexShrink={0}
|
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap="3px"
|
gap="3px"
|
||||||
@@ -322,28 +405,30 @@ const MainlineCard = React.memo(
|
|||||||
<FireOutlined style={{ fontSize: 11 }} />
|
<FireOutlined style={{ fontSize: 11 }} />
|
||||||
HOT
|
HOT
|
||||||
</Badge>
|
</Badge>
|
||||||
{/* HOT 事件标题 */}
|
{/* 最大超额涨幅 */}
|
||||||
<Text
|
{hotEvent.related_max_chg != null && (
|
||||||
fontSize="sm"
|
<Box
|
||||||
color={COLORS.textColor}
|
bg="rgba(239, 68, 68, 0.2)"
|
||||||
noOfLines={2}
|
borderRadius="md"
|
||||||
flex={1}
|
px={2}
|
||||||
lineHeight="1.5"
|
py={0.5}
|
||||||
>
|
|
||||||
{hotEvent.title}
|
|
||||||
</Text>
|
|
||||||
{/* HOT 事件涨幅 */}
|
|
||||||
{(hotEvent.related_max_chg ?? hotEvent.related_avg_chg) != null && (
|
|
||||||
<Text
|
|
||||||
fontSize="sm"
|
|
||||||
fontWeight="bold"
|
|
||||||
color="#fc8181"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
>
|
||||||
+{(hotEvent.related_max_chg ?? hotEvent.related_avg_chg).toFixed(1)}%
|
<Text fontSize="xs" color="#fc8181" fontWeight="bold">
|
||||||
</Text>
|
最大超额 +{hotEvent.related_max_chg.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
{/* 第二行:标题 */}
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
color={COLORS.textColor}
|
||||||
|
noOfLines={2}
|
||||||
|
lineHeight="1.5"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
{hotEvent.title}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -351,7 +436,7 @@ const MainlineCard = React.memo(
|
|||||||
{/* 事件列表区域 */}
|
{/* 事件列表区域 */}
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<Box
|
<Box
|
||||||
px={1}
|
px={2}
|
||||||
py={2}
|
py={2}
|
||||||
flex={1}
|
flex={1}
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
@@ -366,18 +451,15 @@ const MainlineCard = React.memo(
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 事件列表 - 带时间轴 */}
|
{/* 事件列表 - 卡片式 */}
|
||||||
<VStack spacing={0} align="stretch">
|
{displayedEvents.map((event) => (
|
||||||
{displayedEvents.map((event) => (
|
<TimelineEventItem
|
||||||
<TimelineEventItem
|
key={event.id}
|
||||||
key={event.id}
|
event={event}
|
||||||
event={event}
|
isSelected={selectedEvent?.id === event.id}
|
||||||
isSelected={selectedEvent?.id === event.id}
|
onEventClick={onEventSelect}
|
||||||
onEventClick={onEventSelect}
|
/>
|
||||||
isHot={event.id === hotEventId}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
{/* 加载更多按钮 */}
|
{/* 加载更多按钮 */}
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
@@ -389,7 +471,7 @@ const MainlineCard = React.memo(
|
|||||||
isLoading={isLoadingMore}
|
isLoading={isLoadingMore}
|
||||||
loadingText="加载中..."
|
loadingText="加载中..."
|
||||||
w="100%"
|
w="100%"
|
||||||
mt={2}
|
mt={1}
|
||||||
_hover={{ bg: COLORS.headerHoverBg }}
|
_hover={{ bg: COLORS.headerHoverBg }}
|
||||||
>
|
>
|
||||||
加载更多 ({mainline.events.length - displayCount} 条)
|
加载更多 ({mainline.events.length - displayCount} 条)
|
||||||
@@ -397,24 +479,21 @@ const MainlineCard = React.memo(
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
/* 折叠时显示简要信息 - 也带时间轴 */
|
/* 折叠时显示简要信息 - 卡片式 */
|
||||||
<Box px={1} py={2} flex={1} overflow="hidden">
|
<Box px={2} py={2} flex={1} overflow="hidden">
|
||||||
<VStack spacing={0} align="stretch">
|
{mainline.events.slice(0, 3).map((event) => (
|
||||||
{mainline.events.slice(0, 4).map((event) => (
|
<TimelineEventItem
|
||||||
<TimelineEventItem
|
key={event.id}
|
||||||
key={event.id}
|
event={event}
|
||||||
event={event}
|
isSelected={selectedEvent?.id === event.id}
|
||||||
isSelected={selectedEvent?.id === event.id}
|
onEventClick={onEventSelect}
|
||||||
onEventClick={onEventSelect}
|
/>
|
||||||
isHot={event.id === hotEventId}
|
))}
|
||||||
/>
|
{mainline.events.length > 3 && (
|
||||||
))}
|
<Text fontSize="sm" color={COLORS.secondaryTextColor} textAlign="center" pt={1}>
|
||||||
{mainline.events.length > 4 && (
|
... 还有 {mainline.events.length - 3} 条
|
||||||
<Text fontSize="xs" color={COLORS.secondaryTextColor} pl="50px" pt={1}>
|
</Text>
|
||||||
... 还有 {mainline.events.length - 4} 条
|
)}
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user