更新Company页面的UI为FUI风格

This commit is contained in:
2025-12-22 07:48:16 +08:00
parent 453c2f8635
commit ee734e719e

View File

@@ -1,5 +1,5 @@
// src/views/Community/components/DynamicNews/layouts/MainlineTimelineView.js
// 主线时间轴布局组件 - 按 lv2 概念分组展示事件
// 主线时间轴布局组件 - 按 lv2 概念分组展示事件(卡片式布局)
import React, {
useState,
@@ -28,9 +28,8 @@ import {
ChevronDownIcon,
ChevronRightIcon,
RepeatIcon,
TimeIcon,
} from "@chakra-ui/icons";
import { FiTrendingUp, FiClock, FiZap } from "react-icons/fi";
import { FiTrendingUp, FiZap } from "react-icons/fi";
import DynamicNewsEventCard from "../../EventCard/DynamicNewsEventCard";
import { getApiBase } from "@utils/apiConfig";
@@ -39,8 +38,8 @@ import { getApiBase } from "@utils/apiConfig";
*
* 功能:
* 1. 调用 /api/events/mainline 获取预分组数据
* 2. 按 lv2 概念分组展示,时间轴样式
* 3. 不使用无限滚动,一次性加载全部数据
* 2. 按 lv2 概念分组展示,卡片式布局 + 内部时间轴
* 3. 按事件数量从多到少排序
*/
const MainlineTimelineViewComponent = forwardRef(
(
@@ -62,14 +61,15 @@ const MainlineTimelineViewComponent = forwardRef(
const [mainlineData, setMainlineData] = useState(null);
const [expandedGroups, setExpandedGroups] = useState({});
// 主题颜色
const bgColor = useColorModeValue("white", "gray.800");
const headerBg = useColorModeValue("gray.50", "gray.700");
const headerHoverBg = useColorModeValue("gray.100", "gray.600");
const textColor = useColorModeValue("gray.700", "gray.200");
const secondaryTextColor = useColorModeValue("gray.500", "gray.400");
const timelineBorderColor = useColorModeValue("gray.200", "gray.600");
const timelineLineColor = useColorModeValue("blue.200", "blue.600");
// 主题颜色 - 与列表模式统一的深色风格
const cardBg = useColorModeValue("white", "gray.800");
const cardBorderColor = useColorModeValue("gray.200", "gray.600");
const headerBg = useColorModeValue("gray.50", "gray.750");
const headerHoverBg = useColorModeValue("gray.100", "gray.700");
const textColor = useColorModeValue("gray.800", "gray.100");
const secondaryTextColor = useColorModeValue("gray.600", "gray.400");
const timelineLineColor = useColorModeValue("blue.300", "blue.500");
const timelineDotBg = useColorModeValue("blue.500", "blue.400");
const scrollbarTrackBg = useColorModeValue("#f1f1f1", "#2D3748");
const scrollbarThumbBg = useColorModeValue("#888", "#4A5568");
const scrollbarThumbHoverBg = useColorModeValue("#555", "#718096");
@@ -179,11 +179,19 @@ const MainlineTimelineViewComponent = forwardRef(
});
if (result.success) {
setMainlineData(result.data);
// 按事件数量从多到少排序
const sortedMainlines = [...(result.data.mainlines || [])].sort(
(a, b) => b.event_count - a.event_count
);
setMainlineData({
...result.data,
mainlines: sortedMainlines,
});
// 初始化展开状态默认展开前5个
const initialExpanded = {};
result.data.mainlines?.slice(0, 5).forEach((mainline) => {
sortedMainlines.slice(0, 5).forEach((mainline) => {
initialExpanded[mainline.lv2_id] = true;
});
setExpandedGroups(initialExpanded);
@@ -208,7 +216,7 @@ const MainlineTimelineViewComponent = forwardRef(
ref,
() => ({
refresh: fetchMainlineData,
getScrollPosition: () => null, // 不使用滚动位置判断
getScrollPosition: () => null,
}),
[fetchMainlineData]
);
@@ -295,11 +303,10 @@ const MainlineTimelineViewComponent = forwardRef(
<Box
display={display}
overflowY="auto"
minH="600px"
maxH="800px"
h="100%"
w="100%"
bg={bgColor}
borderRadius="md"
px={2}
py={2}
css={{
"&::-webkit-scrollbar": { width: "6px" },
"&::-webkit-scrollbar-track": {
@@ -320,40 +327,36 @@ const MainlineTimelineViewComponent = forwardRef(
<Flex
justify="space-between"
align="center"
px={4}
py={3}
borderBottomWidth="1px"
borderColor={timelineBorderColor}
position="sticky"
top={0}
bg={bgColor}
zIndex={10}
px={3}
py={2}
mb={3}
bg={headerBg}
borderRadius="lg"
borderWidth="1px"
borderColor={cardBorderColor}
>
<HStack spacing={4}>
<HStack spacing={2}>
<Icon as={FiTrendingUp} color="blue.500" />
<Text fontWeight="semibold" color={textColor}>
<Text fontWeight="semibold" color={textColor} fontSize="sm">
{mainline_count} 条主线
</Text>
</HStack>
<HStack spacing={2}>
<Icon as={FiClock} color="green.500" />
<Text fontSize="sm" color={secondaryTextColor}>
{total_events} 个事件
</Text>
</HStack>
<Text fontSize="sm" color={secondaryTextColor}>
{total_events} 个事件
</Text>
{ungrouped_count > 0 && (
<Text fontSize="sm" color="orange.500">
<Text fontSize="xs" color="orange.500">
({ungrouped_count} 个未归类)
</Text>
)}
</HStack>
<HStack spacing={2}>
<HStack spacing={1}>
<Tooltip label="全部展开">
<IconButton
icon={<ChevronDownIcon />}
size="sm"
size="xs"
variant="ghost"
onClick={() => toggleAll(true)}
aria-label="全部展开"
@@ -362,7 +365,7 @@ const MainlineTimelineViewComponent = forwardRef(
<Tooltip label="全部折叠">
<IconButton
icon={<ChevronRightIcon />}
size="sm"
size="xs"
variant="ghost"
onClick={() => toggleAll(false)}
aria-label="全部折叠"
@@ -371,7 +374,7 @@ const MainlineTimelineViewComponent = forwardRef(
<Tooltip label="刷新">
<IconButton
icon={<RepeatIcon />}
size="sm"
size="xs"
variant="ghost"
onClick={fetchMainlineData}
aria-label="刷新"
@@ -380,151 +383,144 @@ const MainlineTimelineViewComponent = forwardRef(
</HStack>
</Flex>
{/* 主线列表 - 时间轴样式 */}
<Box position="relative" px={4} py={4}>
{/* 时间轴垂直线 */}
<Box
position="absolute"
left="28px"
top="0"
bottom="0"
w="2px"
bg={timelineLineColor}
opacity={0.5}
/>
{/* 主线卡片列表 */}
<VStack spacing={3} align="stretch">
{mainlines.map((mainline) => {
const colorScheme = getColorScheme(mainline.lv2_name);
const isExpanded = expandedGroups[mainline.lv2_id];
<VStack spacing={4} align="stretch">
{mainlines.map((mainline, index) => {
const colorScheme = getColorScheme(mainline.lv2_name);
const isExpanded = expandedGroups[mainline.lv2_id];
return (
<Box key={mainline.lv2_id} position="relative" pl={10}>
{/* 时间轴圆点 */}
<Box
position="absolute"
left="22px"
top="16px"
w="14px"
h="14px"
borderRadius="full"
bg={`${colorScheme}.500`}
border="3px solid"
borderColor={bgColor}
boxShadow="0 0 0 2px"
sx={{ boxShadowColor: `${colorScheme}.200` }}
zIndex={2}
/>
{/* 分组头部 */}
<Flex
align="center"
justify="space-between"
px={4}
py={3}
bg={headerBg}
borderRadius="lg"
borderWidth="1px"
borderColor={timelineBorderColor}
cursor="pointer"
_hover={{ bg: headerHoverBg }}
onClick={() => toggleGroup(mainline.lv2_id)}
transition="all 0.2s"
boxShadow="sm"
>
<HStack spacing={3}>
<Icon
as={isExpanded ? ChevronDownIcon : ChevronRightIcon}
boxSize={5}
color={textColor}
transition="transform 0.2s"
/>
<VStack align="start" spacing={0}>
<HStack spacing={2}>
<Text
fontWeight="bold"
fontSize="md"
color={textColor}
>
{mainline.lv2_name}
</Text>
<Badge
colorScheme={colorScheme}
fontSize="xs"
borderRadius="full"
px={2}
>
{mainline.event_count}
</Badge>
</HStack>
return (
<Box
key={mainline.lv2_id || "ungrouped"}
bg={cardBg}
borderRadius="lg"
borderWidth="1px"
borderColor={cardBorderColor}
overflow="hidden"
boxShadow="sm"
_hover={{ boxShadow: "md" }}
transition="all 0.2s"
>
{/* 卡片头部 - 概念名称 */}
<Flex
align="center"
justify="space-between"
px={4}
py={3}
bg={headerBg}
cursor="pointer"
_hover={{ bg: headerHoverBg }}
onClick={() => toggleGroup(mainline.lv2_id)}
transition="all 0.2s"
borderBottomWidth={isExpanded ? "1px" : "0"}
borderBottomColor={cardBorderColor}
>
<HStack spacing={3}>
<Icon
as={isExpanded ? ChevronDownIcon : ChevronRightIcon}
boxSize={5}
color={textColor}
transition="transform 0.2s"
/>
<Box
w="4px"
h="24px"
bg={`${colorScheme}.500`}
borderRadius="full"
/>
<VStack align="start" spacing={0}>
<HStack spacing={2}>
<Text
fontWeight="bold"
fontSize="md"
color={textColor}
>
{mainline.lv2_name || "其他"}
</Text>
<Badge
colorScheme={colorScheme}
fontSize="xs"
borderRadius="full"
px={2}
>
{mainline.event_count}
</Badge>
</HStack>
{mainline.lv1_name && (
<Text fontSize="xs" color={secondaryTextColor}>
{mainline.lv1_name}
</Text>
</VStack>
</HStack>
)}
</VStack>
</HStack>
</Flex>
<HStack spacing={2}>
<Icon
as={TimeIcon}
color={secondaryTextColor}
boxSize={3}
/>
<Text fontSize="xs" color={secondaryTextColor}>
#{index + 1}
</Text>
</HStack>
</Flex>
{/* 分组内容 */}
<Collapse in={isExpanded} animateOpacity>
{/* 卡片内容 - 时间轴事件列表 */}
<Collapse in={isExpanded} animateOpacity>
<Box px={4} py={3} position="relative">
{/* 时间轴线 */}
<Box
mt={3}
pl={2}
borderLeftWidth="2px"
borderLeftColor={`${colorScheme}.200`}
borderLeftStyle="dashed"
>
<Grid
templateColumns={`repeat(${columnsPerRow}, 1fr)`}
gap={3}
w="100%"
>
{mainline.events.map((event, eventIndex) => (
<Box key={event.id} w="100%" minW={0}>
<DynamicNewsEventCard
event={event}
index={eventIndex}
isFollowing={
eventFollowStatus[event.id]?.isFollowing ||
false
}
followerCount={
eventFollowStatus[event.id]?.followerCount ||
event.follower_count ||
0
}
isSelected={selectedEvent?.id === event.id}
onEventClick={(clickedEvent) => {
onEventSelect?.(clickedEvent);
}}
onTitleClick={(e) => {
e.preventDefault();
e.stopPropagation();
onEventSelect?.(event);
}}
onToggleFollow={() => onToggleFollow?.(event.id)}
borderColor={borderColor}
/>
</Box>
))}
</Grid>
</Box>
</Collapse>
</Box>
);
})}
</VStack>
</Box>
position="absolute"
left="24px"
top="12px"
bottom="12px"
w="2px"
bg={timelineLineColor}
opacity={0.4}
borderRadius="full"
/>
{/* 事件列表 */}
<VStack spacing={3} align="stretch" pl={8}>
{mainline.events.map((event, eventIndex) => (
<Box key={event.id} position="relative">
{/* 时间轴圆点 */}
<Box
position="absolute"
left="-26px"
top="50%"
transform="translateY(-50%)"
w="10px"
h="10px"
borderRadius="full"
bg={timelineDotBg}
border="2px solid"
borderColor={cardBg}
zIndex={1}
/>
{/* 事件卡片 */}
<DynamicNewsEventCard
event={event}
index={eventIndex}
isFollowing={
eventFollowStatus[event.id]?.isFollowing || false
}
followerCount={
eventFollowStatus[event.id]?.followerCount ||
event.follower_count ||
0
}
isSelected={selectedEvent?.id === event.id}
onEventClick={(clickedEvent) => {
onEventSelect?.(clickedEvent);
}}
onTitleClick={(e) => {
e.preventDefault();
e.stopPropagation();
onEventSelect?.(event);
}}
onToggleFollow={() => onToggleFollow?.(event.id)}
borderColor={borderColor}
/>
</Box>
))}
</VStack>
</Box>
</Collapse>
</Box>
);
})}
</VStack>
</Box>
);
}