fix: 修复分页、筛选和模式切换相关问题

主要修复:
1. 修复模式切换时 per_page 参数错误
   - 在 useEffect 内直接根据 mode 计算 per_page
   - 避免使用可能过时的 pageSize prop

2. 修复 DISPLAY_MODES 未定义错误
   - 在 DynamicNewsCard.js 中导入 DISPLAY_MODES 常量

3. 添加空状态显示
   - VerticalModeLayout 添加无数据时的友好提示
   - 显示图标和提示文字,引导用户调整筛选条件

4. 修复无限请求循环问题
   - 移除模式切换 useEffect 中的 filters 依赖
   - 避免筛选和模式切换 useEffect 互相触发

5. 修复筛选参数传递问题
   - usePagination 使用 useRef 存储最新 filters
   - 避免 useCallback 闭包捕获旧值
   - 修复时间筛选参数丢失问题

6. 修复分页竞态条件
   - 允许用户在加载时切换到不同页面
   - 只阻止相同页面的重复请求

涉及文件:
- src/views/Community/components/DynamicNewsCard.js
- src/views/Community/components/DynamicNewsCard/VerticalModeLayout.js
- src/views/Community/components/DynamicNewsCard/hooks/usePagination.js
- src/views/Community/hooks/useEventFilters.js
- src/store/slices/communityDataSlice.js
- src/views/Community/components/UnifiedSearchBox.js

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-11-06 17:39:03 +08:00
parent 4bb7bc04cf
commit 22b29bf727
6 changed files with 429 additions and 172 deletions

View File

