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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user