更新Company页面的UI为FUI风格
This commit is contained in:
24
app.py
24
app.py
@@ -43,6 +43,7 @@ else:
|
|||||||
import base64
|
import base64
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib
|
import urllib
|
||||||
import uuid
|
import uuid
|
||||||
@@ -7517,20 +7518,16 @@ def get_stock_quotes():
|
|||||||
prev_close_map[norm_code] = base_close_map[base_code]
|
prev_close_map[norm_code] = base_close_map[base_code]
|
||||||
|
|
||||||
# 批量查询当前价格数据(从 ClickHouse)
|
# 批量查询当前价格数据(从 ClickHouse)
|
||||||
|
# 使用 argMax 函数获取最新价格,比窗口函数效率高很多
|
||||||
batch_price_query = """
|
batch_price_query = """
|
||||||
WITH last_prices AS (
|
|
||||||
SELECT
|
SELECT
|
||||||
code,
|
code,
|
||||||
close as last_price,
|
argMax(close, timestamp) as last_price
|
||||||
ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn
|
|
||||||
FROM stock_minute
|
FROM stock_minute
|
||||||
WHERE code IN %(codes)s
|
WHERE code IN %(codes)s
|
||||||
AND timestamp >= %(start)s
|
AND timestamp >= %(start)s
|
||||||
AND timestamp <= %(end)s
|
AND timestamp <= %(end)s
|
||||||
)
|
GROUP BY code
|
||||||
SELECT code, last_price
|
|
||||||
FROM last_prices
|
|
||||||
WHERE rn = 1
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
batch_data = client.execute(batch_price_query, {
|
batch_data = client.execute(batch_price_query, {
|
||||||
@@ -7626,14 +7623,25 @@ def get_stock_quotes():
|
|||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== ClickHouse 连接池(单例模式) ====================
|
||||||
|
_clickhouse_client = None
|
||||||
|
_clickhouse_client_lock = threading.Lock()
|
||||||
|
|
||||||
def get_clickhouse_client():
|
def get_clickhouse_client():
|
||||||
return Cclient(
|
"""获取 ClickHouse 客户端(单例模式,避免重复创建连接)"""
|
||||||
|
global _clickhouse_client
|
||||||
|
if _clickhouse_client is None:
|
||||||
|
with _clickhouse_client_lock:
|
||||||
|
if _clickhouse_client is None:
|
||||||
|
_clickhouse_client = Cclient(
|
||||||
host='127.0.0.1',
|
host='127.0.0.1',
|
||||||
port=9000,
|
port=9000,
|
||||||
user='default',
|
user='default',
|
||||||
password='Zzl33818!',
|
password='Zzl33818!',
|
||||||
database='stock'
|
database='stock'
|
||||||
)
|
)
|
||||||
|
print("[ClickHouse] 创建新连接(单例)")
|
||||||
|
return _clickhouse_client
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/account/calendar/events', methods=['GET', 'POST'])
|
@app.route('/api/account/calendar/events', methods=['GET', 'POST'])
|
||||||
|
|||||||
@@ -198,10 +198,6 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
}
|
}
|
||||||
}, [sectionState.stocks, stocks.length, refreshQuotes]);
|
}, [sectionState.stocks, stocks.length, refreshQuotes]);
|
||||||
|
|
||||||
// 相关概念 - 展开/收起(无需加载)
|
|
||||||
const handleConceptsToggle = useCallback(() => {
|
|
||||||
dispatchSection({ type: 'TOGGLE', section: 'concepts' });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 历史事件对比 - 数据已预加载,只需切换展开状态
|
// 历史事件对比 - 数据已预加载,只需切换展开状态
|
||||||
const handleHistoricalToggle = useCallback(() => {
|
const handleHistoricalToggle = useCallback(() => {
|
||||||
@@ -350,14 +346,10 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
)}
|
)}
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 相关概念(可折叠) - 需要 PRO 权限 */}
|
{/* 相关概念(手风琴样式) - 需要 PRO 权限 */}
|
||||||
<RelatedConceptsSection
|
<RelatedConceptsSection
|
||||||
eventId={event.id}
|
eventId={event.id}
|
||||||
eventTitle={event.title}
|
eventTitle={event.title}
|
||||||
effectiveTradingDate={event.trading_date || event.created_at}
|
|
||||||
eventTime={event.created_at}
|
|
||||||
isOpen={sectionState.concepts.isOpen}
|
|
||||||
onToggle={handleConceptsToggle}
|
|
||||||
subscriptionBadge={!canAccessConcepts ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
subscriptionBadge={!canAccessConcepts ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||||||
isLocked={!canAccessConcepts}
|
isLocked={!canAccessConcepts}
|
||||||
onLockedClick={() => handleLockedClick('相关概念', 'pro')}
|
onLockedClick={() => handleLockedClick('相关概念', 'pro')}
|
||||||
|
|||||||
@@ -1,91 +1,149 @@
|
|||||||
// src/components/EventDetailPanel/RelatedConceptsSection/index.js
|
// src/components/EventDetailPanel/RelatedConceptsSection/index.js
|
||||||
// 相关概念区组件(主组件)
|
// 相关概念区组件 - 折叠手风琴样式
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
SimpleGrid,
|
|
||||||
Flex,
|
Flex,
|
||||||
Button,
|
|
||||||
Collapse,
|
|
||||||
Heading,
|
Heading,
|
||||||
Center,
|
Center,
|
||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
|
Badge,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
Collapse,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
|
import { ChevronRightIcon, ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import SimpleConceptCard from './SimpleConceptCard';
|
|
||||||
import DetailedConceptCard from './DetailedConceptCard';
|
|
||||||
import TradingDateInfo from './TradingDateInfo';
|
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个概念项组件(手风琴项)
|
||||||
|
*/
|
||||||
|
const ConceptItem = ({ concept, isExpanded, onToggle, onNavigate }) => {
|
||||||
|
const itemBg = useColorModeValue('white', 'gray.700');
|
||||||
|
const itemHoverBg = useColorModeValue('gray.50', 'gray.650');
|
||||||
|
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
|
const conceptColor = useColorModeValue('blue.600', 'blue.300');
|
||||||
|
const reasonBg = useColorModeValue('blue.50', 'gray.800');
|
||||||
|
const reasonColor = useColorModeValue('gray.700', 'gray.200');
|
||||||
|
const iconColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="md"
|
||||||
|
overflow="hidden"
|
||||||
|
bg={itemBg}
|
||||||
|
>
|
||||||
|
{/* 概念标题行 - 可点击展开 */}
|
||||||
|
<Flex
|
||||||
|
px={3}
|
||||||
|
py={2.5}
|
||||||
|
cursor="pointer"
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
_hover={{ bg: itemHoverBg }}
|
||||||
|
onClick={onToggle}
|
||||||
|
transition="background 0.2s"
|
||||||
|
>
|
||||||
|
<HStack spacing={2} flex={1}>
|
||||||
|
<Icon
|
||||||
|
as={isExpanded ? ChevronDownIcon : ChevronRightIcon}
|
||||||
|
color={iconColor}
|
||||||
|
boxSize={4}
|
||||||
|
transition="transform 0.2s"
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="medium"
|
||||||
|
color={conceptColor}
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{ textDecoration: 'underline' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onNavigate(concept);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{concept.concept}
|
||||||
|
</Text>
|
||||||
|
<Badge colorScheme="green" fontSize="xs" flexShrink={0}>
|
||||||
|
AI 分析
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 关联原因 - 可折叠 */}
|
||||||
|
<Collapse in={isExpanded} animateOpacity>
|
||||||
|
<Box
|
||||||
|
px={4}
|
||||||
|
py={3}
|
||||||
|
bg={reasonBg}
|
||||||
|
borderTop="1px solid"
|
||||||
|
borderTopColor={borderColor}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
color={reasonColor}
|
||||||
|
lineHeight="1.8"
|
||||||
|
whiteSpace="pre-wrap"
|
||||||
|
>
|
||||||
|
{concept.reason || '暂无关联原因说明'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 相关概念区组件
|
* 相关概念区组件
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
* @param {number} props.eventId - 事件ID(用于获取 related_concepts 表数据)
|
* @param {number} props.eventId - 事件ID(用于获取 related_concepts 表数据)
|
||||||
* @param {string} props.eventTitle - 事件标题(备用,当 eventId 不存在时使用搜索)
|
* @param {string} props.eventTitle - 事件标题(备用)
|
||||||
* @param {string} props.effectiveTradingDate - 有效交易日期(涨跌幅数据日期)
|
|
||||||
* @param {string|Object} props.eventTime - 事件发生时间
|
|
||||||
* @param {React.ReactNode} props.subscriptionBadge - 订阅徽章组件(可选)
|
* @param {React.ReactNode} props.subscriptionBadge - 订阅徽章组件(可选)
|
||||||
* @param {boolean} props.isLocked - 是否锁定详细模式(需要付费)
|
* @param {boolean} props.isLocked - 是否锁定(需要付费)
|
||||||
* @param {Function} props.onLockedClick - 锁定时的点击回调(触发付费弹窗)
|
* @param {Function} props.onLockedClick - 锁定时的点击回调
|
||||||
*/
|
*/
|
||||||
const RelatedConceptsSection = ({
|
const RelatedConceptsSection = ({
|
||||||
eventId,
|
eventId,
|
||||||
eventTitle,
|
eventTitle,
|
||||||
effectiveTradingDate,
|
|
||||||
eventTime,
|
|
||||||
subscriptionBadge = null,
|
subscriptionBadge = null,
|
||||||
isLocked = false,
|
isLocked = false,
|
||||||
onLockedClick = null,
|
onLockedClick = null,
|
||||||
isOpen = undefined, // 新增:受控模式(外部控制展开状态)
|
|
||||||
onToggle = undefined // 新增:受控模式(外部控制展开回调)
|
|
||||||
}) => {
|
}) => {
|
||||||
// 使用外部 isOpen,如果没有则使用内部 useState
|
|
||||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
|
||||||
const isExpanded = onToggle !== undefined ? isOpen : internalExpanded;
|
|
||||||
const [concepts, setConcepts] = useState([]);
|
const [concepts, setConcepts] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
// 记录每个概念的展开状态
|
||||||
|
const [expandedItems, setExpandedItems] = useState({});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 颜色配置
|
// 颜色配置
|
||||||
const sectionBg = useColorModeValue('gray.50', 'gray.750');
|
const sectionBg = useColorModeValue('gray.50', 'gray.750');
|
||||||
const headingColor = useColorModeValue('gray.700', 'gray.200');
|
const headingColor = useColorModeValue('gray.700', 'gray.200');
|
||||||
const textColor = useColorModeValue('gray.600', 'gray.400');
|
const textColor = useColorModeValue('gray.600', 'gray.400');
|
||||||
|
const countBadgeBg = useColorModeValue('blue.100', 'blue.800');
|
||||||
|
const countBadgeColor = useColorModeValue('blue.700', 'blue.200');
|
||||||
|
|
||||||
console.log('[RelatedConceptsSection] 组件渲染', {
|
// 获取相关概念
|
||||||
eventId,
|
|
||||||
eventTitle,
|
|
||||||
effectiveTradingDate,
|
|
||||||
eventTime,
|
|
||||||
loading,
|
|
||||||
conceptsCount: concepts?.length || 0,
|
|
||||||
error
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取相关概念 - 优先使用 eventId 从数据库获取
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchConcepts = async () => {
|
const fetchConcepts = async () => {
|
||||||
console.log('[RelatedConceptsSection] useEffect 触发', {
|
if (!eventId) {
|
||||||
eventId,
|
setLoading(false);
|
||||||
eventTitle,
|
return;
|
||||||
effectiveTradingDate
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 优先使用 eventId 获取数据库中的相关概念
|
|
||||||
if (eventId) {
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const apiUrl = `${getApiBase()}/api/events/${eventId}/concepts`;
|
const apiUrl = `${getApiBase()}/api/events/${eventId}/concepts`;
|
||||||
console.log('[RelatedConceptsSection] 从数据库获取相关概念', { url: apiUrl });
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -93,9 +151,7 @@ const RelatedConceptsSection = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// 如果是 403,说明需要订阅,不是错误
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
console.log('[RelatedConceptsSection] 需要订阅才能查看');
|
|
||||||
setConcepts([]);
|
setConcepts([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -104,21 +160,12 @@ const RelatedConceptsSection = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('[RelatedConceptsSection] 数据库响应', data);
|
|
||||||
|
|
||||||
if (data.success && Array.isArray(data.data)) {
|
if (data.success && Array.isArray(data.data)) {
|
||||||
// 转换数据格式,使其与原有展示逻辑兼容
|
setConcepts(data.data);
|
||||||
const formattedConcepts = data.data.map(item => ({
|
// 默认展开第一个
|
||||||
concept: item.concept,
|
if (data.data.length > 0) {
|
||||||
reason: item.reason,
|
setExpandedItems({ 0: true });
|
||||||
// 保留原有字段以兼容 DetailedConceptCard
|
}
|
||||||
score: 1, // 数据库中的都是高相关度
|
|
||||||
description: item.reason, // reason 作为描述
|
|
||||||
stocks: [], // 暂无股票数据
|
|
||||||
stock_count: 0
|
|
||||||
}));
|
|
||||||
console.log('[RelatedConceptsSection] 设置概念数据', formattedConcepts);
|
|
||||||
setConcepts(formattedConcepts);
|
|
||||||
} else {
|
} else {
|
||||||
setConcepts([]);
|
setConcepts([]);
|
||||||
}
|
}
|
||||||
@@ -130,161 +177,69 @@ const RelatedConceptsSection = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 降级方案:使用 eventTitle 搜索概念(兼容旧逻辑)
|
|
||||||
if (!eventTitle || !effectiveTradingDate) {
|
|
||||||
console.log('[RelatedConceptsSection] 缺少必要参数,跳过搜索', {
|
|
||||||
hasEventId: !!eventId,
|
|
||||||
hasEventTitle: !!eventTitle,
|
|
||||||
hasEffectiveTradingDate: !!effectiveTradingDate
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// 格式化交易日期
|
|
||||||
let formattedTradeDate;
|
|
||||||
try {
|
|
||||||
formattedTradeDate = dayjs(effectiveTradingDate).format('YYYY-MM-DD');
|
|
||||||
if (!dayjs(formattedTradeDate, 'YYYY-MM-DD', true).isValid()) {
|
|
||||||
formattedTradeDate = dayjs().format('YYYY-MM-DD');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
formattedTradeDate = dayjs().format('YYYY-MM-DD');
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestBody = {
|
|
||||||
query: eventTitle,
|
|
||||||
size: 5,
|
|
||||||
page: 1,
|
|
||||||
sort_by: "_score",
|
|
||||||
trade_date: formattedTradeDate
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiUrl = `${getApiBase()}/concept-api/search`;
|
|
||||||
console.log('[RelatedConceptsSection] 降级:使用搜索接口', { url: apiUrl, requestBody });
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(requestBody)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.results && Array.isArray(data.results)) {
|
|
||||||
setConcepts(data.results);
|
|
||||||
} else if (data.data && data.data.concepts) {
|
|
||||||
setConcepts(data.data.concepts);
|
|
||||||
} else {
|
|
||||||
setConcepts([]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[RelatedConceptsSection] 搜索概念失败', err);
|
|
||||||
setError('加载概念数据失败');
|
|
||||||
setConcepts([]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchConcepts();
|
fetchConcepts();
|
||||||
}, [eventId, eventTitle, effectiveTradingDate]);
|
}, [eventId]);
|
||||||
|
|
||||||
|
// 切换某个概念的展开状态
|
||||||
|
const toggleItem = (index) => {
|
||||||
|
if (isLocked && onLockedClick) {
|
||||||
|
onLockedClick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExpandedItems(prev => ({
|
||||||
|
...prev,
|
||||||
|
[index]: !prev[index]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳转到概念中心
|
||||||
|
const handleNavigate = (concept) => {
|
||||||
|
navigate(`/concepts?q=${encodeURIComponent(concept.concept)}`);
|
||||||
|
};
|
||||||
|
|
||||||
// 加载中状态
|
// 加载中状态
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Box bg={sectionBg} p={3} borderRadius="md">
|
<Box bg={sectionBg} p={3} borderRadius="md">
|
||||||
<Center py={4}>
|
<Center py={4}>
|
||||||
<Spinner size="md" color="blue.500" mr={2} />
|
<Spinner size="sm" color="blue.500" mr={2} />
|
||||||
<Text color={textColor} fontSize="sm">加载相关概念中...</Text>
|
<Text color={textColor} fontSize="sm">加载相关概念中...</Text>
|
||||||
</Center>
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否有数据
|
|
||||||
const hasNoConcepts = !concepts || concepts.length === 0;
|
const hasNoConcepts = !concepts || concepts.length === 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据相关度获取颜色(浅色背景 + 深色文字)
|
|
||||||
* @param {number} relevance - 相关度(0-100)
|
|
||||||
* @returns {Object} 包含背景色和文字色
|
|
||||||
*/
|
|
||||||
const getRelevanceColor = (relevance) => {
|
|
||||||
if (relevance >= 90) {
|
|
||||||
return { bg: 'purple.50', color: 'purple.800' }; // 极高相关
|
|
||||||
} else if (relevance >= 80) {
|
|
||||||
return { bg: 'pink.50', color: 'pink.800' }; // 高相关
|
|
||||||
} else if (relevance >= 70) {
|
|
||||||
return { bg: 'orange.50', color: 'orange.800' }; // 中等相关
|
|
||||||
} else {
|
|
||||||
return { bg: 'gray.100', color: 'gray.700' }; // 低相关
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理概念点击
|
|
||||||
* @param {Object} concept - 概念对象
|
|
||||||
*/
|
|
||||||
const handleConceptClick = (concept) => {
|
|
||||||
// 跳转到概念中心,并搜索该概念
|
|
||||||
navigate(`/concepts?q=${encodeURIComponent(concept.concept)}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box bg={sectionBg} p={3} borderRadius="md">
|
<Box bg={sectionBg} p={3} borderRadius="md">
|
||||||
{/* 标题栏 - 两行布局 */}
|
{/* 标题栏 */}
|
||||||
<Box mb={3}>
|
<Flex justify="space-between" align="center" mb={3}>
|
||||||
{/* 第一行:标题 + Badge + 按钮 */}
|
<HStack spacing={2}>
|
||||||
<Flex justify="space-between" align="center" mb={2}>
|
|
||||||
<Flex align="center" gap={2}>
|
|
||||||
<Heading size="sm" color={headingColor}>
|
<Heading size="sm" color={headingColor}>
|
||||||
相关概念
|
相关概念
|
||||||
</Heading>
|
</Heading>
|
||||||
{/* 订阅徽章 */}
|
{!hasNoConcepts && (
|
||||||
{subscriptionBadge}
|
<Badge
|
||||||
</Flex>
|
bg={countBadgeBg}
|
||||||
<Button
|
color={countBadgeColor}
|
||||||
size="sm"
|
fontSize="xs"
|
||||||
variant="ghost"
|
px={2}
|
||||||
colorScheme="blue"
|
py={0.5}
|
||||||
rightIcon={isExpanded ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
borderRadius="full"
|
||||||
onClick={() => {
|
|
||||||
// 如果被锁定且有回调函数,触发付费弹窗
|
|
||||||
if (isLocked && onLockedClick) {
|
|
||||||
onLockedClick();
|
|
||||||
} else if (onToggle !== undefined) {
|
|
||||||
// 受控模式:调用外部回调
|
|
||||||
onToggle();
|
|
||||||
} else {
|
|
||||||
// 非受控模式:使用内部状态
|
|
||||||
setInternalExpanded(!internalExpanded);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isExpanded ? '收起' : '查看详细'}
|
{concepts.length}
|
||||||
</Button>
|
</Badge>
|
||||||
|
)}
|
||||||
|
{subscriptionBadge}
|
||||||
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* 第二行:交易日期信息 */}
|
|
||||||
<TradingDateInfo
|
|
||||||
effectiveTradingDate={effectiveTradingDate}
|
|
||||||
eventTime={eventTime}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 简单模式:横向卡片列表(总是显示) */}
|
{/* 概念列表 - 手风琴样式 */}
|
||||||
{hasNoConcepts ? (
|
{hasNoConcepts ? (
|
||||||
<Box mb={isExpanded ? 3 : 0}>
|
<Box py={2}>
|
||||||
{error ? (
|
{error ? (
|
||||||
<Text color="red.500" fontSize="sm">{error}</Text>
|
<Text color="red.500" fontSize="sm">{error}</Text>
|
||||||
) : (
|
) : (
|
||||||
@@ -292,41 +247,18 @@ const RelatedConceptsSection = ({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Flex gap={2} flexWrap="wrap" mb={isExpanded ? 3 : 0}>
|
<VStack spacing={2} align="stretch">
|
||||||
{concepts.map((concept, index) => (
|
{concepts.map((concept, index) => (
|
||||||
<SimpleConceptCard
|
<ConceptItem
|
||||||
key={index}
|
key={concept.id || index}
|
||||||
concept={concept}
|
concept={concept}
|
||||||
onClick={handleConceptClick}
|
isExpanded={!!expandedItems[index]}
|
||||||
getRelevanceColor={getRelevanceColor}
|
onToggle={() => toggleItem(index)}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 详细模式:卡片网格(可折叠) */}
|
|
||||||
<Collapse in={isExpanded} animateOpacity>
|
|
||||||
{hasNoConcepts ? (
|
|
||||||
<Box py={4}>
|
|
||||||
{error ? (
|
|
||||||
<Text color="red.500" fontSize="sm" textAlign="center">{error}</Text>
|
|
||||||
) : (
|
|
||||||
<Text color={textColor} fontSize="sm" textAlign="center">暂无详细数据</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
/* 详细概念卡片网格 */
|
|
||||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
|
||||||
{concepts.map((concept, index) => (
|
|
||||||
<DetailedConceptCard
|
|
||||||
key={index}
|
|
||||||
concept={concept}
|
|
||||||
onClick={handleConceptClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
)}
|
|
||||||
</Collapse>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user