@@ -22,7 +22,9 @@ const UnifiedSearchBox = ({
onSearch,
onSearchFocus,
popularKeywords = [],
filters = {}
filters = {},
mode, // 显示模式vertical, horizontal 等)
pageSize // 每页显示数量
}) => {
// 其他状态
@@ -145,7 +147,8 @@ const UnifiedSearchBox = ({
}
// ✅ 初始化行业分类(需要 industryData 加载完成)
if (filters.industry_code && industryData && industryData.length > 0) {
// ⚠️ 只在 industryValue 为空时才从 filters 初始化,避免用户选择后被覆盖
if (filters.industry_code && industryData && industryData.length > 0 && (!industryValue || industryValue.length === 0)) {
const path = findIndustryPath(filters.industry_code, industryData);
if (path) {
setIndustryValue(path);
@@ -154,6 +157,10 @@ const UnifiedSearchBox = ({
path
});
}
} else if (!filters.industry_code && industryValue && industryValue.length > 0) {
// 如果 filters 中没有行业代码,但本地有值,清空本地值
setIndustryValue([]);
logger.debug('UnifiedSearchBox', '清空行业分类filters中无值');
}
// ✅ 同步 filters.q 到输入框显示值
@@ -163,7 +170,54 @@ const UnifiedSearchBox = ({
// 如果 filters 中没有搜索关键词,清空输入框
setInputValue('');
}
}, [filters.sort, filters.importance, filters.industry_code, filters.q, industryData, findIndustryPath]);
// ✅ 初始化时间筛选(从 filters 中恢复)
// ⚠️ 只在 tradingTimeRange 为空时才从 filters 初始化,避免用户选择后被覆盖
const hasTimeInFilters = filters.start_date || filters.end_date || filters.recent_days;
if (hasTimeInFilters && (!tradingTimeRange || !tradingTimeRange.key)) {
// 根据参数推断按钮 key
let inferredKey = 'custom';
let inferredLabel = '';
if (filters.recent_days) {
// 推断是否是预设按钮
if (filters.recent_days === '7') {
inferredKey = 'week';
inferredLabel = '近一周';
} else if (filters.recent_days === '30') {
inferredKey = 'month';
inferredLabel = '近一月';
} else {
inferredLabel = `${filters.recent_days}`;
}
} else if (filters.start_date && filters.end_date) {
inferredLabel = `${dayjs(filters.start_date).format('MM-DD HH:mm')} - ${dayjs(filters.end_date).format('MM-DD HH:mm')}`;
}
// 从 filters 重建 tradingTimeRange 状态
const timeRange = {
start_date: filters.start_date || '',
end_date: filters.end_date || '',
recent_days: filters.recent_days || '',
label: inferredLabel,
key: inferredKey
};
setTradingTimeRange(timeRange);
logger.debug('UnifiedSearchBox', '初始化时间筛选', {
filters_time: {
start_date: filters.start_date,
end_date: filters.end_date,
recent_days: filters.recent_days
},
tradingTimeRange: timeRange
});
} else if (!hasTimeInFilters && tradingTimeRange) {
// 如果 filters 中没有时间参数,但本地有值,清空本地值
setTradingTimeRange(null);
logger.debug('UnifiedSearchBox', '清空时间筛选filters中无值');
}
}, [filters.sort, filters.importance, filters.industry_code, filters.q, filters.start_date, filters.end_date, filters.recent_days, industryData, findIndustryPath, industryValue, tradingTimeRange]);
// AutoComplete 搜索股票(模糊匹配 code 或 name
const handleSearch = (value) => {
@@ -242,59 +296,45 @@ const UnifiedSearchBox = ({
triggerSearch(params);
};
// ✅ 排序变化(使用防抖
// ✅ 排序变化(立即触发搜索
const handleSortChange = (value) => {
logger.debug('UnifiedSearchBox', '【1/5】排序值改变', {
logger.debug('UnifiedSearchBox', '排序值改变', {
oldValue: sort,
newValue: value
});
setSort(value);
// ⚠️ 注意:setState是异步的,此时sort仍是旧值
logger.debug('UnifiedSearchBox', '【2/5】调用buildFilterParams前的状态', {
sort: sort, // 旧值
importance: importance,
dateRange: dateRange,
industryValue: industryValue
});
// 使用防抖搜索
const params = buildFilterParams({ sort: value });
logger.debug('UnifiedSearchBox', '【3/5】buildFilterParams返回的参数', params);
// 取消之前的防抖搜索
if (debouncedSearchRef.current) {
logger.debug('UnifiedSearchBox', '【4/5】调用防抖函数(300ms延迟)');
debouncedSearchRef.current(params);
debouncedSearchRef.current.cancel();
}
// 立即触发搜索
const params = buildFilterParams({ sort: value });
logger.debug('UnifiedSearchBox', '排序改变,立即触发搜索', params);
triggerSearch(params);
};
// ✅ 行业分类变化(使用防抖
// ✅ 行业分类变化(立即触发搜索
const handleIndustryChange = (value) => {
logger.debug('UnifiedSearchBox', '【1/5】行业分类值改变', {
logger.debug('UnifiedSearchBox', '行业分类值改变', {
oldValue: industryValue,
newValue: value
});
setIndustryValue(value);
// ⚠️ 注意:setState是异步的,此时industryValue仍是旧值
logger.debug('UnifiedSearchBox', '【2/5】调用buildFilterParams前的状态', {
industryValue: industryValue, // 旧值
sort: sort,
importance: importance,
dateRange: dateRange
});
// 使用防抖搜索 (需要从新值推导参数)
const params = {
...buildFilterParams(),
industry_code: value?.[value.length - 1] || ''
};
logger.debug('UnifiedSearchBox', '【3/5】buildFilterParams返回的参数', params);
// 取消之前的防抖搜索
if (debouncedSearchRef.current) {
logger.debug('UnifiedSearchBox', '【4/5】调用防抖函数(300ms延迟)');
debouncedSearchRef.current(params);
debouncedSearchRef.current.cancel();
}
// 立即触发搜索
const params = buildFilterParams({
industry_code: value?.[value.length - 1] || ''
});
logger.debug('UnifiedSearchBox', '行业改变,立即触发搜索', params);
triggerSearch(params);
};
// ✅ 热门概念点击处理(立即搜索,不使用防抖) - 更新输入框并触发搜索
@@ -350,7 +390,7 @@ const UnifiedSearchBox = ({
setTradingTimeRange({ ...params, label, key });
// 立即触发搜索
const searchParams = buildFilterParams(params);
const searchParams = buildFilterParams({ ...params, mode });
logger.debug('UnifiedSearchBox', '交易时段筛选变化,立即触发搜索', {
timeConfig,
params: searchParams
@@ -392,7 +432,9 @@ const UnifiedSearchBox = ({
sort,
importance,
industryValue,
'filters.q': filters.q
'filters.q': filters.q,
mode,
pageSize
}
});
@@ -421,7 +463,7 @@ const UnifiedSearchBox = ({
// 基础参数overrides 优先级高于本地状态)
sort: actualSort,
importance: importanceValue,
page: 1,
// 搜索参数: 统一使用 q 参数进行搜索(话题/股票/关键词)
q: (overrides.q ?? filters.q) ?? '',
@@ -434,17 +476,30 @@ const UnifiedSearchBox = ({
recent_days: overrides.recent_days ?? (tradingTimeRange?.recent_days || ''),
// 最终 overrides 具有最高优先级
...overrides
...overrides,
page: 1,
per_page: overrides.mode === 'four-row' ? 30: 10
};
// 删除可能来自 overrides 的旧 per_page 值(将由 pageSize 重新设置)
delete result.per_page;
// 添加 return_type 参数(如果需要)
if (returnType) {
result.return_type = returnType;
}
// 添加 mode 和 per_page 参数(如果提供了的话)
if (mode !== undefined && mode !== null) {
result.mode = mode;
}
if (pageSize !== undefined && pageSize !== null) {
result.per_page = pageSize; // 后端实际使用的参数
}
logger.debug('UnifiedSearchBox', '🔧 buildFilterParams - 输出结果', result);
return result;
}, [sort, importance, filters.q, industryValue, tradingTimeRange]);
}, [sort, importance, filters.q, industryValue, tradingTimeRange, mode, pageSize]);
// ✅ 重置筛选 - 清空所有筛选器并触发搜索
const handleReset = () => {
@@ -578,12 +633,12 @@ const UnifiedSearchBox = ({
};
return (
<Card>
<div style={{padding: '8px'}}>
{/* 第三行:行业 + 重要性 + 排序 */}
<Space style={{ width: '100%', justifyContent: 'space-between' }} size="middle">
{/* 左侧:筛选器组 */}
<Space size="middle" wrap>
<span style={{ fontSize: 14, color: '#666', fontWeight: 'bold' }}>筛选:</span>
<Space size="small" wrap>
<span style={{ fontSize: 12, color: '#666', fontWeight: 'bold' }}>筛选:</span>
{/* 行业分类 */}
<Cascader
value={industryValue}
@@ -602,19 +657,19 @@ const UnifiedSearchBox = ({
expandTrigger="hover"
displayRender={(labels) => labels.join(' > ')}
disabled={industryLoading}
style={{ width: 200 }}
size="middle"
style={{ width: 160 }}
size="small"
/>
{/* 重要性 */}
<Space size="small">
<span style={{ fontSize: 14, color: '#666' }}>重要性:</span>
<span style={{ fontSize: 12, color: '#666' }}>重要性:</span>
<AntSelect
mode="multiple"
value={importance}
onChange={handleImportanceChange}
style={{ width: 150 }}
size="middle"
style={{ width: 120 }}
size="small"
placeholder="全部"
maxTagCount={3}
>
@@ -626,27 +681,27 @@ const UnifiedSearchBox = ({
</Space>
{/* 搜索图标(可点击) + 搜索框 */}
<Space.Compact style={{ flex: 1, minWidth: 300 }}>
<Space.Compact style={{ flex: 1, minWidth: 250 }}>
<SearchOutlined
onClick={handleMainSearch}
style={{
fontSize: 18,
padding: '8px 12px',
background: '#f5f5f5',
fontSize: 14,
padding: '5px 8px',
background: '#e6f7ff',
borderRadius: '6px 0 0 6px',
display: 'flex',
alignItems: 'center',
color: '#666',
color: '#1890ff',
cursor: 'pointer',
transition: 'all 0.3s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#1890ff';
e.currentTarget.style.background = '#e6f7ff';
e.currentTarget.style.color = '#096dd9';
e.currentTarget.style.background = '#bae7ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#666';
e.currentTarget.style.background = '#f5f5f5';
e.currentTarget.style.color = '#1890ff';
e.currentTarget.style.background = '#e6f7ff';
}}
/>
<AutoComplete
@@ -663,7 +718,7 @@ const UnifiedSearchBox = ({
}
}}
style={{ flex: 1 }}
size="middle"
size="small"
notFoundContent={inputValue && stockOptions.length === 0 ? "未找到匹配的股票" : null}
/>
</Space.Compact>
@@ -672,14 +727,14 @@ const UnifiedSearchBox = ({
<Button
icon={<CloseCircleOutlined />}
onClick={handleReset}
size="middle"
size="small"
style={{
borderRadius: 6,
border: '1px solid #d9d9d9',
backgroundColor: '#fff',
color: '#666',
fontWeight: 500,
padding: '4px 12px',
padding: '4px 10px',
display: 'flex',
alignItems: 'center',
gap: 4,
@@ -707,12 +762,12 @@ const UnifiedSearchBox = ({
{/* 右侧:排序 */}
<Space size="small">
<span style={{ fontSize: 14, color: '#666' }}>排序:</span>
<span style={{ fontSize: 12, color: '#666' }}>排序:</span>
<AntSelect
value={sort}
onChange={handleSortChange}
style={{ width: 120 }}
size="middle"
style={{ width: 100 }}
size="small"
>
<Option value="new">最新</Option>
<Option value="hot">最热</Option>
@@ -724,7 +779,7 @@ const UnifiedSearchBox = ({
</Space>
{/* 第一行:筛选 + 时间按钮 + 搜索图标 + 搜索框 */}
<Space wrap style={{ width: '100%', marginBottom: 12, marginTop: 8 }} size="middle">
<Space wrap style={{ width: '100%', marginBottom: 4, marginTop: 6 }} size="middle">
<span style={{ fontSize: 14, color: '#666', fontWeight: 'bold' }}>时间筛选:</span>
{/* 交易时段筛选 */}
@@ -735,29 +790,13 @@ const UnifiedSearchBox = ({
</Space>
{/* 第二行:热门概念 */}
<div style={{ marginTop: 8 }}>
<div style={{ marginTop: 2 }}>
<PopularKeywords
keywords={popularKeywords}
onKeywordClick={handleKeywordClick}
/>
</div>
{/* 已选条件标签 */}
{filterTags.length > 0 && (
<Space size={[8, 8]} wrap style={{ marginTop: 12 }}>
{filterTags.map(tag => (
<Tag
key={tag.key}
closable
onClose={() => handleRemoveTag(tag.key)}
color="blue"
>
{tag.label}
</Tag>
))}
</Space>
)}
</Card>
</div>
);
};