feat: 事件详情权限加上权限校验
This commit is contained in:
97
src/components/SubscriptionBadge.js
Normal file
97
src/components/SubscriptionBadge.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// src/components/SubscriptionBadge.js
|
||||||
|
// 会员专享标签组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
Text,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { FaStar, FaCrown } from 'react-icons/fa';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员专享标签组件
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {'pro' | 'max'} props.tier - 会员等级:pro 或 max
|
||||||
|
* @param {'sm' | 'md'} props.size - 标签尺寸
|
||||||
|
*/
|
||||||
|
const SubscriptionBadge = ({ tier = 'pro', size = 'sm' }) => {
|
||||||
|
// 🔍 调试:SubscriptionBadge 被渲染(强制输出)
|
||||||
|
console.error('🔴 [DEBUG] SubscriptionBadge 组件被渲染 - 这不应该出现(max 会员)', {
|
||||||
|
tier,
|
||||||
|
size,
|
||||||
|
调用位置: new Error().stack
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔍 调试:SubscriptionBadge 被渲染(logger)
|
||||||
|
logger.debug('SubscriptionBadge', '组件被渲染', {
|
||||||
|
tier,
|
||||||
|
size,
|
||||||
|
调用栈: new Error().stack
|
||||||
|
});
|
||||||
|
|
||||||
|
// PRO 和 MAX 配置
|
||||||
|
const config = {
|
||||||
|
pro: {
|
||||||
|
label: 'PRO专享',
|
||||||
|
icon: FaStar,
|
||||||
|
bgGradient: 'linear(to-r, blue.400, purple.500)',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
label: 'MAX专享',
|
||||||
|
icon: FaCrown,
|
||||||
|
bgGradient: 'linear(to-r, pink.400, red.500)',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tierConfig = config[tier] || config.pro;
|
||||||
|
|
||||||
|
// 尺寸配置
|
||||||
|
const sizeConfig = {
|
||||||
|
sm: {
|
||||||
|
fontSize: 'xs',
|
||||||
|
iconSize: 2.5,
|
||||||
|
px: 2,
|
||||||
|
py: 0.5,
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
fontSize: 'sm',
|
||||||
|
iconSize: 3,
|
||||||
|
px: 3,
|
||||||
|
py: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentSize = sizeConfig[size] || sizeConfig.sm;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
bgGradient={tierConfig.bgGradient}
|
||||||
|
color={tierConfig.color}
|
||||||
|
borderRadius="full"
|
||||||
|
px={currentSize.px}
|
||||||
|
py={currentSize.py}
|
||||||
|
fontSize={currentSize.fontSize}
|
||||||
|
fontWeight="bold"
|
||||||
|
boxShadow="sm"
|
||||||
|
display="inline-flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap={1}
|
||||||
|
transition="all 0.2s"
|
||||||
|
_hover={{
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
boxShadow: 'md',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon as={tierConfig.icon} boxSize={currentSize.iconSize} />
|
||||||
|
<Text>{tierConfig.label}</Text>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubscriptionBadge;
|
||||||
@@ -16,21 +16,43 @@ import CollapsibleHeader from './CollapsibleHeader';
|
|||||||
* @param {boolean} props.isOpen - 是否展开
|
* @param {boolean} props.isOpen - 是否展开
|
||||||
* @param {Function} props.onToggle - 切换展开/收起的回调
|
* @param {Function} props.onToggle - 切换展开/收起的回调
|
||||||
* @param {number} props.count - 可选的数量徽章
|
* @param {number} props.count - 可选的数量徽章
|
||||||
|
* @param {React.ReactNode} props.subscriptionBadge - 可选的会员标签组件
|
||||||
|
* @param {boolean} props.isLocked - 是否锁定(不可展开)
|
||||||
|
* @param {Function} props.onLockedClick - 锁定时点击的回调
|
||||||
* @param {React.ReactNode} props.children - 子内容
|
* @param {React.ReactNode} props.children - 子内容
|
||||||
*/
|
*/
|
||||||
const CollapsibleSection = ({ title, isOpen, onToggle, count = null, children }) => {
|
const CollapsibleSection = ({
|
||||||
|
title,
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
|
count = null,
|
||||||
|
subscriptionBadge = null,
|
||||||
|
isLocked = false,
|
||||||
|
onLockedClick = null,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
const sectionBg = useColorModeValue('gray.50', 'gray.750');
|
const sectionBg = useColorModeValue('gray.50', 'gray.750');
|
||||||
|
|
||||||
|
// 处理点击:如果锁定则触发锁定回调,否则触发正常切换
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (isLocked && onLockedClick) {
|
||||||
|
onLockedClick();
|
||||||
|
} else if (!isLocked) {
|
||||||
|
onToggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<CollapsibleHeader
|
<CollapsibleHeader
|
||||||
title={title}
|
title={title}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onToggle={onToggle}
|
onToggle={handleToggle}
|
||||||
count={count}
|
count={count}
|
||||||
|
subscriptionBadge={subscriptionBadge}
|
||||||
/>
|
/>
|
||||||
<Collapse
|
<Collapse
|
||||||
in={isOpen}
|
in={isOpen && !isLocked}
|
||||||
animateOpacity
|
animateOpacity
|
||||||
unmountOnExit={false}
|
unmountOnExit={false}
|
||||||
startingHeight={0}
|
startingHeight={0}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { getImportanceConfig } from '../../../../constants/importanceLevels';
|
|||||||
import { eventService } from '../../../../services/eventService';
|
import { eventService } from '../../../../services/eventService';
|
||||||
import { useEventStocks } from '../StockDetailPanel/hooks/useEventStocks';
|
import { useEventStocks } from '../StockDetailPanel/hooks/useEventStocks';
|
||||||
import { toggleEventFollow, selectEventFollowStatus } from '../../../../store/slices/communityDataSlice';
|
import { toggleEventFollow, selectEventFollowStatus } from '../../../../store/slices/communityDataSlice';
|
||||||
|
import { useAuth } from '../../../../contexts/AuthContext';
|
||||||
import EventHeaderInfo from './EventHeaderInfo';
|
import EventHeaderInfo from './EventHeaderInfo';
|
||||||
import EventDescriptionSection from './EventDescriptionSection';
|
import EventDescriptionSection from './EventDescriptionSection';
|
||||||
import RelatedConceptsSection from './RelatedConceptsSection';
|
import RelatedConceptsSection from './RelatedConceptsSection';
|
||||||
@@ -24,6 +25,8 @@ import RelatedStocksSection from './RelatedStocksSection';
|
|||||||
import CollapsibleSection from './CollapsibleSection';
|
import CollapsibleSection from './CollapsibleSection';
|
||||||
import HistoricalEvents from '../../../EventDetail/components/HistoricalEvents';
|
import HistoricalEvents from '../../../EventDetail/components/HistoricalEvents';
|
||||||
import TransmissionChainAnalysis from '../../../EventDetail/components/TransmissionChainAnalysis';
|
import TransmissionChainAnalysis from '../../../EventDetail/components/TransmissionChainAnalysis';
|
||||||
|
import SubscriptionBadge from '../../../../components/SubscriptionBadge';
|
||||||
|
import SubscriptionUpgradeModal from '../../../../components/SubscriptionUpgradeModal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态新闻详情面板主组件
|
* 动态新闻详情面板主组件
|
||||||
@@ -32,16 +35,34 @@ import TransmissionChainAnalysis from '../../../EventDetail/components/Transmiss
|
|||||||
*/
|
*/
|
||||||
const DynamicNewsDetailPanel = ({ event }) => {
|
const DynamicNewsDetailPanel = ({ event }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { user } = useAuth();
|
||||||
const cardBg = useColorModeValue('white', 'gray.800');
|
const cardBg = useColorModeValue('white', 'gray.800');
|
||||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||||
const textColor = useColorModeValue('gray.600', 'gray.400');
|
const textColor = useColorModeValue('gray.600', 'gray.400');
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
// 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type)
|
||||||
|
const userTier = user?.subscription_type || 'free';
|
||||||
|
|
||||||
// 从 Redux 读取关注状态
|
// 从 Redux 读取关注状态
|
||||||
const eventFollowStatus = useSelector(selectEventFollowStatus);
|
const eventFollowStatus = useSelector(selectEventFollowStatus);
|
||||||
const isFollowing = event?.id ? (eventFollowStatus[event.id]?.isFollowing || false) : false;
|
const isFollowing = event?.id ? (eventFollowStatus[event.id]?.isFollowing || false) : false;
|
||||||
const followerCount = event?.id ? (eventFollowStatus[event.id]?.followerCount || event.follower_count || 0) : 0;
|
const followerCount = event?.id ? (eventFollowStatus[event.id]?.followerCount || event.follower_count || 0) : 0;
|
||||||
|
|
||||||
|
// 权限判断函数
|
||||||
|
const hasAccess = useCallback((requiredTier) => {
|
||||||
|
const tierLevel = { free: 0, pro: 1, max: 2 };
|
||||||
|
const result = tierLevel[userTier] >= tierLevel[requiredTier];
|
||||||
|
return result;
|
||||||
|
}, [userTier]);
|
||||||
|
|
||||||
|
// 升级弹窗状态
|
||||||
|
const [upgradeModal, setUpgradeModal] = useState({
|
||||||
|
isOpen: false,
|
||||||
|
requiredLevel: 'pro',
|
||||||
|
featureName: ''
|
||||||
|
});
|
||||||
|
|
||||||
// 使用 Hook 获取实时数据(禁用自动加载,改为手动触发)
|
// 使用 Hook 获取实时数据(禁用自动加载,改为手动触发)
|
||||||
const {
|
const {
|
||||||
stocks,
|
stocks,
|
||||||
@@ -55,10 +76,19 @@ const DynamicNewsDetailPanel = ({ event }) => {
|
|||||||
loadChainAnalysis
|
loadChainAnalysis
|
||||||
} = useEventStocks(event?.id, event?.created_at, { autoLoad: false });
|
} = useEventStocks(event?.id, event?.created_at, { autoLoad: false });
|
||||||
|
|
||||||
// 子区块折叠状态管理(默认折叠)+ 加载追踪
|
// 相关股票、相关概念、历史事件和传导链的权限
|
||||||
const [isStocksOpen, setIsStocksOpen] = useState(false);
|
const canAccessStocks = hasAccess('pro');
|
||||||
|
const canAccessConcepts = hasAccess('pro');
|
||||||
|
const canAccessHistorical = hasAccess('pro');
|
||||||
|
const canAccessTransmission = hasAccess('max');
|
||||||
|
|
||||||
|
// 子区块折叠状态管理 + 加载追踪
|
||||||
|
// PRO 会员的相关股票默认展开
|
||||||
|
const [isStocksOpen, setIsStocksOpen] = useState(canAccessStocks && userTier === 'pro');
|
||||||
const [hasLoadedStocks, setHasLoadedStocks] = useState(false);
|
const [hasLoadedStocks, setHasLoadedStocks] = useState(false);
|
||||||
|
|
||||||
|
const [isConceptsOpen, setIsConceptsOpen] = useState(false);
|
||||||
|
|
||||||
const [isHistoricalOpen, setIsHistoricalOpen] = useState(false);
|
const [isHistoricalOpen, setIsHistoricalOpen] = useState(false);
|
||||||
const [hasLoadedHistorical, setHasLoadedHistorical] = useState(false);
|
const [hasLoadedHistorical, setHasLoadedHistorical] = useState(false);
|
||||||
|
|
||||||
@@ -75,7 +105,25 @@ const DynamicNewsDetailPanel = ({ event }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 相关股票 - 展开时加载
|
// 锁定点击处理 - 弹出升级弹窗
|
||||||
|
const handleLockedClick = useCallback((featureName, requiredLevel) => {
|
||||||
|
setUpgradeModal({
|
||||||
|
isOpen: true,
|
||||||
|
requiredLevel,
|
||||||
|
featureName
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 关闭升级弹窗
|
||||||
|
const handleCloseUpgradeModal = useCallback(() => {
|
||||||
|
setUpgradeModal({
|
||||||
|
isOpen: false,
|
||||||
|
requiredLevel: 'pro',
|
||||||
|
featureName: ''
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 相关股票 - 展开时加载(需要 PRO 权限)
|
||||||
const handleStocksToggle = useCallback(() => {
|
const handleStocksToggle = useCallback(() => {
|
||||||
const newState = !isStocksOpen;
|
const newState = !isStocksOpen;
|
||||||
setIsStocksOpen(newState);
|
setIsStocksOpen(newState);
|
||||||
@@ -87,6 +135,11 @@ const DynamicNewsDetailPanel = ({ event }) => {
|
|||||||
}
|
}
|
||||||
}, [isStocksOpen, hasLoadedStocks, loadStocksData, event?.id]);
|
}, [isStocksOpen, hasLoadedStocks, loadStocksData, event?.id]);
|
||||||
|
|
||||||
|
// 相关概念 - 展开/收起(无需加载)
|
||||||
|
const handleConceptsToggle = useCallback(() => {
|
||||||
|
setIsConceptsOpen(!isConceptsOpen);
|
||||||
|
}, [isConceptsOpen]);
|
||||||
|
|
||||||
// 历史事件对比 - 展开时加载
|
// 历史事件对比 - 展开时加载
|
||||||
const handleHistoricalToggle = useCallback(() => {
|
const handleHistoricalToggle = useCallback(() => {
|
||||||
const newState = !isHistoricalOpen;
|
const newState = !isHistoricalOpen;
|
||||||
@@ -114,13 +167,25 @@ const DynamicNewsDetailPanel = ({ event }) => {
|
|||||||
// 事件切换时重置所有子模块状态
|
// 事件切换时重置所有子模块状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('%c🔄 [事件切换] 重置所有子模块状态', 'color: #F59E0B; font-weight: bold;', { eventId: event?.id });
|
console.log('%c🔄 [事件切换] 重置所有子模块状态', 'color: #F59E0B; font-weight: bold;', { eventId: event?.id });
|
||||||
setIsStocksOpen(false);
|
|
||||||
|
// PRO 会员的相关股票默认展开,其他情况收起
|
||||||
|
const shouldOpenStocks = canAccessStocks && userTier === 'pro';
|
||||||
|
setIsStocksOpen(shouldOpenStocks);
|
||||||
setHasLoadedStocks(false);
|
setHasLoadedStocks(false);
|
||||||
|
|
||||||
|
// PRO 会员默认展开时,自动加载股票数据
|
||||||
|
if (shouldOpenStocks) {
|
||||||
|
console.log('%c📊 [PRO会员] 自动加载相关股票数据', 'color: #10B981; font-weight: bold;', { eventId: event?.id });
|
||||||
|
loadStocksData();
|
||||||
|
setHasLoadedStocks(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsConceptsOpen(false);
|
||||||
setIsHistoricalOpen(false);
|
setIsHistoricalOpen(false);
|
||||||
setHasLoadedHistorical(false);
|
setHasLoadedHistorical(false);
|
||||||
setIsTransmissionOpen(false);
|
setIsTransmissionOpen(false);
|
||||||
setHasLoadedTransmission(false);
|
setHasLoadedTransmission(false);
|
||||||
}, [event?.id]);
|
}, [event?.id, canAccessStocks, userTier, loadStocksData]);
|
||||||
|
|
||||||
// 切换关注状态
|
// 切换关注状态
|
||||||
const handleToggleFollow = useCallback(async () => {
|
const handleToggleFollow = useCallback(async () => {
|
||||||
@@ -196,12 +261,20 @@ const DynamicNewsDetailPanel = ({ event }) => {
|
|||||||
{/* 事件描述 */}
|
{/* 事件描述 */}
|
||||||
<EventDescriptionSection description={event.description} />
|
<EventDescriptionSection description={event.description} />
|
||||||
|
|
||||||
{/* 相关股票(可折叠) - 懒加载 */}
|
{/* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 */}
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="相关股票"
|
title="相关股票"
|
||||||
isOpen={isStocksOpen}
|
isOpen={isStocksOpen}
|
||||||
onToggle={handleStocksToggle}
|
onToggle={handleStocksToggle}
|
||||||
count={stocks?.length || 0}
|
count={stocks?.length || 0}
|
||||||
|
subscriptionBadge={(() => {
|
||||||
|
if (!canAccessStocks) {
|
||||||
|
return <SubscriptionBadge tier="pro" size="sm" />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
isLocked={!canAccessStocks}
|
||||||
|
onLockedClick={() => handleLockedClick('相关股票', 'pro')}
|
||||||
>
|
>
|
||||||
{loading.stocks || loading.quotes ? (
|
{loading.stocks || loading.quotes ? (
|
||||||
<Center py={4}>
|
<Center py={4}>
|
||||||
@@ -219,19 +292,27 @@ const DynamicNewsDetailPanel = ({ event }) => {
|
|||||||
)}
|
)}
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 相关概念 */}
|
{/* 相关概念(可折叠) - 需要 PRO 权限 */}
|
||||||
<RelatedConceptsSection
|
<RelatedConceptsSection
|
||||||
eventTitle={event.title}
|
eventTitle={event.title}
|
||||||
effectiveTradingDate={event.trading_date || event.created_at}
|
effectiveTradingDate={event.trading_date || event.created_at}
|
||||||
eventTime={event.created_at}
|
eventTime={event.created_at}
|
||||||
|
isOpen={isConceptsOpen}
|
||||||
|
onToggle={handleConceptsToggle}
|
||||||
|
subscriptionBadge={!canAccessConcepts ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||||||
|
isLocked={!canAccessConcepts}
|
||||||
|
onLockedClick={() => handleLockedClick('相关概念', 'pro')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 历史事件对比(可折叠) - 懒加载 */}
|
{/* 历史事件对比(可折叠) - 懒加载 - 需要 PRO 权限 */}
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="历史事件对比"
|
title="历史事件对比"
|
||||||
isOpen={isHistoricalOpen}
|
isOpen={isHistoricalOpen}
|
||||||
onToggle={handleHistoricalToggle}
|
onToggle={handleHistoricalToggle}
|
||||||
count={historicalEvents?.length || 0}
|
count={historicalEvents?.length || 0}
|
||||||
|
subscriptionBadge={!canAccessHistorical ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||||||
|
isLocked={!canAccessHistorical}
|
||||||
|
onLockedClick={() => handleLockedClick('历史事件对比', 'pro')}
|
||||||
>
|
>
|
||||||
{loading.historicalEvents ? (
|
{loading.historicalEvents ? (
|
||||||
<Center py={4}>
|
<Center py={4}>
|
||||||
@@ -246,11 +327,14 @@ const DynamicNewsDetailPanel = ({ event }) => {
|
|||||||
)}
|
)}
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 传导链分析(可折叠) - 懒加载 */}
|
{/* 传导链分析(可折叠) - 懒加载 - 需要 MAX 权限 */}
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="传导链分析"
|
title="传导链分析"
|
||||||
isOpen={isTransmissionOpen}
|
isOpen={isTransmissionOpen}
|
||||||
onToggle={handleTransmissionToggle}
|
onToggle={handleTransmissionToggle}
|
||||||
|
subscriptionBadge={!canAccessTransmission ? <SubscriptionBadge tier="max" size="sm" /> : null}
|
||||||
|
isLocked={!canAccessTransmission}
|
||||||
|
onLockedClick={() => handleLockedClick('传导链分析', 'max')}
|
||||||
>
|
>
|
||||||
<TransmissionChainAnalysis
|
<TransmissionChainAnalysis
|
||||||
eventId={event.id}
|
eventId={event.id}
|
||||||
@@ -259,6 +343,17 @@ const DynamicNewsDetailPanel = ({ event }) => {
|
|||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</VStack>
|
</VStack>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
|
{/* 升级弹窗 */}
|
||||||
|
{upgradeModal.isOpen ? (
|
||||||
|
<SubscriptionUpgradeModal
|
||||||
|
isOpen={upgradeModal.isOpen}
|
||||||
|
onClose={handleCloseUpgradeModal}
|
||||||
|
requiredLevel={upgradeModal.requiredLevel}
|
||||||
|
featureName={upgradeModal.featureName}
|
||||||
|
currentLevel={userTier}
|
||||||
|
/>
|
||||||
|
): null }
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
|
VStack,
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
|
Badge,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
@@ -27,10 +29,15 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
|
|||||||
const relevanceScore = Math.round((concept.score || 0) * 100);
|
const relevanceScore = Math.round((concept.score || 0) * 100);
|
||||||
const relevanceColors = getRelevanceColor(relevanceScore);
|
const relevanceColors = getRelevanceColor(relevanceScore);
|
||||||
|
|
||||||
|
// 涨跌幅数据
|
||||||
|
const changePct = concept.price_info?.avg_change_pct ? parseFloat(concept.price_info.avg_change_pct) : null;
|
||||||
|
const changeColor = changePct !== null ? (changePct > 0 ? 'red' : changePct < 0 ? 'green' : 'gray') : null;
|
||||||
|
const changeSymbol = changePct !== null && changePct > 0 ? '+' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<VStack
|
||||||
align="center"
|
align="stretch"
|
||||||
justify="space-between"
|
spacing={2}
|
||||||
bg={cardBg}
|
bg={cardBg}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
@@ -46,28 +53,50 @@ const SimpleConceptCard = ({ concept, onClick, getRelevanceColor }) => {
|
|||||||
}}
|
}}
|
||||||
onClick={() => onClick(concept)}
|
onClick={() => onClick(concept)}
|
||||||
>
|
>
|
||||||
{/* 左侧:概念名 + 数量 */}
|
{/* 第一行:概念名 + 数量(允许折行) */}
|
||||||
<Text fontSize="sm" fontWeight="normal" color={conceptNameColor} mr={3}>
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="normal"
|
||||||
|
color={conceptNameColor}
|
||||||
|
wordBreak="break-word"
|
||||||
|
lineHeight="1.4"
|
||||||
|
>
|
||||||
{concept.concept}{' '}
|
{concept.concept}{' '}
|
||||||
<Text as="span" color="gray.500">
|
<Text as="span" color="gray.500">
|
||||||
({concept.stock_count})
|
({concept.stock_count})
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* 右侧:相关度标签 */}
|
{/* 第二行:相关度 + 涨跌幅 */}
|
||||||
<Box
|
<Flex justify="space-between" align="center" gap={2} flexWrap="wrap">
|
||||||
bg={relevanceColors.bg}
|
{/* 相关度标签 */}
|
||||||
color={relevanceColors.color}
|
<Box
|
||||||
px={3}
|
bg={relevanceColors.bg}
|
||||||
py={1}
|
color={relevanceColors.color}
|
||||||
borderRadius="md"
|
px={2}
|
||||||
flexShrink={0}
|
py={0.5}
|
||||||
>
|
borderRadius="sm"
|
||||||
<Text fontSize="xs" fontWeight="medium" whiteSpace="nowrap">
|
flexShrink={0}
|
||||||
相关度: {relevanceScore}%
|
>
|
||||||
</Text>
|
<Text fontSize="xs" fontWeight="medium" whiteSpace="nowrap">
|
||||||
</Box>
|
相关度: {relevanceScore}%
|
||||||
</Flex>
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 涨跌幅数据 */}
|
||||||
|
{changePct !== null && (
|
||||||
|
<Badge
|
||||||
|
colorScheme={changeColor}
|
||||||
|
fontSize="xs"
|
||||||
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
{changeSymbol}{changePct.toFixed(2)}%
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user