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