Merge branch 'feature_bugfix/251217_stock' of https://git.valuefrontier.cn/vf/vf_react into feature_bugfix/251217_stock

This commit is contained in:
2025-12-24 18:39:54 +08:00
33 changed files with 415 additions and 2698 deletions

View File

@@ -23,7 +23,8 @@ const Center: React.FC = () => {
return (
<Box bg={THEME.bg.primary} minH="100vh" overflowX="hidden">
<Box px={{ base: 3, md: 4 }} py={{ base: 4, md: 6 }} maxW="container.xl" mx="auto">
{/* padding 由 MainLayout 统一设置 */}
<Box py={{ base: 4, md: 6 }}>
{/* 市场概览仪表盘 */}
<Box mb={4}>
<MarketDashboard />

View File

@@ -8,8 +8,6 @@ import {
} from '@/store/slices/communityDataSlice';
import {
Box,
Container,
useBreakpointValue,
Skeleton,
} from '@chakra-ui/react';
@@ -45,23 +43,6 @@ const Community = () => {
// Ref用于首次滚动到内容区域
const containerRef = useRef(null);
// 响应式容器宽度
const containerMaxW = useBreakpointValue({
base: '100%', // 移动端:全宽
sm: '100%', // 小屏:全宽
md: '100%', // 中屏:全宽
lg: '1200px', // 大屏1200px
xl: '1400px', // 超大屏1400px
});
// 响应式内边距
const containerPx = useBreakpointValue({
base: 2, // 移动端:最小内边距
sm: 3,
md: 4,
lg: 6,
});
// ⚡ 通知权限引导
const { browserPermission, requestBrowserPermission, registerEventUpdateCallback } = useNotification();
@@ -165,8 +146,8 @@ const Community = () => {
return (
<Box minH="100vh" bg={bgColor}>
{/* 主内容区域 */}
<Container ref={containerRef} maxW={containerMaxW} px={containerPx} pt={{ base: 3, md: 6 }} pb={{ base: 4, md: 8 }}>
{/* 主内容区域 - padding 由 MainLayout 统一设置 */}
<Box ref={containerRef} pt={{ base: 3, md: 6 }} pb={{ base: 4, md: 8 }}>
{/* ⚡ 顶部说明面板(懒加载):产品介绍 + 沪深指数 + 热门概念词云 */}
<Suspense fallback={
<Box mb={6} p={4} borderRadius="xl" bg="rgba(255,255,255,0.02)">
@@ -200,7 +181,7 @@ const Community = () => {
events={hotEvents}
onEventClick={communityEvents.trackNewsArticleClicked}
/>
</Container>
</Box>
</Box>
);
};

View File

@@ -7,6 +7,7 @@ import { Box, Flex, HStack, VStack, Text } from '@chakra-ui/react';
import { AutoComplete, Input, Spin } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { useStockSearch } from '@hooks/useStockSearch';
import { LAYOUT_PADDING } from '@/layouts/config/layoutConfig';
import { THEME } from '../../config';
import { FUI_COLORS, FUI_GLOW } from '../../theme/fui';
import type { CompanyHeaderProps, StockSearchResult } from '../../types';
@@ -115,17 +116,17 @@ SearchBox.displayName = 'SearchBox';
const CompanyHeader: React.FC<CompanyHeaderProps> = memo(({ onStockChange }) => (
<Box
position="relative"
bg={FUI_COLORS.bg.primary}
borderBottom="1px solid"
borderColor={FUI_COLORS.line.default}
px={6}
py={4}
border="1px solid"
borderRadius="8px"
sx={{
bg: FUI_COLORS.bg.primary,
borderColor: FUI_COLORS.line.default,
padding: '20px 20px',
}}
>
<Flex
position="relative"
zIndex={1}
maxW="container.xl"
mx="auto"
justify="space-between"
align="center"
>

View File

@@ -332,7 +332,8 @@ const CompanyIndex: React.FC = () => {
position="relative"
bg={THEME.bg}
minH="calc(100vh - 60px)"
overflow="hidden"
overflowX="visible"
overflowY="hidden"
>
{/* ========================================
全局环境光效果
@@ -371,12 +372,10 @@ const CompanyIndex: React.FC = () => {
{/*
内容容器
- maxW="container.xl": 最大宽度限制,保持内容可读性
- mx="auto": 水平居中
- px={4}: 左右内边距 16px
- py={6}: 上下内边距 24px
- 水平 padding 由 MainLayout 统一设置
*/}
<Box maxW="container.xl" mx="auto" px={4} py={6}>
<Box py={6}>
{/* ========================================
股票行情卡片
========================================

View File

@@ -4,7 +4,6 @@ import { logger } from '../../utils/logger';
import defaultEventImage from '../../assets/img/default-event.jpg';
import {
Box,
Container,
Heading,
Text,
Input,
@@ -1538,12 +1537,14 @@ const ConceptCenter = () => {
{/* 导航栏已由 MainLayout 提供 */}
{/* Hero Section - 精简版 */}
{/* Hero Section - 使用负 margin 抵消 Layout 的 padding 实现全宽背景 */}
<Box
position="relative"
bgGradient="linear(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%)"
color="white"
overflow="hidden"
zIndex={1}
mx={{ base: -4, md: -6, lg: '-80px' }}
>
{/* 科幻网格背景 */}
<Box
@@ -1579,7 +1580,7 @@ const ConceptCenter = () => {
filter="blur(50px)"
/>
<Container maxW="container.xl" position="relative" py={{ base: 8, md: 12 }}>
<Box px={{ base: 4, md: 6, lg: '80px' }} position="relative" py={{ base: 8, md: 12 }}>
<VStack spacing={6}>
{/* 标题区域 */}
<VStack spacing={3} textAlign="center">
@@ -1743,11 +1744,11 @@ const ConceptCenter = () => {
</VStack>
</Box>
</VStack>
</Container>
</Box>
</Box>
{/* 主内容区域 - 深色主题 */}
<Container maxW="container.xl" py={10} position="relative" zIndex={1}>
{/* 主内容区域 - padding 由 MainLayout 统一设置 */}
<Box py={10} position="relative" zIndex={1}>
<Box mb={6}>
<DateSelector />
</Box>
@@ -2102,7 +2103,7 @@ const ConceptCenter = () => {
</Box>
</Box>
</Flex>
</Container>
</Box>
{/* 股票详情Modal - 复用通用组件 */}
<ConceptStocksModal

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
Box,
Container,
Flex,
Text,
Input,
@@ -480,7 +479,8 @@ const DataBrowser: React.FC = () => {
pointerEvents="none"
/>
<Container maxW="container.xl" position="relative" zIndex={1}>
{/* padding 由 MainLayout 统一设置 */}
<Box position="relative" zIndex={1}>
{/* 标题区域 */}
<MotionBox
initial={{ opacity: 0, y: -20 }}
@@ -866,7 +866,7 @@ const DataBrowser: React.FC = () => {
</Card>
</MotionBox>
</Flex>
</Container>
</Box>
{/* 指标数据详情模态框 */}
{selectedMetric && (

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import { useSelector } from 'react-redux';
import {
Box,
Button,
@@ -39,6 +40,7 @@ import CitedContent from '../../../components/Citation/CitedContent';
import { logger } from '../../../utils/logger';
import { getApiBase } from '../../../utils/apiConfig';
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
import { selectSubscriptionInfo } from '../../../store/slices/subscriptionSlice';
// 节点样式配置 - 完全复刻Flask版本
const NODE_STYLES = {
@@ -460,6 +462,10 @@ function getSankeyOption(data) {
}
const TransmissionChainAnalysis = ({ eventId }) => {
// 获取订阅信息,用于判断会员是否过期
const subscriptionInfo = useSelector(selectSubscriptionInfo);
const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active;
// 状态管理
const [graphData, setGraphData] = useState(null);
const [sankeyData, setSankeyData] = useState(null);
@@ -474,7 +480,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
const [stats, setStats] = useState({
totalNodes: 0,
involvedIndustries: 0,
relatedCompanies: 0,
relatedCompanies: 0,
positiveImpact: 0,
negativeImpact: 0,
circularEffect: 0
@@ -514,9 +520,18 @@ const TransmissionChainAnalysis = ({ eventId }) => {
}
}, [graphData]);
// 加载数据
// 加载数据 - 如果会员过期则跳过 API 请求
useEffect(() => {
async function fetchData() {
// 会员已过期,不发起 API 请求
if (isSubscriptionExpired) {
logger.debug('TransmissionChain', '会员已过期,跳过传导链数据加载', { eventId });
setLoading(false);
setGraphData(null);
setSankeyData(null);
return;
}
setLoading(true);
setError(null);
try {
@@ -563,7 +578,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
if (eventId) {
fetchData();
}
}, [eventId]);
}, [eventId, isSubscriptionExpired]);
// BFS路径查找 - 完全复刻Flask版本
function findPath(nodes, edges, fromId, toId) {

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState, useEffect, useRef } from 'react';
import React, { useMemo } from 'react';
import {
Box,
Card,
@@ -39,21 +39,6 @@ import {
import { getFormattedTextProps } from '../../../utils/textUtils';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import RiskDisclaimer from '../../../components/RiskDisclaimer';
import {
BarChart, Bar,
PieChart, Pie, Cell,
XAxis, YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
Legend,
ResponsiveContainer,
Treemap,
Area, AreaChart,
} from 'recharts';
// 词云库 - 支持两种实现
import { Wordcloud } from '@visx/wordcloud';
import { scaleLog } from '@visx/scale';
import { Text as VisxText } from '@visx/text';
import ReactECharts from 'echarts-for-react';
import 'echarts-wordcloud';
// 颜色配置
@@ -65,97 +50,7 @@ const CHART_COLORS = [
// 词云颜色常量
const WORDCLOUD_COLORS = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD'];
// ==================== 词云组件实现 1: @visx/wordcloud ====================
// 使用 SVG 渲染React 18 原生支持,配置灵活
const VisxWordCloud = ({ data }) => {
const [dimensions, setDimensions] = useState({ width: 0, height: 400 });
const containerRef = useRef(null);
// 监听容器尺寸变化
useEffect(() => {
if (!containerRef.current) return;
const updateDimensions = () => {
if (containerRef.current) {
setDimensions({
width: containerRef.current.offsetWidth,
height: 400
});
}
};
updateDimensions();
const resizeObserver = new ResizeObserver(updateDimensions);
resizeObserver.observe(containerRef.current);
return () => resizeObserver.disconnect();
}, []);
if (!data || data.length === 0) {
return (
<Center h="400px">
<VStack>
<Text color="gray.500">暂无词云数据</Text>
</VStack>
</Center>
);
}
const words = data.slice(0, 100).map(item => ({
name: item.name || item.text,
value: item.value || item.count || 1
}));
// 计算字体大小比例
const fontScale = scaleLog({
domain: [
Math.min(...words.map(w => w.value)),
Math.max(...words.map(w => w.value))
],
range: [16, 80],
});
const fontSizeSetter = (datum) => fontScale(datum.value);
return (
<Box ref={containerRef} h="400px" w="100%">
{dimensions.width > 0 && (
<svg width={dimensions.width} height={dimensions.height}>
<Wordcloud
words={words}
width={dimensions.width}
height={dimensions.height}
fontSize={fontSizeSetter}
font="Microsoft YaHei, sans-serif"
padding={3}
spiral="archimedean"
rotate={0}
random={() => 0.5}
>
{(cloudWords) =>
cloudWords.map((w, i) => (
<VisxText
key={w.text}
fill={WORDCLOUD_COLORS[i % WORDCLOUD_COLORS.length]}
textAnchor="middle"
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
fontSize={w.size}
fontFamily={w.font}
fontWeight="bold"
>
{w.text}
</VisxText>
))
}
</Wordcloud>
</svg>
)}
</Box>
);
};
// ==================== 词云组件实现 2: ECharts Wordcloud ====================
// 使用 Canvas 渲染内置交互效果tooltip、emphasis配置简单
// 词云组件 - 使用 ECharts 实现
const EChartsWordCloud = ({ data }) => {
if (!data || data.length === 0) {
return (
@@ -216,9 +111,8 @@ const EChartsWordCloud = ({ data }) => {
);
};
// ==================== 词云组件包装器 ====================
// 统一接口,支持切换两种实现方式
const WordCloud = ({ data, engine = 'echarts' }) => {
// 词云组件
const WordCloud = ({ data }) => {
if (!data || data.length === 0) {
return (
<Center h="400px">
@@ -229,8 +123,150 @@ const WordCloud = ({ data, engine = 'echarts' }) => {
);
}
// 根据 engine 参数选择实现方式
return engine === 'visx' ? <VisxWordCloud data={data} /> : <EChartsWordCloud data={data} />;
return <EChartsWordCloud data={data} />;
};
// 板块分布饼图 - ECharts 实现
const SectorPieChart = ({ data }) => {
if (!data || data.length === 0) {
return (
<Center h="400px">
<Text color="gray.500">暂无数据</Text>
</Center>
);
}
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: { fontSize: 12 }
},
series: [{
type: 'pie',
radius: ['0%', '65%'],
center: ['40%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b} {d}%',
fontSize: 12
},
labelLine: {
show: true,
length: 10,
length2: 15
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
data: data.map((item, index) => ({
name: item.name,
value: item.value,
itemStyle: { color: CHART_COLORS[index % CHART_COLORS.length] }
})),
animationDuration: 800
}]
};
return (
<ReactECharts
option={option}
style={{ height: '400px', width: '100%' }}
opts={{ renderer: 'canvas' }}
/>
);
};
// 时间分布环形图 - ECharts 实现
const TimeDistributionChart = ({ data }) => {
if (!data || data.length === 0) {
return (
<Center h="300px">
<Text color="gray.500">暂无数据</Text>
</Center>
);
}
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 10,
textStyle: { fontSize: 12 }
},
series: [{
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '45%'],
avoidLabelOverlap: true,
padAngle: 3,
itemStyle: {
borderRadius: 6,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'outside',
formatter: '{b}\n{c}只',
fontSize: 11
},
labelLine: {
show: true,
length: 8,
length2: 12
},
emphasis: {
label: {
show: true,
fontSize: 13,
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
data: data.map(item => ({
name: item.name,
value: item.value,
itemStyle: { color: item.color }
})),
animationDuration: 800
}]
};
return (
<ReactECharts
option={option}
style={{ height: '300px', width: '100%' }}
opts={{ renderer: 'canvas' }}
/>
);
};
// 板块热力图组件
@@ -592,27 +628,7 @@ export const DataAnalysis = ({ dailyData, wordCloudData, totalStocks, dateStr })
</TabPanel>
<TabPanel>
<ResponsiveContainer width="100%" height={400}>
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
outerRadius={120}
fill="#8884d8"
dataKey="value"
animationBegin={0}
animationDuration={800}
>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
))}
</Pie>
<RechartsTooltip />
</PieChart>
</ResponsiveContainer>
<SectorPieChart data={pieData} />
</TabPanel>
<TabPanel>
@@ -641,25 +657,7 @@ export const DataAnalysis = ({ dailyData, wordCloudData, totalStocks, dateStr })
<TabPanel>
<VStack spacing={4}>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={timeDistributionData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
fill="#8884d8"
paddingAngle={5}
dataKey="value"
>
{timeDistributionData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<RechartsTooltip />
</PieChart>
</ResponsiveContainer>
<TimeDistributionChart data={timeDistributionData} />
<SimpleGrid columns={3} spacing={4} w="full">
{timeDistributionData.map((item, index) => (
<Stat key={index} textAlign="center">

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
VStack,
HStack,
Heading,
@@ -263,8 +262,7 @@ export default function LimitAnalyse() {
{/* 导航栏已由 MainLayout 提供 */}
{/* 顶部Header */}
<Box bgGradient="linear(to-br, blue.500, purple.600)" color="white" py={8}>
<Container maxW="container.xl">
<Box bgGradient="linear(to-br, blue.500, purple.600)" color="white" py={8} px={6} borderRadius="xl">
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6} alignItems="stretch">
{/* 左侧:标题置顶,注释与图例贴底 */}
<Flex direction="column" minH="420px" justify="space-between">
@@ -366,11 +364,10 @@ export default function LimitAnalyse() {
</CardBody>
</Card>
</SimpleGrid>
</Container>
</Box>
{/* 主内容区 */}
<Container maxW="container.xl" py={8}>
{/* 主内容区 - padding 由 MainLayout 统一设置 */}
<Box py={8}>
{/* 搜索框 */}
<AdvancedSearch onSearch={handleSearch} loading={loading} />
@@ -402,7 +399,7 @@ export default function LimitAnalyse() {
{/* 高位股统计 */}
<HighPositionStocks dateStr={dateStr} />
</Container>
</Box>
{/* 弹窗 */}
<SearchResultsModal

View File

@@ -182,7 +182,7 @@ export default function ProfilePage() {
};
return (
<Container maxW="container.xl" py={8}>
<Box py={8}>
<VStack spacing={8} align="stretch">
{/* 页面标题 */}
<HStack justify="space-between">
@@ -627,6 +627,6 @@ export default function ProfilePage() {
</VStack>
</SimpleGrid>
</VStack>
</Container>
</Box>
);
}

View File

@@ -135,7 +135,6 @@ const ProfilePage = () => {
return (
<Box bg={forumColors.background.main} minH="100vh" py="8">
<Container maxW="container.xl">
{/* 用户信息头部 */}
<Card
bg={forumColors.background.card}
@@ -375,7 +374,6 @@ const ProfilePage = () => {
</Tabs>
</CardBody>
</Card>
</Container>
</Box>
);
};

View File

@@ -2,7 +2,6 @@
import React, { useState } from 'react';
import {
Box,
Container,
VStack,
HStack,
Text,
@@ -219,7 +218,7 @@ export default function SettingsPage() {
};
return (
<Container maxW="container.xl" py={8}>
<Box py={8}>
<VStack spacing={8} align="stretch">
{/* 页面标题 */}
<Heading size="lg" color={headingColor}>账户设置</Heading>
@@ -543,6 +542,6 @@ export default function SettingsPage() {
</ModalContent>
</Modal>
</Container>
</Box>
);
}

View File

@@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom';
import { getApiBase } from '@utils/apiConfig';
import {
Box,
Container,
Heading,
Text,
Input,
@@ -633,6 +632,7 @@ const StockOverview = () => {
pt={{ base: 20, md: 24 }}
pb={{ base: 16, md: 20 }}
borderBottom={`1px solid rgba(139, 92, 246, 0.3)`}
borderRadius="xl"
zIndex={1}
>
{/* 背景装饰 */}
@@ -648,7 +648,7 @@ const StockOverview = () => {
filter="blur(60px)"
/>
<Container maxW="container.xl" position="relative">
<Box px={6} position="relative">
<VStack spacing={8} align="center">
<VStack spacing={4} textAlign="center" maxW="3xl">
<HStack spacing={3}>
@@ -855,11 +855,11 @@ const StockOverview = () => {
</Stat>
</SimpleGrid>
</VStack>
</Container>
</Box>
</Box>
{/* 主内容区 */}
<Container maxW="container.xl" py={10} position="relative" zIndex={1}>
<Box py={10} px={6} position="relative" zIndex={1}>
{/* 日期选择器 */}
<Box mb={6}>
<Flex align="center" gap={4} flexWrap="wrap">
@@ -1195,7 +1195,7 @@ const StockOverview = () => {
)}
</Card>
</Box>
</Container>
</Box>
{/* 个股列表弹窗 */}
<ConceptStocksModal

View File

@@ -6,7 +6,6 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Heading,
Text,
HStack,
@@ -157,7 +156,6 @@ const PostDetail = () => {
return (
<Box minH="100vh" bg={forumColors.background.main} pt="80px" pb="20">
<Container maxW="container.xl">
{/* 返回按钮 */}
<Button
leftIcon={<ArrowLeft size={18} />}
@@ -374,7 +372,6 @@ const PostDetail = () => {
</MotionBox>
</Box>
</SimpleGrid>
</Container>
{/* 图片预览弹窗 */}
<ImagePreviewModal

View File

@@ -6,7 +6,6 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Heading,
Text,
Button,
@@ -224,7 +223,6 @@ const PredictionTopicDetail = () => {
return (
<Box minH="100vh" bg={forumColors.background.main} pt={LAYOUT_SIZE.navbarHeight} pb={{ base: "6", md: "20" }}>
<Container maxW="container.xl" px={{ base: "3", sm: "4", md: "6" }}>
{/* 头部:返回按钮 */}
<Button
variant="ghost"
@@ -627,7 +625,6 @@ const PredictionTopicDetail = () => {
</MotionBox>
</Box>
</SimpleGrid>
</Container>
{/* 交易模态框 */}
<TradeModal

View File

@@ -6,7 +6,6 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Heading,
Text,
Button,
@@ -154,7 +153,8 @@ const ValueForum = () => {
pt="80px"
pb="20"
>
<Container maxW="container.xl">
{/* padding 由 MainLayout 统一设置 */}
<Box>
{/* 顶部横幅 */}
<MotionBox
initial={{ opacity: 0, y: -20 }}
@@ -469,7 +469,7 @@ const ValueForum = () => {
</TabPanel>
</TabPanels>
</Tabs>
</Container>
</Box>
{/* 发帖模态框 */}
<CreatePostModal