refactor(Community): 题材模式筛选控件移至搜索框 + 修复展开/折叠

- MainlineTimelineView: 通过 onControlsChange 暴露控件状态
- MainlineTimelineView: 修复 toggleAll 使用错误的 groupId 键
- CompactSearchBox: mainline 模式下渲染概念/排序选择器和操作按钮
- DynamicNewsCard: 管理 mainlineControls 状态并传递给子组件
- EventScrollList: 传递 onMainlineControlsChange 回调

布局变更:
搜索行:[搜索框] | [时间筛选] | [按三级概念▼] [按事件数量▼] [展开] [折叠] [刷新]
统计行:📈 51条主线 · 132个事件 · 15个未归类(小字)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2026-01-21 14:57:10 +08:00
parent 0b2185777e
commit 478eead899
4 changed files with 178 additions and 131 deletions

View File

@@ -90,6 +90,14 @@ const DynamicNewsCardComponent = forwardRef(({
const cardBodyRef = useRef(null); const cardBodyRef = useRef(null);
const mainlineRef = useRef(null); // MainlineTimelineView 的 ref const mainlineRef = useRef(null); // MainlineTimelineView 的 ref
// 主线模式控件状态(用于传递给 CompactSearchBox
const [mainlineControls, setMainlineControls] = useState(null);
// 主线模式控件变化回调
const handleMainlineControlsChange = useCallback((controls) => {
setMainlineControls(controls);
}, []);
// 从 Redux 读取关注状态 // 从 Redux 读取关注状态
const eventFollowStatus = useSelector(selectEventFollowStatus); const eventFollowStatus = useSelector(selectEventFollowStatus);
@@ -747,6 +755,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
pageSize={pageSize} pageSize={pageSize}
trackingFunctions={trackingFunctions} trackingFunctions={trackingFunctions}
isMobile={isMobile} isMobile={isMobile}
mainlineControls={mainlineControls}
/> />
</Box> </Box>
</VStack> </VStack>
@@ -808,6 +817,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
onToggleFollow={handleToggleFollow} onToggleFollow={handleToggleFollow}
onVoteChange={handleVoteChange} onVoteChange={handleVoteChange}
mainlineRef={mainlineRef} mainlineRef={mainlineRef}
onMainlineControlsChange={handleMainlineControlsChange}
/> />
</Box> </Box>
</CardBody> </CardBody>

View File

@@ -24,6 +24,7 @@ import VerticalModeLayout from "./layouts/VerticalModeLayout";
* @param {Function} onToggleFollow - 关注按钮回调 * @param {Function} onToggleFollow - 关注按钮回调
* @param {Function} onVoteChange - 投票变化回调 { eventId, voteType: 'bullish' | 'bearish' } * @param {Function} onVoteChange - 投票变化回调 { eventId, voteType: 'bullish' | 'bearish' }
* @param {React.Ref} mainlineRef - MainlineTimelineView 的 ref * @param {React.Ref} mainlineRef - MainlineTimelineView 的 ref
* @param {Function} onMainlineControlsChange - 主线模式控件状态变化回调
*/ */
const EventScrollList = React.memo( const EventScrollList = React.memo(
({ ({
@@ -43,6 +44,7 @@ const EventScrollList = React.memo(
onToggleFollow, onToggleFollow,
onVoteChange, onVoteChange,
mainlineRef, mainlineRef,
onMainlineControlsChange,
}) => { }) => {
const scrollContainerRef = useRef(null); const scrollContainerRef = useRef(null);
@@ -82,6 +84,7 @@ const EventScrollList = React.memo(
eventFollowStatus={eventFollowStatus} eventFollowStatus={eventFollowStatus}
onToggleFollow={onToggleFollow} onToggleFollow={onToggleFollow}
borderColor={borderColor} borderColor={borderColor}
onControlsChange={onMainlineControlsChange}
/> />
{/* 纵向分栏模式 */} {/* 纵向分栏模式 */}

View File

@@ -20,10 +20,8 @@ import {
Spinner, Spinner,
Center, Center,
IconButton, IconButton,
Tooltip,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { ChevronDown, ChevronUp, RefreshCw, TrendingUp, Zap } from "lucide-react"; import { RefreshCw, TrendingUp, Zap } from "lucide-react";
import { Select } from "antd";
import { getApiBase } from "@utils/apiConfig"; import { getApiBase } from "@utils/apiConfig";
import "../../SearchFilters/CompactSearchBox.css"; import "../../SearchFilters/CompactSearchBox.css";
@@ -44,6 +42,7 @@ const MainlineTimelineViewComponent = forwardRef(
eventFollowStatus = {}, eventFollowStatus = {},
onToggleFollow, onToggleFollow,
borderColor, borderColor,
onControlsChange, // 控件状态变化回调
}, },
ref ref
) => { ) => {
@@ -112,21 +111,6 @@ const MainlineTimelineViewComponent = forwardRef(
} }
}, [display, filters.start_date, filters.end_date, filters.recent_days, groupBy]); }, [display, filters.start_date, filters.end_date, filters.recent_days, groupBy]);
// 初始加载 & 筛选变化时刷新
useEffect(() => {
fetchMainlineData();
}, [fetchMainlineData]);
// 暴露方法给父组件
useImperativeHandle(
ref,
() => ({
refresh: fetchMainlineData,
getScrollPosition: () => null,
}),
[fetchMainlineData]
);
// 切换分组展开/折叠 // 切换分组展开/折叠
const toggleGroup = useCallback((lv2Id) => { const toggleGroup = useCallback((lv2Id) => {
setExpandedGroups((prev) => ({ setExpandedGroups((prev) => ({
@@ -141,13 +125,53 @@ const MainlineTimelineViewComponent = forwardRef(
if (!mainlineData?.mainlines) return; if (!mainlineData?.mainlines) return;
const newState = {}; const newState = {};
mainlineData.mainlines.forEach((mainline) => { mainlineData.mainlines.forEach((mainline) => {
newState[mainline.lv2_id] = expand; const groupId = mainline.group_id || mainline.lv2_id || mainline.lv1_id || "ungrouped";
newState[groupId] = expand;
}); });
setExpandedGroups(newState); setExpandedGroups(newState);
}, },
[mainlineData] [mainlineData]
); );
// 初始加载 & 筛选变化时刷新
useEffect(() => {
fetchMainlineData();
}, [fetchMainlineData]);
// 暴露方法给父组件
useImperativeHandle(
ref,
() => ({
refresh: fetchMainlineData,
getScrollPosition: () => null,
// 暴露控件状态和方法
getControls: () => ({
groupBy,
sortBy,
hierarchyOptions,
}),
setGroupBy,
setSortBy,
toggleAll,
}),
[fetchMainlineData, groupBy, sortBy, hierarchyOptions, toggleAll]
);
// 控件状态变化时通知父组件
useEffect(() => {
if (onControlsChange) {
onControlsChange({
groupBy,
sortBy,
hierarchyOptions,
setGroupBy,
setSortBy,
toggleAll,
refresh: fetchMainlineData,
});
}
}, [groupBy, sortBy, hierarchyOptions, onControlsChange, toggleAll, fetchMainlineData]);
// 根据排序方式排序主线列表 // 根据排序方式排序主线列表
const sortedMainlines = useMemo(() => { const sortedMainlines = useMemo(() => {
const rawMainlines = mainlineData?.mainlines; const rawMainlines = mainlineData?.mainlines;
@@ -219,132 +243,30 @@ const MainlineTimelineViewComponent = forwardRef(
return ( return (
<Box display={display} w="100%" bg={COLORS.containerBg}> <Box display={display} w="100%" bg={COLORS.containerBg}>
{/* 顶部统计栏 */} {/* 顶部统计栏 - 筛选控件已移至搜索框 */}
<Flex <Flex
justify="space-between"
align="center" align="center"
px={4} px={4}
py={2} py={1.5}
bg={COLORS.statBarBg} bg={COLORS.statBarBg}
borderBottomWidth="1px" borderBottomWidth="1px"
borderBottomColor={COLORS.cardBorderColor} borderBottomColor={COLORS.cardBorderColor}
position="sticky" position="sticky"
left={0} left={0}
> >
<HStack spacing={4}> {/* 统计信息(小字) */}
<HStack spacing={2}> <HStack spacing={3} fontSize="xs" color={COLORS.secondaryTextColor}>
<Icon as={TrendingUp} color="blue.400" /> <HStack spacing={1}>
<Text fontWeight="bold" color={COLORS.textColor} fontSize="sm"> <Icon as={TrendingUp} color="blue.400" boxSize={3} />
{mainline_count} 条主线 <Text fontWeight="medium">{mainline_count} 条主线</Text>
</Text>
</HStack> </HStack>
<Text fontSize="sm" color={COLORS.secondaryTextColor}> <Text>· {total_events} 个事件</Text>
{total_events} 个事件
</Text>
{ungrouped_count > 0 && ( {ungrouped_count > 0 && (
<Badge colorScheme="orange" fontSize="xs"> <Badge colorScheme="orange" fontSize="2xs" px={1}>
{ungrouped_count} 个未归类 {ungrouped_count} 个未归类
</Badge> </Badge>
)} )}
</HStack> </HStack>
<HStack spacing={2}>
{/* 概念级别选择器 */}
<Select
value={groupBy}
onChange={setGroupBy}
size="small"
style={{ width: 180, backgroundColor: "transparent" }}
popupClassName="dark-select-dropdown"
dropdownStyle={{ backgroundColor: "#252a34", borderColor: "#3a3f4b", maxHeight: 400 }}
showSearch
optionFilterProp="label"
options={[
{
label: "分组方式",
options: [
{ value: "lv1", label: "按一级概念(大类)" },
{ value: "lv2", label: "按二级概念(细分)" },
{ value: "lv3", label: "按三级概念(更细)" },
],
},
...(hierarchyOptions.lv1?.length > 0
? [{
label: "一级概念(展开)",
options: hierarchyOptions.lv1.map((opt) => ({
value: opt.id,
label: opt.name,
})),
}]
: []),
...(hierarchyOptions.lv2?.length > 0
? [{
label: "二级概念(展开)",
options: hierarchyOptions.lv2.map((opt) => ({
value: opt.id,
label: `${opt.name}`,
})),
}]
: []),
...(hierarchyOptions.lv3?.length > 0
? [{
label: "三级概念(展开)",
options: hierarchyOptions.lv3.map((opt) => ({
value: opt.id,
label: `${opt.name}`,
})),
}]
: []),
]}
/>
{/* 排序方式选择器 */}
<Select
value={sortBy}
onChange={setSortBy}
size="small"
style={{ width: 140, backgroundColor: "transparent" }}
popupClassName="dark-select-dropdown"
dropdownStyle={{ backgroundColor: "#252a34", borderColor: "#3a3f4b" }}
options={[
{ value: "event_count", label: "按事件数量" },
{ value: "change_desc", label: "按涨幅↓" },
{ value: "change_asc", label: "按跌幅↓" },
]}
/>
<Tooltip label="全部展开">
<IconButton
icon={<ChevronDown size={16} />}
size="sm"
variant="ghost"
color={COLORS.secondaryTextColor}
onClick={() => toggleAll(true)}
aria-label="全部展开"
_hover={{ bg: COLORS.headerHoverBg }}
/>
</Tooltip>
<Tooltip label="全部折叠">
<IconButton
icon={<ChevronUp size={16} />}
size="sm"
variant="ghost"
color={COLORS.secondaryTextColor}
onClick={() => toggleAll(false)}
aria-label="全部折叠"
_hover={{ bg: COLORS.headerHoverBg }}
/>
</Tooltip>
<Tooltip label="刷新">
<IconButton
icon={<RefreshCw size={16} />}
size="sm"
variant="ghost"
color={COLORS.secondaryTextColor}
onClick={fetchMainlineData}
aria-label="刷新"
_hover={{ bg: COLORS.headerHoverBg }}
/>
</Tooltip>
</HStack>
</Flex> </Flex>
{/* 横向滚动容器 */} {/* 横向滚动容器 */}

View File

@@ -22,6 +22,8 @@ import {
SortAscendingOutlined, SortAscendingOutlined,
ReloadOutlined, ReloadOutlined,
ThunderboltOutlined, ThunderboltOutlined,
DownOutlined,
UpOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
@@ -58,6 +60,7 @@ const CompactSearchBox = ({
pageSize, pageSize,
trackingFunctions = {}, trackingFunctions = {},
isMobile = false, isMobile = false,
mainlineControls = null, // 主线模式控件状态
}) => { }) => {
// 状态 // 状态
const [stockOptions, setStockOptions] = useState([]); const [stockOptions, setStockOptions] = useState([]);
@@ -451,7 +454,7 @@ const CompactSearchBox = ({
/> />
</div> </div>
{/* 筛选控件 */} {/* 筛选控件 - 列表模式 */}
{mode !== "mainline" && ( {mode !== "mainline" && (
<> <>
{!isMobile && ( {!isMobile && (
@@ -545,6 +548,115 @@ const CompactSearchBox = ({
</Tooltip> </Tooltip>
</> </>
)} )}
{/* 筛选控件 - 题材模式 */}
{mode === "mainline" && mainlineControls && (
<>
{!isMobile && (
<Divider
type="vertical"
style={{ height: 24, margin: "0 4px", borderColor: "rgba(255,255,255,0.15)" }}
/>
)}
{/* 概念级别选择器 */}
<AntSelect
value={mainlineControls.groupBy}
onChange={mainlineControls.setGroupBy}
style={{ minWidth: isMobile ? 100 : 160 }}
popupClassName="dark-select-dropdown"
dropdownStyle={{ backgroundColor: "#252a34", borderColor: "#3a3f4b", maxHeight: 400 }}
showSearch
optionFilterProp="label"
className="bracket-select"
options={[
{
label: "分组方式",
options: [
{ value: "lv1", label: "按一级概念" },
{ value: "lv2", label: "按二级概念" },
{ value: "lv3", label: "按三级概念" },
],
},
...(mainlineControls.hierarchyOptions?.lv1?.length > 0
? [{
label: "一级概念",
options: mainlineControls.hierarchyOptions.lv1.map((opt) => ({
value: opt.id,
label: opt.name,
})),
}]
: []),
...(mainlineControls.hierarchyOptions?.lv2?.length > 0
? [{
label: "二级概念",
options: mainlineControls.hierarchyOptions.lv2.map((opt) => ({
value: opt.id,
label: opt.name,
})),
}]
: []),
...(mainlineControls.hierarchyOptions?.lv3?.length > 0
? [{
label: "三级概念",
options: mainlineControls.hierarchyOptions.lv3.map((opt) => ({
value: opt.id,
label: opt.name,
})),
}]
: []),
]}
/>
{/* 排序方式选择器 */}
<AntSelect
value={mainlineControls.sortBy}
onChange={mainlineControls.setSortBy}
style={{ minWidth: isMobile ? 80 : 120 }}
popupClassName="dark-select-dropdown"
dropdownStyle={{ backgroundColor: "#252a34", borderColor: "#3a3f4b" }}
className="bracket-select"
options={[
{ value: "event_count", label: "按事件数量" },
{ value: "change_desc", label: "按涨幅↓" },
{ value: "change_asc", label: "按跌幅↓" },
]}
/>
{/* 全部展开 */}
<Tooltip title="全部展开">
<Button
icon={<DownOutlined />}
onClick={() => mainlineControls.toggleAll(true)}
type="text"
size="small"
style={{ color: PROFESSIONAL_COLORS.text.secondary, flexShrink: 0 }}
/>
</Tooltip>
{/* 全部折叠 */}
<Tooltip title="全部折叠">
<Button
icon={<UpOutlined />}
onClick={() => mainlineControls.toggleAll(false)}
type="text"
size="small"
style={{ color: PROFESSIONAL_COLORS.text.secondary, flexShrink: 0 }}
/>
</Tooltip>
{/* 刷新 */}
<Tooltip title="刷新">
<Button
icon={<ReloadOutlined />}
onClick={mainlineControls.refresh}
type="text"
size="small"
style={{ color: PROFESSIONAL_COLORS.text.secondary, flexShrink: 0 }}
/>
</Tooltip>
</>
)}
</Flex> </Flex>
</div> </div>
); );