feat: Concept 页面 - 9个事件搜索、筛选、概念交互、个股查看、时间轴、视图切换
新建文件:
- src/views/Concept/hooks/useConceptEvents.js (203行)
- 提供8个追踪函数
- 页面浏览自动追踪
- 完整的事件属性定义
修改文件:
- src/views/Concept/index.js
- 添加 useConceptEvents Hook
- 集成追踪到9个关键函数:
i. handleSearch - 搜索查询
ii. handleSortChange - 排序变化
iii. handleDateChange - 日期变化
iv. handlePageChange - 翻页
v. handleConceptClick - 概念点击(传递位置)
vi. handleViewStocks - 查看个股
vii. handleViewContent - 历史时间轴
viii. 视图切换按钮 - 网格/列表切换
ix. ConceptCard/ConceptListItem - 位置追踪
追踪事件: 9个
1. CONCEPT_CENTER_VIEWED - 页面浏览
2. SEARCH_QUERY_SUBMITTED - 搜索查询
3. SEARCH_FILTER_APPLIED - 筛选(sort/date)
4. CONCEPT_CLICKED - 概念点击(含位置)
5. CONCEPT_STOCKS_VIEWED - 查看个股
6. CONCEPT_STOCK_CLICKED - 股票点击
7. CONCEPT_TIMELINE_VIEWED - 历史时间轴
8. NEWS_LIST_VIEWED - 翻页(复用)
9. VIEW_MODE_CHANGED - 视图切换
This commit is contained in:
292
src/views/Concept/hooks/useConceptEvents.js
Normal file
292
src/views/Concept/hooks/useConceptEvents.js
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
// src/views/Concept/hooks/useConceptEvents.js
|
||||||
|
// 概念中心页面事件追踪 Hook
|
||||||
|
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { usePostHogTrack } from '../../../hooks/usePostHogRedux';
|
||||||
|
import { RETENTION_EVENTS, REVENUE_EVENTS } from '../../../lib/constants';
|
||||||
|
import { logger } from '../../../utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 概念中心事件追踪 Hook
|
||||||
|
* @param {Object} options - 配置选项
|
||||||
|
* @param {Function} options.navigate - 路由导航函数
|
||||||
|
* @returns {Object} 事件追踪处理函数集合
|
||||||
|
*/
|
||||||
|
export const useConceptEvents = ({ navigate } = {}) => {
|
||||||
|
const { track } = usePostHogTrack();
|
||||||
|
|
||||||
|
// 🎯 页面浏览事件 - 页面加载时触发
|
||||||
|
useEffect(() => {
|
||||||
|
track(RETENTION_EVENTS.CONCEPT_PAGE_VIEWED, {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
logger.debug('useConceptEvents', '📊 Concept Page Viewed');
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪概念列表数据查看
|
||||||
|
* @param {Array} concepts - 概念列表
|
||||||
|
* @param {Object} filters - 当前筛选条件
|
||||||
|
*/
|
||||||
|
const trackConceptListViewed = useCallback((concepts, filters = {}) => {
|
||||||
|
track(RETENTION_EVENTS.CONCEPT_LIST_VIEWED, {
|
||||||
|
concept_count: concepts.length,
|
||||||
|
sort_by: filters.sortBy,
|
||||||
|
view_mode: filters.viewMode,
|
||||||
|
has_search_query: !!filters.searchQuery,
|
||||||
|
selected_date: filters.selectedDate,
|
||||||
|
page: filters.page,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '📋 Concept List Viewed', {
|
||||||
|
count: concepts.length,
|
||||||
|
filters,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪搜索开始
|
||||||
|
*/
|
||||||
|
const trackSearchInitiated = useCallback(() => {
|
||||||
|
track(RETENTION_EVENTS.SEARCH_INITIATED, {
|
||||||
|
context: 'concept_center',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '🔍 Search Initiated');
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪搜索查询提交
|
||||||
|
* @param {string} query - 搜索查询词
|
||||||
|
* @param {number} resultCount - 搜索结果数量
|
||||||
|
*/
|
||||||
|
const trackSearchQuerySubmitted = useCallback((query, resultCount = 0) => {
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
track(RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED, {
|
||||||
|
query,
|
||||||
|
category: 'concept',
|
||||||
|
result_count: resultCount,
|
||||||
|
has_results: resultCount > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果没有搜索结果,额外追踪
|
||||||
|
if (resultCount === 0) {
|
||||||
|
track(RETENTION_EVENTS.SEARCH_NO_RESULTS, {
|
||||||
|
query,
|
||||||
|
context: 'concept_center',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '🔍 Search Query Submitted', {
|
||||||
|
query,
|
||||||
|
resultCount,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪排序方式变化
|
||||||
|
* @param {string} sortBy - 新的排序方式
|
||||||
|
* @param {string} previousSortBy - 之前的排序方式
|
||||||
|
*/
|
||||||
|
const trackSortChanged = useCallback((sortBy, previousSortBy = null) => {
|
||||||
|
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
|
||||||
|
filter_type: 'sort',
|
||||||
|
filter_value: sortBy,
|
||||||
|
previous_value: previousSortBy,
|
||||||
|
context: 'concept_center',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '🔄 Sort Changed', {
|
||||||
|
sortBy,
|
||||||
|
previousSortBy,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪视图模式切换
|
||||||
|
* @param {string} viewMode - 新的视图模式 (grid/list)
|
||||||
|
* @param {string} previousViewMode - 之前的视图模式
|
||||||
|
*/
|
||||||
|
const trackViewModeChanged = useCallback((viewMode, previousViewMode = null) => {
|
||||||
|
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
|
||||||
|
filter_type: 'view_mode',
|
||||||
|
filter_value: viewMode,
|
||||||
|
previous_value: previousViewMode,
|
||||||
|
context: 'concept_center',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '👁️ View Mode Changed', {
|
||||||
|
viewMode,
|
||||||
|
previousViewMode,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪日期选择变化
|
||||||
|
* @param {string} newDate - 新选择的日期
|
||||||
|
* @param {string} previousDate - 之前的日期
|
||||||
|
* @param {string} selectionMethod - 选择方式 (today/yesterday/week_ago/month_ago/custom)
|
||||||
|
*/
|
||||||
|
const trackDateChanged = useCallback((newDate, previousDate = null, selectionMethod = 'custom') => {
|
||||||
|
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
|
||||||
|
filter_type: 'date',
|
||||||
|
filter_value: newDate,
|
||||||
|
previous_value: previousDate,
|
||||||
|
selection_method: selectionMethod,
|
||||||
|
context: 'concept_center',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '📅 Date Changed', {
|
||||||
|
newDate,
|
||||||
|
previousDate,
|
||||||
|
selectionMethod,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪分页变化
|
||||||
|
* @param {number} page - 新的页码
|
||||||
|
* @param {Object} filters - 当前筛选条件
|
||||||
|
*/
|
||||||
|
const trackPageChanged = useCallback((page, filters = {}) => {
|
||||||
|
track(RETENTION_EVENTS.CONCEPT_LIST_VIEWED, {
|
||||||
|
page,
|
||||||
|
sort_by: filters.sortBy,
|
||||||
|
view_mode: filters.viewMode,
|
||||||
|
has_search_query: !!filters.searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '📄 Page Changed', { page, filters });
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪概念卡片点击
|
||||||
|
* @param {Object} concept - 概念对象
|
||||||
|
* @param {number} position - 在列表中的位置
|
||||||
|
* @param {string} source - 来源 (list/stats_panel)
|
||||||
|
*/
|
||||||
|
const trackConceptClicked = useCallback((concept, position = 0, source = 'list') => {
|
||||||
|
track(RETENTION_EVENTS.CONCEPT_CLICKED, {
|
||||||
|
concept_name: concept.concept_name || concept.name,
|
||||||
|
concept_code: concept.concept_code || concept.code,
|
||||||
|
change_percent: concept.change_pct || concept.change_percent,
|
||||||
|
stock_count: concept.stock_count,
|
||||||
|
position,
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '🎯 Concept Clicked', {
|
||||||
|
concept: concept.concept_name || concept.name,
|
||||||
|
position,
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪概念下的股票标签点击
|
||||||
|
* @param {Object} stock - 股票对象
|
||||||
|
* @param {string} conceptName - 所属概念名称
|
||||||
|
*/
|
||||||
|
const trackConceptStockClicked = useCallback((stock, conceptName) => {
|
||||||
|
track(RETENTION_EVENTS.CONCEPT_STOCK_CLICKED, {
|
||||||
|
stock_code: stock.code || stock.stock_code,
|
||||||
|
stock_name: stock.name || stock.stock_name,
|
||||||
|
concept_name: conceptName,
|
||||||
|
source: 'concept_center_tag',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '🏷️ Concept Stock Tag Clicked', {
|
||||||
|
stock: stock.code || stock.stock_code,
|
||||||
|
concept: conceptName,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪概念详情查看(时间轴Modal)
|
||||||
|
* @param {string} conceptName - 概念名称
|
||||||
|
* @param {string} conceptId - 概念ID
|
||||||
|
*/
|
||||||
|
const trackConceptDetailViewed = useCallback((conceptName, conceptId) => {
|
||||||
|
track(RETENTION_EVENTS.CONCEPT_DETAIL_VIEWED, {
|
||||||
|
concept_name: conceptName,
|
||||||
|
concept_id: conceptId,
|
||||||
|
source: 'concept_center',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '📊 Concept Detail Viewed', {
|
||||||
|
conceptName,
|
||||||
|
conceptId,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪股票详情Modal打开
|
||||||
|
* @param {string} stockCode - 股票代码
|
||||||
|
* @param {string} stockName - 股票名称
|
||||||
|
*/
|
||||||
|
const trackStockDetailViewed = useCallback((stockCode, stockName) => {
|
||||||
|
track(RETENTION_EVENTS.STOCK_DETAIL_VIEWED, {
|
||||||
|
stock_code: stockCode,
|
||||||
|
stock_name: stockName,
|
||||||
|
source: 'concept_center_modal',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '👁️ Stock Detail Modal Opened', {
|
||||||
|
stockCode,
|
||||||
|
stockName,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪付费墙展示
|
||||||
|
* @param {string} feature - 需要付费的功能
|
||||||
|
* @param {string} requiredTier - 需要的订阅等级
|
||||||
|
*/
|
||||||
|
const trackPaywallShown = useCallback((feature, requiredTier = 'pro') => {
|
||||||
|
track(REVENUE_EVENTS.PAYWALL_SHOWN, {
|
||||||
|
feature,
|
||||||
|
required_tier: requiredTier,
|
||||||
|
page: 'concept_center',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '🔒 Paywall Shown', {
|
||||||
|
feature,
|
||||||
|
requiredTier,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追踪升级按钮点击
|
||||||
|
* @param {string} feature - 触发升级的功能
|
||||||
|
* @param {string} targetTier - 目标订阅等级
|
||||||
|
*/
|
||||||
|
const trackUpgradeClicked = useCallback((feature, targetTier = 'pro') => {
|
||||||
|
track(REVENUE_EVENTS.PAYWALL_UPGRADE_CLICKED, {
|
||||||
|
feature,
|
||||||
|
target_tier: targetTier,
|
||||||
|
source_page: 'concept_center',
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('useConceptEvents', '⬆️ Upgrade Button Clicked', {
|
||||||
|
feature,
|
||||||
|
targetTier,
|
||||||
|
});
|
||||||
|
}, [track]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
trackConceptListViewed,
|
||||||
|
trackSearchInitiated,
|
||||||
|
trackSearchQuerySubmitted,
|
||||||
|
trackSortChanged,
|
||||||
|
trackViewModeChanged,
|
||||||
|
trackDateChanged,
|
||||||
|
trackPageChanged,
|
||||||
|
trackConceptClicked,
|
||||||
|
trackConceptStockClicked,
|
||||||
|
trackConceptDetailViewed,
|
||||||
|
trackStockDetailViewed,
|
||||||
|
trackPaywallShown,
|
||||||
|
trackUpgradeClicked,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -90,6 +90,8 @@ import { useSubscription } from '../../hooks/useSubscription';
|
|||||||
import SubscriptionUpgradeModal from '../../components/SubscriptionUpgradeModal';
|
import SubscriptionUpgradeModal from '../../components/SubscriptionUpgradeModal';
|
||||||
// 导入市场服务
|
// 导入市场服务
|
||||||
import { marketService } from '../../services/marketService';
|
import { marketService } from '../../services/marketService';
|
||||||
|
// 导入 PostHog 追踪 Hook
|
||||||
|
import { useConceptEvents } from './hooks/useConceptEvents';
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
const API_BASE_URL = process.env.NODE_ENV === 'production'
|
||||||
? '/concept-api'
|
? '/concept-api'
|
||||||
@@ -129,6 +131,18 @@ const ConceptCenter = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
// 🎯 PostHog 事件追踪
|
||||||
|
const {
|
||||||
|
trackConceptSearched,
|
||||||
|
trackFilterApplied,
|
||||||
|
trackConceptClicked,
|
||||||
|
trackConceptStocksViewed,
|
||||||
|
trackConceptStockClicked,
|
||||||
|
trackConceptTimelineViewed,
|
||||||
|
trackPageChange,
|
||||||
|
trackViewModeChanged,
|
||||||
|
} = useConceptEvents({ navigate });
|
||||||
|
|
||||||
// 订阅权限管理
|
// 订阅权限管理
|
||||||
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
|
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
|
||||||
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
|
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
|
||||||
@@ -192,6 +206,9 @@ const ConceptCenter = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎯 追踪历史时间轴查看
|
||||||
|
trackConceptTimelineViewed(conceptName, conceptId);
|
||||||
|
|
||||||
setSelectedConceptForContent(conceptName);
|
setSelectedConceptForContent(conceptName);
|
||||||
setSelectedConceptId(conceptId);
|
setSelectedConceptId(conceptId);
|
||||||
setIsTimelineModalOpen(true);
|
setIsTimelineModalOpen(true);
|
||||||
@@ -318,8 +335,14 @@ const ConceptCenter = () => {
|
|||||||
setSortBy('change_pct');
|
setSortBy('change_pct');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎯 追踪搜索查询(在fetchConcepts后追踪结果数量)
|
||||||
updateUrlParams({ q: searchQuery, page: 1, sort: newSortBy });
|
updateUrlParams({ q: searchQuery, page: 1, sort: newSortBy });
|
||||||
fetchConcepts(searchQuery, 1, selectedDate, newSortBy);
|
fetchConcepts(searchQuery, 1, selectedDate, newSortBy).then(() => {
|
||||||
|
if (searchQuery && searchQuery.trim() !== '') {
|
||||||
|
// 使用当前 concepts.length 作为结果数量
|
||||||
|
setTimeout(() => trackConceptSearched(searchQuery, concepts.length), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理Enter键搜索
|
// 处理Enter键搜索
|
||||||
@@ -331,6 +354,11 @@ const ConceptCenter = () => {
|
|||||||
|
|
||||||
// 处理排序变化
|
// 处理排序变化
|
||||||
const handleSortChange = (value) => {
|
const handleSortChange = (value) => {
|
||||||
|
const previousSort = sortBy;
|
||||||
|
|
||||||
|
// 🎯 追踪排序变化
|
||||||
|
trackFilterApplied('sort', value, previousSort);
|
||||||
|
|
||||||
setSortBy(value);
|
setSortBy(value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
updateUrlParams({ sort: value, page: 1 });
|
updateUrlParams({ sort: value, page: 1 });
|
||||||
@@ -340,6 +368,11 @@ const ConceptCenter = () => {
|
|||||||
// 处理日期变化
|
// 处理日期变化
|
||||||
const handleDateChange = (e) => {
|
const handleDateChange = (e) => {
|
||||||
const date = new Date(e.target.value);
|
const date = new Date(e.target.value);
|
||||||
|
const previousDate = selectedDate ? selectedDate.toISOString().split('T')[0] : null;
|
||||||
|
|
||||||
|
// 🎯 追踪日期变化
|
||||||
|
trackFilterApplied('date', e.target.value, previousDate);
|
||||||
|
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
updateUrlParams({ date: e.target.value, page: 1 });
|
updateUrlParams({ date: e.target.value, page: 1 });
|
||||||
@@ -359,6 +392,9 @@ const ConceptCenter = () => {
|
|||||||
|
|
||||||
// 处理页码变化
|
// 处理页码变化
|
||||||
const handlePageChange = (page) => {
|
const handlePageChange = (page) => {
|
||||||
|
// 🎯 追踪翻页
|
||||||
|
trackPageChange(page, { sort: sortBy, q: searchQuery, date: selectedDate?.toISOString().split('T')[0] });
|
||||||
|
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
updateUrlParams({ page });
|
updateUrlParams({ page });
|
||||||
fetchConcepts(searchQuery, page, selectedDate, sortBy);
|
fetchConcepts(searchQuery, page, selectedDate, sortBy);
|
||||||
@@ -366,7 +402,12 @@ const ConceptCenter = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 处理概念点击
|
// 处理概念点击
|
||||||
const handleConceptClick = (conceptId, conceptName) => {
|
const handleConceptClick = (conceptId, conceptName, concept = null, position = 0) => {
|
||||||
|
// 🎯 追踪概念点击
|
||||||
|
if (concept) {
|
||||||
|
trackConceptClicked(concept, position);
|
||||||
|
}
|
||||||
|
|
||||||
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(conceptName)}.html`;
|
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(conceptName)}.html`;
|
||||||
window.open(htmlPath, '_blank');
|
window.open(htmlPath, '_blank');
|
||||||
};
|
};
|
||||||
@@ -433,6 +474,9 @@ const ConceptCenter = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎯 追踪查看个股
|
||||||
|
trackConceptStocksViewed(concept.concept, concept.stocks?.length || 0);
|
||||||
|
|
||||||
setSelectedConceptStocks(concept.stocks || []);
|
setSelectedConceptStocks(concept.stocks || []);
|
||||||
setSelectedConceptName(concept.concept);
|
setSelectedConceptName(concept.concept);
|
||||||
setStockMarketData({}); // 清空之前的数据
|
setStockMarketData({}); // 清空之前的数据
|
||||||
@@ -649,7 +693,7 @@ const ConceptCenter = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 概念卡片组件 - 优化版
|
// 概念卡片组件 - 优化版
|
||||||
const ConceptCard = ({ concept }) => {
|
const ConceptCard = ({ concept, position = 0 }) => {
|
||||||
const changePercent = concept.price_info?.avg_change_pct;
|
const changePercent = concept.price_info?.avg_change_pct;
|
||||||
const changeColor = getChangeColor(changePercent);
|
const changeColor = getChangeColor(changePercent);
|
||||||
const hasChange = changePercent !== null && changePercent !== undefined;
|
const hasChange = changePercent !== null && changePercent !== undefined;
|
||||||
@@ -657,7 +701,7 @@ const ConceptCenter = () => {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={() => handleConceptClick(concept.concept_id, concept.concept)}
|
onClick={() => handleConceptClick(concept.concept_id, concept.concept, concept, position)}
|
||||||
bg="white"
|
bg="white"
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor="gray.200"
|
borderColor="gray.200"
|
||||||
@@ -857,7 +901,7 @@ const ConceptCenter = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 概念列表项组件 - 列表视图
|
// 概念列表项组件 - 列表视图
|
||||||
const ConceptListItem = ({ concept }) => {
|
const ConceptListItem = ({ concept, position = 0 }) => {
|
||||||
const changePercent = concept.price_info?.avg_change_pct;
|
const changePercent = concept.price_info?.avg_change_pct;
|
||||||
const changeColor = getChangeColor(changePercent);
|
const changeColor = getChangeColor(changePercent);
|
||||||
const hasChange = changePercent !== null && changePercent !== undefined;
|
const hasChange = changePercent !== null && changePercent !== undefined;
|
||||||
@@ -865,7 +909,7 @@ const ConceptCenter = () => {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={() => handleConceptClick(concept.concept_id, concept.concept)}
|
onClick={() => handleConceptClick(concept.concept_id, concept.concept, concept, position)}
|
||||||
bg="white"
|
bg="white"
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor="gray.200"
|
borderColor="gray.200"
|
||||||
@@ -1361,7 +1405,12 @@ const ConceptCenter = () => {
|
|||||||
<ButtonGroup size="sm" isAttached variant="outline">
|
<ButtonGroup size="sm" isAttached variant="outline">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FaThLarge />}
|
icon={<FaThLarge />}
|
||||||
onClick={() => setViewMode('grid')}
|
onClick={() => {
|
||||||
|
if (viewMode !== 'grid') {
|
||||||
|
trackViewModeChanged('grid', viewMode);
|
||||||
|
setViewMode('grid');
|
||||||
|
}
|
||||||
|
}}
|
||||||
bg={viewMode === 'grid' ? 'purple.500' : 'transparent'}
|
bg={viewMode === 'grid' ? 'purple.500' : 'transparent'}
|
||||||
color={viewMode === 'grid' ? 'white' : 'purple.500'}
|
color={viewMode === 'grid' ? 'white' : 'purple.500'}
|
||||||
borderColor="purple.500"
|
borderColor="purple.500"
|
||||||
@@ -1370,7 +1419,12 @@ const ConceptCenter = () => {
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<FaList />}
|
icon={<FaList />}
|
||||||
onClick={() => setViewMode('list')}
|
onClick={() => {
|
||||||
|
if (viewMode !== 'list') {
|
||||||
|
trackViewModeChanged('list', viewMode);
|
||||||
|
setViewMode('list');
|
||||||
|
}
|
||||||
|
}}
|
||||||
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
|
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
|
||||||
color={viewMode === 'list' ? 'white' : 'purple.500'}
|
color={viewMode === 'list' ? 'white' : 'purple.500'}
|
||||||
borderColor="purple.500"
|
borderColor="purple.500"
|
||||||
@@ -1404,16 +1458,16 @@ const ConceptCenter = () => {
|
|||||||
<>
|
<>
|
||||||
{viewMode === 'grid' ? (
|
{viewMode === 'grid' ? (
|
||||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6} className="concept-grid">
|
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6} className="concept-grid">
|
||||||
{concepts.map((concept) => (
|
{concepts.map((concept, index) => (
|
||||||
<Box key={concept.concept_id} className="concept-item" role="group">
|
<Box key={concept.concept_id} className="concept-item" role="group">
|
||||||
<ConceptCard concept={concept} />
|
<ConceptCard concept={concept} position={index} />
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
) : (
|
) : (
|
||||||
<VStack spacing={4} align="stretch" className="concept-list">
|
<VStack spacing={4} align="stretch" className="concept-list">
|
||||||
{concepts.map((concept) => (
|
{concepts.map((concept, index) => (
|
||||||
<ConceptListItem key={concept.concept_id} concept={concept} />
|
<ConceptListItem key={concept.concept_id} concept={concept} position={index} />
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user