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:
@@ -90,6 +90,14 @@ const DynamicNewsCardComponent = forwardRef(({
|
||||
const cardBodyRef = useRef(null);
|
||||
const mainlineRef = useRef(null); // MainlineTimelineView 的 ref
|
||||
|
||||
// 主线模式控件状态(用于传递给 CompactSearchBox)
|
||||
const [mainlineControls, setMainlineControls] = useState(null);
|
||||
|
||||
// 主线模式控件变化回调
|
||||
const handleMainlineControlsChange = useCallback((controls) => {
|
||||
setMainlineControls(controls);
|
||||
}, []);
|
||||
|
||||
// 从 Redux 读取关注状态
|
||||
const eventFollowStatus = useSelector(selectEventFollowStatus);
|
||||
|
||||
@@ -747,6 +755,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
|
||||
pageSize={pageSize}
|
||||
trackingFunctions={trackingFunctions}
|
||||
isMobile={isMobile}
|
||||
mainlineControls={mainlineControls}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
@@ -808,6 +817,7 @@ const [currentMode, setCurrentMode] = useState('vertical');
|
||||
onToggleFollow={handleToggleFollow}
|
||||
onVoteChange={handleVoteChange}
|
||||
mainlineRef={mainlineRef}
|
||||
onMainlineControlsChange={handleMainlineControlsChange}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
|
||||
@@ -24,6 +24,7 @@ import VerticalModeLayout from "./layouts/VerticalModeLayout";
|
||||
* @param {Function} onToggleFollow - 关注按钮回调
|
||||
* @param {Function} onVoteChange - 投票变化回调 { eventId, voteType: 'bullish' | 'bearish' }
|
||||
* @param {React.Ref} mainlineRef - MainlineTimelineView 的 ref
|
||||
* @param {Function} onMainlineControlsChange - 主线模式控件状态变化回调
|
||||
*/
|
||||
const EventScrollList = React.memo(
|
||||
({
|
||||
@@ -43,6 +44,7 @@ const EventScrollList = React.memo(
|
||||
onToggleFollow,
|
||||
onVoteChange,
|
||||
mainlineRef,
|
||||
onMainlineControlsChange,
|
||||
}) => {
|
||||
const scrollContainerRef = useRef(null);
|
||||
|
||||
@@ -82,6 +84,7 @@ const EventScrollList = React.memo(
|
||||
eventFollowStatus={eventFollowStatus}
|
||||
onToggleFollow={onToggleFollow}
|
||||
borderColor={borderColor}
|
||||
onControlsChange={onMainlineControlsChange}
|
||||
/>
|
||||
|
||||
{/* 纵向分栏模式 */}
|
||||
|
||||
@@ -20,10 +20,8 @@ import {
|
||||
Spinner,
|
||||
Center,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@chakra-ui/react";
|
||||
import { ChevronDown, ChevronUp, RefreshCw, TrendingUp, Zap } from "lucide-react";
|
||||
import { Select } from "antd";
|
||||
import { RefreshCw, TrendingUp, Zap } from "lucide-react";
|
||||
import { getApiBase } from "@utils/apiConfig";
|
||||
import "../../SearchFilters/CompactSearchBox.css";
|
||||
|
||||
@@ -44,6 +42,7 @@ const MainlineTimelineViewComponent = forwardRef(
|
||||
eventFollowStatus = {},
|
||||
onToggleFollow,
|
||||
borderColor,
|
||||
onControlsChange, // 控件状态变化回调
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -112,21 +111,6 @@ const MainlineTimelineViewComponent = forwardRef(
|
||||
}
|
||||
}, [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) => {
|
||||
setExpandedGroups((prev) => ({
|
||||
@@ -141,13 +125,53 @@ const MainlineTimelineViewComponent = forwardRef(
|
||||
if (!mainlineData?.mainlines) return;
|
||||
const newState = {};
|
||||
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);
|
||||
},
|
||||
[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 rawMainlines = mainlineData?.mainlines;
|
||||
@@ -219,132 +243,30 @@ const MainlineTimelineViewComponent = forwardRef(
|
||||
|
||||
return (
|
||||
<Box display={display} w="100%" bg={COLORS.containerBg}>
|
||||
{/* 顶部统计栏 */}
|
||||
{/* 顶部统计栏 - 筛选控件已移至搜索框 */}
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
px={4}
|
||||
py={2}
|
||||
py={1.5}
|
||||
bg={COLORS.statBarBg}
|
||||
borderBottomWidth="1px"
|
||||
borderBottomColor={COLORS.cardBorderColor}
|
||||
position="sticky"
|
||||
left={0}
|
||||
>
|
||||
<HStack spacing={4}>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={TrendingUp} color="blue.400" />
|
||||
<Text fontWeight="bold" color={COLORS.textColor} fontSize="sm">
|
||||
{mainline_count} 条主线
|
||||
</Text>
|
||||
{/* 统计信息(小字) */}
|
||||
<HStack spacing={3} fontSize="xs" color={COLORS.secondaryTextColor}>
|
||||
<HStack spacing={1}>
|
||||
<Icon as={TrendingUp} color="blue.400" boxSize={3} />
|
||||
<Text fontWeight="medium">{mainline_count} 条主线</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" color={COLORS.secondaryTextColor}>
|
||||
共 {total_events} 个事件
|
||||
</Text>
|
||||
<Text>· {total_events} 个事件</Text>
|
||||
{ungrouped_count > 0 && (
|
||||
<Badge colorScheme="orange" fontSize="xs">
|
||||
<Badge colorScheme="orange" fontSize="2xs" px={1}>
|
||||
{ungrouped_count} 个未归类
|
||||
</Badge>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* 横向滚动容器 */}
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
SortAscendingOutlined,
|
||||
ReloadOutlined,
|
||||
ThunderboltOutlined,
|
||||
DownOutlined,
|
||||
UpOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
@@ -58,6 +60,7 @@ const CompactSearchBox = ({
|
||||
pageSize,
|
||||
trackingFunctions = {},
|
||||
isMobile = false,
|
||||
mainlineControls = null, // 主线模式控件状态
|
||||
}) => {
|
||||
// 状态
|
||||
const [stockOptions, setStockOptions] = useState([]);
|
||||
@@ -451,7 +454,7 @@ const CompactSearchBox = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 筛选控件 */}
|
||||
{/* 筛选控件 - 列表模式 */}
|
||||
{mode !== "mainline" && (
|
||||
<>
|
||||
{!isMobile && (
|
||||
@@ -545,6 +548,115 @@ const CompactSearchBox = ({
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user