Compare commits

...

4 Commits

Author SHA1 Message Date
zdl
4274341ed5 feat: 动态跟踪添加新闻动态二级 Tab
- 添加 Tabs 结构支持二级 Tab 扩展
- Tab1: 新闻动态(复用 NewsEventsTab 组件)
- 实现 loadNewsEvents 数据加载逻辑
- 支持搜索和分页功能
- 自动获取股票名称用于新闻搜索

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 18:46:37 +08:00
zdl
40f6eaced6 refactor: 移除暗色模式相关代码,使用固定浅色主题
- DeepAnalysisTab: 移除 useColorModeValue,使用固定颜色值
- NewsEventsTab: 移除 useColorModeValue,简化 hover 颜色
- FinancialPanorama: 移除 useColorMode/useColorModeValue
- MarketDataView: 移除 dark 主题配置,简化颜色逻辑
- StockQuoteCard: 移除 useColorModeValue,使用固定颜色

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 18:46:30 +08:00
zdl
2dd7dd755a refactor: Company 页面一级 Tab 重构为 6 个
- 新增深度分析 Tab(从 CompanyOverview 提取为独立组件)
- 新增动态跟踪 Tab(占位组件,后续添加内容)
- Tab 顺序:公司概览 | 深度分析 | 股票行情 | 财务全景 | 盈利预测 | 动态跟踪
- 简化 CompanyOverview:移除内部 Tabs,只保留头部卡片 + 基本信息
- DeepAnalysis 组件独立管理深度分析数据加载(3个接口)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 17:52:23 +08:00
zdl
04ce16df56 perf: CompanyOverview Tab 懒加载优化
- 拆分 loadData 为 loadBasicInfoData 和 loadDeepAnalysisData
- 首次加载仅请求 9 个基本信息接口(原 12 个)
- 深度分析 3 个接口切换 Tab 时按需加载
- 新闻动态 1 个接口切换 Tab 时按需加载
- 调整 Tab 顺序:基本信息 → 深度分析 → 新闻动态
- stockCode 变更时重置 Tab 状态和数据

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 17:37:11 +08:00
12 changed files with 527 additions and 496 deletions

View File

@@ -155,6 +155,7 @@ const BasicInfoTab = ({
branches = [], branches = [],
disclosureSchedule = [], disclosureSchedule = [],
cardBg, cardBg,
loading = false,
}) => { }) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedAnnouncement, setSelectedAnnouncement] = React.useState(null); const [selectedAnnouncement, setSelectedAnnouncement] = React.useState(null);

View File

@@ -20,7 +20,6 @@ import {
Tab, Tab,
TabPanel, TabPanel,
Button, Button,
useColorModeValue,
Tag, Tag,
TagLabel, TagLabel,
Icon, Icon,
@@ -194,7 +193,7 @@ const ScoreBar = ({ label, score, icon }) => {
// 业务结构树形图组件 // 业务结构树形图组件
const BusinessTreeItem = ({ business, depth = 0 }) => { const BusinessTreeItem = ({ business, depth = 0 }) => {
const bgColor = useColorModeValue("gray.50", "gray.700"); const bgColor = "gray.50";
return ( return (
<Box <Box
@@ -286,11 +285,8 @@ const ValueChainNodeCard = ({ node, isCompany = false, level = 0 }) => {
}; };
const colorScheme = getColorScheme(); const colorScheme = getColorScheme();
const bgColor = useColorModeValue(`${colorScheme}.50`, `${colorScheme}.900`); const bgColor = `${colorScheme}.50`;
const borderColor = useColorModeValue( const borderColor = `${colorScheme}.200`;
`${colorScheme}.200`,
`${colorScheme}.600`
);
const getNodeTypeIcon = (type) => { const getNodeTypeIcon = (type) => {
const icons = { const icons = {
@@ -700,8 +696,8 @@ const KeyFactorCard = ({ factor }) => {
mixed: "yellow", mixed: "yellow",
}[factor.impact_direction] || "gray"; }[factor.impact_direction] || "gray";
const bgColor = useColorModeValue("white", "gray.800"); const bgColor = "white";
const borderColor = useColorModeValue("gray.200", "gray.600"); const borderColor = "gray.200";
return ( return (
<Card bg={bgColor} borderColor={borderColor} size="sm"> <Card bg={bgColor} borderColor={borderColor} size="sm">
@@ -769,9 +765,9 @@ const TimelineComponent = ({ events }) => {
const [selectedEvent, setSelectedEvent] = useState(null); const [selectedEvent, setSelectedEvent] = useState(null);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
// 颜色模式值需要在组件顶层调用 // 背景颜色
const positiveBgColor = useColorModeValue("red.50", "red.900"); const positiveBgColor = "red.50";
const negativeBgColor = useColorModeValue("green.50", "green.900"); const negativeBgColor = "green.50";
const handleEventClick = (event) => { const handleEventClick = (event) => {
setSelectedEvent(event); setSelectedEvent(event);
@@ -1137,10 +1133,10 @@ const DeepAnalysisTab = ({
expandedSegments, expandedSegments,
onToggleSegment, onToggleSegment,
}) => { }) => {
const blueBg = useColorModeValue("blue.50", "blue.900"); const blueBg = "blue.50";
const greenBg = useColorModeValue("green.50", "green.900"); const greenBg = "green.50";
const purpleBg = useColorModeValue("purple.50", "purple.900"); const purpleBg = "purple.50";
const orangeBg = useColorModeValue("orange.50", "orange.900"); const orangeBg = "orange.50";
if (loading) { if (loading) {
return ( return (

View File

@@ -18,7 +18,6 @@ import {
Tag, Tag,
Center, Center,
Spinner, Spinner,
useColorModeValue,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { SearchIcon } from "@chakra-ui/icons"; import { SearchIcon } from "@chakra-ui/icons";
import { import {
@@ -67,9 +66,6 @@ const NewsEventsTab = ({
onPageChange, onPageChange,
cardBg, cardBg,
}) => { }) => {
// 颜色模式值需要在组件顶层调用
const hoverBg = useColorModeValue("gray.50", "gray.700");
// 事件类型图标映射 // 事件类型图标映射
const getEventTypeIcon = (eventType) => { const getEventTypeIcon = (eventType) => {
const iconMap = { const iconMap = {
@@ -233,7 +229,7 @@ const NewsEventsTab = ({
key={event.id || idx} key={event.id || idx}
variant="outline" variant="outline"
_hover={{ _hover={{
bg: hoverBg, bg: "gray.50",
shadow: "md", shadow: "md",
borderColor: "blue.300", borderColor: "blue.300",
}} }}

View File

@@ -1,5 +1,5 @@
// src/views/Company/components/CompanyOverview/index.js // src/views/Company/components/CompanyOverview/index.js
// 公司概览主组件 - 状态管理 + Tab 容器 // 公司概览 - 头部卡片 + 基本信息
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { import {
@@ -15,19 +15,12 @@ import {
Divider, Divider,
Spinner, Spinner,
Center, Center,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
useColorModeValue,
Icon, Icon,
Grid, Grid,
GridItem, GridItem,
Stat, Stat,
StatLabel, StatLabel,
StatNumber, StatNumber,
Container,
Circle, Circle,
Link, Link,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
@@ -42,9 +35,6 @@ import {
FaEnvelope, FaEnvelope,
FaPhone, FaPhone,
FaCrown, FaCrown,
FaBrain,
FaInfoCircle,
FaNewspaper,
} from "react-icons/fa"; } from "react-icons/fa";
import { ExternalLinkIcon } from "@chakra-ui/icons"; import { ExternalLinkIcon } from "@chakra-ui/icons";
@@ -53,9 +43,7 @@ import { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig"; import { getApiBase } from "@utils/apiConfig";
// 子组件 // 子组件
import DeepAnalysisTab from "./DeepAnalysisTab";
import BasicInfoTab from "./BasicInfoTab"; import BasicInfoTab from "./BasicInfoTab";
import NewsEventsTab from "./NewsEventsTab";
// API配置 // API配置
const API_BASE_URL = getApiBase(); const API_BASE_URL = getApiBase();
@@ -76,83 +64,51 @@ const formatUtils = {
}, },
}; };
// 主组件 /**
const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => { * 公司概览组件
*
* 功能:
* - 显示公司头部信息卡片
* - 显示基本信息(股权结构、管理层、公告等)
*
* @param {Object} props
* @param {string} props.stockCode - 股票代码
*/
const CompanyOverview = ({ stockCode: propStockCode }) => {
const [stockCode, setStockCode] = useState(propStockCode || "000001"); const [stockCode, setStockCode] = useState(propStockCode || "000001");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [dataLoaded, setDataLoaded] = useState(false);
// 监听 props 中的 stockCode 变化 // 监听 props 中的 stockCode 变化
useEffect(() => { useEffect(() => {
if (propStockCode && propStockCode !== stockCode) { if (propStockCode && propStockCode !== stockCode) {
setStockCode(propStockCode); setStockCode(propStockCode);
setDataLoaded(false);
} }
}, [propStockCode, stockCode]); }, [propStockCode, stockCode]);
// 企业深度分析数据 // 基本信息数据
const [comprehensiveData, setComprehensiveData] = useState(null);
const [valueChainData, setValueChainData] = useState(null);
const [keyFactorsData, setKeyFactorsData] = useState(null);
// 股票概览数据
const [basicInfo, setBasicInfo] = useState(null); const [basicInfo, setBasicInfo] = useState(null);
const [actualControl, setActualControl] = useState([]); const [actualControl, setActualControl] = useState([]);
const [concentration, setConcentration] = useState([]); const [concentration, setConcentration] = useState([]);
const [management, setManagement] = useState([]); const [management, setManagement] = useState([]);
const [topCirculationShareholders, setTopCirculationShareholders] = useState( const [topCirculationShareholders, setTopCirculationShareholders] = useState([]);
[]
);
const [topShareholders, setTopShareholders] = useState([]); const [topShareholders, setTopShareholders] = useState([]);
const [branches, setBranches] = useState([]); const [branches, setBranches] = useState([]);
const [announcements, setAnnouncements] = useState([]); const [announcements, setAnnouncements] = useState([]);
const [disclosureSchedule, setDisclosureSchedule] = useState([]); const [disclosureSchedule, setDisclosureSchedule] = useState([]);
// 新闻动态数据
const [newsEvents, setNewsEvents] = useState([]);
const [newsLoading, setNewsLoading] = useState(false);
const [newsSearchQuery, setNewsSearchQuery] = useState("");
const [newsPagination, setNewsPagination] = useState({
page: 1,
per_page: 10,
total: 0,
pages: 0,
has_next: false,
has_prev: false,
});
const [_error, setError] = useState(null); const [_error, setError] = useState(null);
const bgColor = useColorModeValue("gray.50", "gray.900"); // 加载基本信息数据9个接口
const cardBg = useColorModeValue("white", "gray.800"); const loadBasicInfoData = async () => {
if (dataLoaded) return;
// 业务板块详情展开状态
const [expandedSegments, setExpandedSegments] = useState({});
// 切换业务板块展开状态
const toggleSegmentExpansion = (segmentIndex) => {
setExpandedSegments((prev) => ({
...prev,
[segmentIndex]: !prev[segmentIndex],
}));
};
// 加载数据
const loadData = async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const requests = [ const requests = [
// 深度分析数据
fetch(
`${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/company/value-chain-analysis/${stockCode}`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/company/key-factors-timeline/${stockCode}`
).then((r) => r.json()),
// 股票概览数据
fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`).then((r) => fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`).then((r) =>
r.json() r.json()
), ),
@@ -183,9 +139,6 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
]; ];
const [ const [
comprehensiveRes,
valueChainRes,
keyFactorsRes,
basicRes, basicRes,
actualRes, actualRes,
concentrationRes, concentrationRes,
@@ -197,12 +150,6 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
disclosureRes, disclosureRes,
] = await Promise.all(requests); ] = await Promise.all(requests);
// 设置深度分析数据
if (comprehensiveRes.success) setComprehensiveData(comprehensiveRes.data);
if (valueChainRes.success) setValueChainData(valueChainRes.data);
if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data);
// 设置股票概览数据
if (basicRes.success) setBasicInfo(basicRes.data); if (basicRes.success) setBasicInfo(basicRes.data);
if (actualRes.success) setActualControl(actualRes.data); if (actualRes.success) setActualControl(actualRes.data);
if (concentrationRes.success) setConcentration(concentrationRes.data); if (concentrationRes.success) setConcentration(concentrationRes.data);
@@ -213,116 +160,42 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
if (branchesRes.success) setBranches(branchesRes.data); if (branchesRes.success) setBranches(branchesRes.data);
if (announcementsRes.success) setAnnouncements(announcementsRes.data); if (announcementsRes.success) setAnnouncements(announcementsRes.data);
if (disclosureRes.success) setDisclosureSchedule(disclosureRes.data); if (disclosureRes.success) setDisclosureSchedule(disclosureRes.data);
setDataLoaded(true);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
logger.error("CompanyOverview", "loadData", err, { stockCode }); logger.error("CompanyOverview", "loadBasicInfoData", err, { stockCode });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
// 首次加载
useEffect(() => { useEffect(() => {
if (stockCode) { if (stockCode) {
loadData(); loadBasicInfoData();
} }
}, [stockCode]); }, [stockCode]);
// 加载新闻事件 if (loading && !basicInfo) {
const loadNewsEvents = async (page = 1, searchQuery = "") => {
setNewsLoading(true);
try {
const params = new URLSearchParams({
page: page.toString(),
per_page: "10",
sort: "new",
include_creator: "true",
include_stats: "true",
});
const queryText = searchQuery || basicInfo?.SECNAME || "";
if (queryText) {
params.append("q", queryText);
}
const response = await fetch(
`${API_BASE_URL}/api/events?${params.toString()}`
);
const data = await response.json();
const events = data.data?.events || data.events || [];
const pagination = data.data?.pagination || {
page: 1,
per_page: 10,
total: 0,
pages: 0,
has_next: false,
has_prev: false,
};
setNewsEvents(events);
setNewsPagination(pagination);
} catch (err) {
logger.error("CompanyOverview", "loadNewsEvents", err, {
stockCode,
searchQuery,
page,
});
setNewsEvents([]);
setNewsPagination({
page: 1,
per_page: 10,
total: 0,
pages: 0,
has_next: false,
has_prev: false,
});
} finally {
setNewsLoading(false);
}
};
// 当基本信息加载完成后,加载新闻事件
useEffect(() => {
if (basicInfo) {
loadNewsEvents(1);
}
}, [basicInfo]);
// 处理新闻搜索
const handleNewsSearch = () => {
loadNewsEvents(1, newsSearchQuery);
};
// 处理新闻分页
const handleNewsPageChange = (newPage) => {
loadNewsEvents(newPage, newsSearchQuery);
};
if (loading) {
return ( return (
<Box bg={bgColor} minH="100vh" p={4}> <Center h="300px">
<Container maxW="container.xl">
<Center h="400px">
<VStack spacing={4}> <VStack spacing={4}>
<Spinner size="xl" color="blue.500" thickness="4px" /> <Spinner size="xl" color="blue.500" thickness="4px" />
<Text>正在加载企业全景数据...</Text> <Text>正在加载公司概览数据...</Text>
</VStack> </VStack>
</Center> </Center>
</Container>
</Box>
); );
} }
return ( return (
<Box bg={bgColor} minH="100vh" py={6}>
<Container maxW="container.xl">
<VStack spacing={6} align="stretch"> <VStack spacing={6} align="stretch">
{/* 公司头部信息 - 醒目展示 */} {/* 公司头部信息卡片 */}
{basicInfo && ( {basicInfo && (
<Card <Card
bg={cardBg} bg="white"
shadow="2xl" shadow="lg"
borderTop="6px solid" borderTop="4px solid"
borderTopColor="blue.500" borderTopColor="blue.500"
> >
<CardBody> <CardBody>
@@ -330,32 +203,27 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
<GridItem colSpan={{ base: 12, lg: 8 }}> <GridItem colSpan={{ base: 12, lg: 8 }}>
<VStack align="start" spacing={4}> <VStack align="start" spacing={4}>
<HStack spacing={4}> <HStack spacing={4}>
<Circle size="70px" bg="blue.500"> <Circle size="60px" bg="blue.500">
<Icon as={FaBuilding} color="white" boxSize={10} /> <Icon as={FaBuilding} color="white" boxSize={8} />
</Circle> </Circle>
<VStack align="start" spacing={1}> <VStack align="start" spacing={1}>
<HStack> <HStack>
<Heading size="xl" color="blue.600"> <Heading size="lg" color="blue.600">
{basicInfo.ORGNAME || basicInfo.SECNAME} {basicInfo.ORGNAME || basicInfo.SECNAME}
</Heading> </Heading>
<Badge <Badge colorScheme="blue" fontSize="md" px={2} py={1}>
colorScheme="blue"
fontSize="lg"
px={3}
py={1}
>
{basicInfo.SECCODE} {basicInfo.SECCODE}
</Badge> </Badge>
</HStack> </HStack>
<HStack spacing={3}> <HStack spacing={2}>
<Badge colorScheme="purple" fontSize="sm"> <Badge colorScheme="purple" fontSize="xs">
{basicInfo.sw_industry_l1} {basicInfo.sw_industry_l1}
</Badge> </Badge>
<Badge colorScheme="orange" fontSize="sm"> <Badge colorScheme="orange" fontSize="xs">
{basicInfo.sw_industry_l2} {basicInfo.sw_industry_l2}
</Badge> </Badge>
{basicInfo.sw_industry_l3 && ( {basicInfo.sw_industry_l3 && (
<Badge colorScheme="green" fontSize="sm"> <Badge colorScheme="green" fontSize="xs">
{basicInfo.sw_industry_l3} {basicInfo.sw_industry_l3}
</Badge> </Badge>
)} )}
@@ -365,58 +233,40 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
<Divider /> <Divider />
<SimpleGrid columns={2} spacing={4} w="full"> <SimpleGrid columns={{ base: 1, md: 2 }} spacing={3} w="full">
<HStack> <HStack>
<Icon as={FaUserShield} color="gray.500" /> <Icon as={FaUserShield} color="gray.500" boxSize={4} />
<Text fontSize="sm"> <Text fontSize="sm">
<Text as="span" color="gray.500"> <Text as="span" color="gray.500">法定代表人</Text>
法定代表人 <Text as="span" fontWeight="bold">{basicInfo.legal_representative}</Text>
</Text>
<Text as="span" fontWeight="bold">
{basicInfo.legal_representative}
</Text>
</Text> </Text>
</HStack> </HStack>
<HStack> <HStack>
<Icon as={FaCrown} color="gray.500" /> <Icon as={FaCrown} color="gray.500" boxSize={4} />
<Text fontSize="sm"> <Text fontSize="sm">
<Text as="span" color="gray.500"> <Text as="span" color="gray.500">董事长</Text>
董事长 <Text as="span" fontWeight="bold">{basicInfo.chairman}</Text>
</Text>
<Text as="span" fontWeight="bold">
{basicInfo.chairman}
</Text>
</Text> </Text>
</HStack> </HStack>
<HStack> <HStack>
<Icon as={FaBriefcase} color="gray.500" /> <Icon as={FaBriefcase} color="gray.500" boxSize={4} />
<Text fontSize="sm"> <Text fontSize="sm">
<Text as="span" color="gray.500"> <Text as="span" color="gray.500">总经理</Text>
总经理 <Text as="span" fontWeight="bold">{basicInfo.general_manager}</Text>
</Text>
<Text as="span" fontWeight="bold">
{basicInfo.general_manager}
</Text>
</Text> </Text>
</HStack> </HStack>
<HStack> <HStack>
<Icon as={FaCalendarAlt} color="gray.500" /> <Icon as={FaCalendarAlt} color="gray.500" boxSize={4} />
<Text fontSize="sm"> <Text fontSize="sm">
<Text as="span" color="gray.500"> <Text as="span" color="gray.500">成立日期</Text>
成立日期 <Text as="span" fontWeight="bold">{formatUtils.formatDate(basicInfo.establish_date)}</Text>
</Text>
<Text as="span" fontWeight="bold">
{formatUtils.formatDate(basicInfo.establish_date)}
</Text>
</Text> </Text>
</HStack> </HStack>
</SimpleGrid> </SimpleGrid>
<Box> <Text fontSize="sm" color="gray.600" noOfLines={2}>
<Text fontSize="sm" color="gray.600" noOfLines={3}>
{basicInfo.company_intro} {basicInfo.company_intro}
</Text> </Text>
</Box>
</VStack> </VStack>
</GridItem> </GridItem>
@@ -424,38 +274,30 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
<Stat> <Stat>
<StatLabel>注册资本</StatLabel> <StatLabel>注册资本</StatLabel>
<StatNumber fontSize="3xl" color="blue.500"> <StatNumber fontSize="2xl" color="blue.500">
{formatUtils.formatRegisteredCapital( {formatUtils.formatRegisteredCapital(basicInfo.reg_capital)}
basicInfo.reg_capital
)}
</StatNumber> </StatNumber>
</Stat> </Stat>
<Divider /> <Divider />
<VStack align="stretch" spacing={2}> <VStack align="stretch" spacing={1}>
<HStack fontSize="sm"> <HStack fontSize="sm">
<Icon as={FaMapMarkerAlt} color="gray.500" /> <Icon as={FaMapMarkerAlt} color="gray.500" boxSize={3} />
<Text noOfLines={1}> <Text noOfLines={1}>{basicInfo.province} {basicInfo.city}</Text>
{basicInfo.province} {basicInfo.city}
</Text>
</HStack> </HStack>
<HStack fontSize="sm"> <HStack fontSize="sm">
<Icon as={FaGlobe} color="gray.500" /> <Icon as={FaGlobe} color="gray.500" boxSize={3} />
<Link <Link href={basicInfo.website} isExternal color="blue.500" noOfLines={1}>
href={basicInfo.website}
isExternal
color="blue.500"
>
{basicInfo.website} <ExternalLinkIcon mx="2px" /> {basicInfo.website} <ExternalLinkIcon mx="2px" />
</Link> </Link>
</HStack> </HStack>
<HStack fontSize="sm"> <HStack fontSize="sm">
<Icon as={FaEnvelope} color="gray.500" /> <Icon as={FaEnvelope} color="gray.500" boxSize={3} />
<Text>{basicInfo.email}</Text> <Text noOfLines={1}>{basicInfo.email}</Text>
</HStack> </HStack>
<HStack fontSize="sm"> <HStack fontSize="sm">
<Icon as={FaPhone} color="gray.500" /> <Icon as={FaPhone} color="gray.500" boxSize={3} />
<Text>{basicInfo.tel}</Text> <Text>{basicInfo.tel}</Text>
</HStack> </HStack>
</VStack> </VStack>
@@ -466,50 +308,7 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
</Card> </Card>
)} )}
{/* 主要内容区 - 分为深度分析、基本信息和新闻动态 */} {/* 基本信息内容 */}
<Tabs
variant="soft-rounded"
colorScheme="blue"
size="lg"
defaultIndex={0}
>
<TabList
bg={cardBg}
p={4}
borderRadius="lg"
shadow="md"
flexWrap="wrap"
>
<Tab fontWeight="bold">
<Icon as={FaBrain} mr={2} />
深度分析
</Tab>
<Tab fontWeight="bold">
<Icon as={FaInfoCircle} mr={2} />
基本信息
</Tab>
<Tab fontWeight="bold">
<Icon as={FaNewspaper} mr={2} />
新闻动态
</Tab>
</TabList>
<TabPanels>
{/* 深度分析标签页 */}
<TabPanel p={0} pt={6}>
<DeepAnalysisTab
comprehensiveData={comprehensiveData}
valueChainData={valueChainData}
keyFactorsData={keyFactorsData}
loading={loading}
cardBg={cardBg}
expandedSegments={expandedSegments}
onToggleSegment={toggleSegmentExpansion}
/>
</TabPanel>
{/* 基本信息标签页 */}
<TabPanel p={0} pt={6}>
<BasicInfoTab <BasicInfoTab
basicInfo={basicInfo} basicInfo={basicInfo}
actualControl={actualControl} actualControl={actualControl}
@@ -520,29 +319,11 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
announcements={announcements} announcements={announcements}
branches={branches} branches={branches}
disclosureSchedule={disclosureSchedule} disclosureSchedule={disclosureSchedule}
cardBg={cardBg} cardBg="white"
loading={loading}
/> />
</TabPanel>
{/* 新闻动态标签页 */}
<TabPanel p={0} pt={6}>
<NewsEventsTab
newsEvents={newsEvents}
newsLoading={newsLoading}
newsPagination={newsPagination}
searchQuery={newsSearchQuery}
onSearchChange={setNewsSearchQuery}
onSearch={handleNewsSearch}
onPageChange={handleNewsPageChange}
cardBg={cardBg}
/>
</TabPanel>
</TabPanels>
</Tabs>
</VStack> </VStack>
</Container>
</Box>
); );
}; };
export default CompanyAnalysisComplete; export default CompanyOverview;

View File

@@ -9,7 +9,6 @@ import {
TabPanels, TabPanels,
TabPanel, TabPanel,
Divider, Divider,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import TabNavigation from './TabNavigation'; import TabNavigation from './TabNavigation';
@@ -17,9 +16,11 @@ import { COMPANY_TABS, getTabNameByIndex } from '../../constants';
// 子组件导入Tab 内容组件) // 子组件导入Tab 内容组件)
import CompanyOverview from '../CompanyOverview'; import CompanyOverview from '../CompanyOverview';
import DeepAnalysis from '../DeepAnalysis';
import MarketDataView from '../MarketDataView'; import MarketDataView from '../MarketDataView';
import FinancialPanorama from '../FinancialPanorama'; import FinancialPanorama from '../FinancialPanorama';
import ForecastReport from '../ForecastReport'; import ForecastReport from '../ForecastReport';
import DynamicTracking from '../DynamicTracking';
/** /**
* Tab 组件映射 * Tab 组件映射
@@ -27,9 +28,11 @@ import ForecastReport from '../ForecastReport';
*/ */
const TAB_COMPONENTS = { const TAB_COMPONENTS = {
overview: CompanyOverview, overview: CompanyOverview,
analysis: DeepAnalysis,
market: MarketDataView, market: MarketDataView,
financial: FinancialPanorama, financial: FinancialPanorama,
forecast: ForecastReport, forecast: ForecastReport,
tracking: DynamicTracking,
}; };
/** /**
@@ -48,10 +51,6 @@ const TAB_COMPONENTS = {
const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => { const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
// 主题相关颜色
const tabBg = useColorModeValue('gray.50', 'gray.700');
const activeBg = useColorModeValue('blue.500', 'blue.400');
/** /**
* 处理 Tab 切换 * 处理 Tab 切换
*/ */
@@ -76,7 +75,7 @@ const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => {
onChange={handleTabChange} onChange={handleTabChange}
> >
{/* Tab 导航 */} {/* Tab 导航 */}
<TabNavigation tabBg={tabBg} activeBg={activeBg} /> <TabNavigation tabBg="gray.50" activeBg="blue.500" />
<Divider /> <Divider />

View File

@@ -0,0 +1,100 @@
// src/views/Company/components/DeepAnalysis/index.js
// 深度分析 - 独立一级 Tab 组件
import React, { useState, useEffect } from "react";
import { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig";
// 复用原有的展示组件
import DeepAnalysisTab from "../CompanyOverview/DeepAnalysisTab";
const API_BASE_URL = getApiBase();
/**
* 深度分析组件
*
* 功能:
* - 加载深度分析数据3个接口
* - 管理展开状态
* - 渲染 DeepAnalysisTab 展示组件
*
* @param {Object} props
* @param {string} props.stockCode - 股票代码
*/
const DeepAnalysis = ({ stockCode }) => {
// 数据状态
const [comprehensiveData, setComprehensiveData] = useState(null);
const [valueChainData, setValueChainData] = useState(null);
const [keyFactorsData, setKeyFactorsData] = useState(null);
const [loading, setLoading] = useState(false);
// 业务板块展开状态
const [expandedSegments, setExpandedSegments] = useState({});
// 切换业务板块展开状态
const toggleSegmentExpansion = (segmentIndex) => {
setExpandedSegments((prev) => ({
...prev,
[segmentIndex]: !prev[segmentIndex],
}));
};
// 加载深度分析数据3个接口
const loadDeepAnalysisData = async () => {
if (!stockCode) return;
setLoading(true);
try {
const requests = [
fetch(
`${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/company/value-chain-analysis/${stockCode}`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/company/key-factors-timeline/${stockCode}`
).then((r) => r.json()),
];
const [comprehensiveRes, valueChainRes, keyFactorsRes] =
await Promise.all(requests);
if (comprehensiveRes.success) setComprehensiveData(comprehensiveRes.data);
if (valueChainRes.success) setValueChainData(valueChainRes.data);
if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data);
} catch (err) {
logger.error("DeepAnalysis", "loadDeepAnalysisData", err, { stockCode });
} finally {
setLoading(false);
}
};
// stockCode 变更时重新加载数据
useEffect(() => {
if (stockCode) {
// 重置数据
setComprehensiveData(null);
setValueChainData(null);
setKeyFactorsData(null);
setExpandedSegments({});
// 加载新数据
loadDeepAnalysisData();
}
}, [stockCode]);
return (
<DeepAnalysisTab
comprehensiveData={comprehensiveData}
valueChainData={valueChainData}
keyFactorsData={keyFactorsData}
loading={loading}
cardBg="white"
expandedSegments={expandedSegments}
onToggleSegment={toggleSegmentExpansion}
/>
);
};
export default DeepAnalysis;

View File

@@ -0,0 +1,184 @@
// src/views/Company/components/DynamicTracking/index.js
// 动态跟踪 - 独立一级 Tab 组件(包含新闻动态等二级 Tab
import React, { useState, useEffect, useCallback } from "react";
import {
Box,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
} from "@chakra-ui/react";
import { FaNewspaper } from "react-icons/fa";
import { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig";
import NewsEventsTab from "../CompanyOverview/NewsEventsTab";
// API配置
const API_BASE_URL = getApiBase();
// 二级 Tab 配置
const TRACKING_TABS = [
{ key: "news", name: "新闻动态", icon: FaNewspaper },
// 后续可扩展更多二级 Tab
];
/**
* 动态跟踪组件
*
* 功能:
* - 二级 Tab 结构
* - Tab1: 新闻动态(复用 NewsEventsTab
* - 预留后续扩展
*
* @param {Object} props
* @param {string} props.stockCode - 股票代码
*/
const DynamicTracking = ({ stockCode: propStockCode }) => {
const [stockCode, setStockCode] = useState(propStockCode || "000001");
const [activeTab, setActiveTab] = useState(0);
// 新闻动态状态
const [newsEvents, setNewsEvents] = useState([]);
const [newsLoading, setNewsLoading] = useState(false);
const [newsPagination, setNewsPagination] = useState({
page: 1,
per_page: 10,
total: 0,
pages: 0,
has_next: false,
has_prev: false,
});
const [searchQuery, setSearchQuery] = useState("");
const [stockName, setStockName] = useState("");
const [dataLoaded, setDataLoaded] = useState(false);
// 监听 props 中的 stockCode 变化
useEffect(() => {
if (propStockCode && propStockCode !== stockCode) {
setStockCode(propStockCode);
setDataLoaded(false);
setNewsEvents([]);
setStockName("");
setSearchQuery("");
}
}, [propStockCode, stockCode]);
// 获取股票名称(用于搜索)
const fetchStockName = useCallback(async () => {
try {
const response = await fetch(
`${API_BASE_URL}/api/stock/${stockCode}/basic-info`
);
const result = await response.json();
if (result.success && result.data) {
const name = result.data.SECNAME || result.data.ORGNAME || stockCode;
setStockName(name);
return name;
}
return stockCode;
} catch (err) {
logger.error("DynamicTracking", "fetchStockName", err, { stockCode });
return stockCode;
}
}, [stockCode]);
// 加载新闻事件数据
const loadNewsEvents = useCallback(
async (query, page = 1) => {
setNewsLoading(true);
try {
const searchTerm = query || stockName || stockCode;
const response = await fetch(
`${API_BASE_URL}/api/events?q=${encodeURIComponent(searchTerm)}&page=${page}&per_page=10`
);
const result = await response.json();
if (result.success) {
setNewsEvents(result.data || []);
setNewsPagination({
page: result.pagination?.page || page,
per_page: result.pagination?.per_page || 10,
total: result.pagination?.total || 0,
pages: result.pagination?.pages || 0,
has_next: result.pagination?.has_next || false,
has_prev: result.pagination?.has_prev || false,
});
}
} catch (err) {
logger.error("DynamicTracking", "loadNewsEvents", err, { stockCode });
setNewsEvents([]);
} finally {
setNewsLoading(false);
}
},
[stockCode, stockName]
);
// 首次加载
useEffect(() => {
const initLoad = async () => {
if (stockCode && !dataLoaded) {
const name = await fetchStockName();
await loadNewsEvents(name, 1);
setDataLoaded(true);
}
};
initLoad();
}, [stockCode, dataLoaded, fetchStockName, loadNewsEvents]);
// 搜索处理
const handleSearchChange = (value) => {
setSearchQuery(value);
};
const handleSearch = () => {
loadNewsEvents(searchQuery || stockName, 1);
};
// 分页处理
const handlePageChange = (page) => {
loadNewsEvents(searchQuery || stockName, page);
};
return (
<Box>
<Tabs
variant="enclosed"
colorScheme="blue"
index={activeTab}
onChange={setActiveTab}
>
<TabList>
{TRACKING_TABS.map((tab) => (
<Tab key={tab.key} fontWeight="medium">
{tab.name}
</Tab>
))}
</TabList>
<TabPanels>
{/* 新闻动态 Tab */}
<TabPanel p={4}>
<NewsEventsTab
newsEvents={newsEvents}
newsLoading={newsLoading}
newsPagination={newsPagination}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
onSearch={handleSearch}
onPageChange={handlePageChange}
cardBg="white"
/>
</TabPanel>
{/* 后续可扩展更多 Tab Panel */}
</TabPanels>
</Tabs>
</Box>
);
};
export default DynamicTracking;

View File

@@ -35,7 +35,6 @@ import {
VStack, VStack,
HStack, HStack,
Divider, Divider,
useColorModeValue,
Select, Select,
Button, Button,
Tooltip, Tooltip,
@@ -60,7 +59,6 @@ import {
ButtonGroup, ButtonGroup,
Stack, Stack,
Collapse, Collapse,
useColorMode,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {
ChevronDownIcon, ChevronDownIcon,
@@ -101,14 +99,13 @@ const FinancialPanorama = ({ stockCode: propStockCode }) => {
const [modalContent, setModalContent] = useState(null); const [modalContent, setModalContent] = useState(null);
const [expandedRows, setExpandedRows] = useState({}); const [expandedRows, setExpandedRows] = useState({});
const toast = useToast(); const toast = useToast();
const { colorMode } = useColorMode();
// 颜色配置(中国市场:红涨绿跌) // 颜色配置(中国市场:红涨绿跌)
const bgColor = useColorModeValue('white', 'gray.800'); const bgColor = 'white';
const borderColor = useColorModeValue('gray.200', 'gray.600'); const borderColor = 'gray.200';
const hoverBg = useColorModeValue('gray.50', 'gray.700'); const hoverBg = 'gray.50';
const positiveColor = useColorModeValue('red.500', 'red.400'); // 红涨 const positiveColor = 'red.500'; // 红涨
const negativeColor = useColorModeValue('green.500', 'green.400'); // 绿跌 const negativeColor = 'green.500'; // 绿跌
// 加载所有财务数据 // 加载所有财务数据
const loadFinancialData = async () => { const loadFinancialData = async () => {
@@ -492,7 +489,7 @@ const FinancialPanorama = ({ stockCode: propStockCode }) => {
<React.Fragment key={section.key}> <React.Fragment key={section.key}>
{section.title !== '资产总计' && section.title !== '负债合计' && ( {section.title !== '资产总计' && section.title !== '负债合计' && (
<Tr <Tr
bg={useColorModeValue('gray.50', 'gray.700')} bg="gray.50"
cursor="pointer" cursor="pointer"
onClick={() => toggleSection(section.key)} onClick={() => toggleSection(section.key)}
> >
@@ -515,7 +512,7 @@ const FinancialPanorama = ({ stockCode: propStockCode }) => {
key={metric.key} key={metric.key}
_hover={{ bg: hoverBg, cursor: 'pointer' }} _hover={{ bg: hoverBg, cursor: 'pointer' }}
onClick={() => showMetricChart(metric.name, metric.key, balanceSheet, metric.path)} onClick={() => showMetricChart(metric.name, metric.key, balanceSheet, metric.path)}
bg={metric.isTotal ? useColorModeValue('blue.50', 'blue.900') : 'transparent'} bg={metric.isTotal ? 'blue.50' : 'transparent'}
> >
<Td position="sticky" left={0} bg={bgColor} zIndex={1}> <Td position="sticky" left={0} bg={bgColor} zIndex={1}>
<HStack spacing={2}> <HStack spacing={2}>
@@ -733,7 +730,7 @@ const FinancialPanorama = ({ stockCode: propStockCode }) => {
const renderSection = (section) => ( const renderSection = (section) => (
<React.Fragment key={section.key}> <React.Fragment key={section.key}>
<Tr <Tr
bg={useColorModeValue('gray.50', 'gray.700')} bg="gray.50"
cursor="pointer" cursor="pointer"
onClick={() => toggleSection(section.key)} onClick={() => toggleSection(section.key)}
> >
@@ -755,8 +752,8 @@ const FinancialPanorama = ({ stockCode: propStockCode }) => {
key={metric.key} key={metric.key}
_hover={{ bg: hoverBg, cursor: 'pointer' }} _hover={{ bg: hoverBg, cursor: 'pointer' }}
onClick={() => showMetricChart(metric.name, metric.key, incomeStatement, metric.path)} onClick={() => showMetricChart(metric.name, metric.key, incomeStatement, metric.path)}
bg={metric.isTotal ? useColorModeValue('blue.50', 'blue.900') : bg={metric.isTotal ? 'blue.50' :
metric.isSubtotal ? useColorModeValue('orange.50', 'orange.900') : 'transparent'} metric.isSubtotal ? 'orange.50' : 'transparent'}
> >
<Td position="sticky" left={0} bg={bgColor} zIndex={1}> <Td position="sticky" left={0} bg={bgColor} zIndex={1}>
<HStack spacing={2}> <HStack spacing={2}>
@@ -1268,7 +1265,7 @@ const FinancialPanorama = ({ stockCode: propStockCode }) => {
{ label: '资产负债率', value: financialMetrics[0].solvency?.asset_liability_ratio, format: 'percent' }, { label: '资产负债率', value: financialMetrics[0].solvency?.asset_liability_ratio, format: 'percent' },
{ label: '研发费用率', value: financialMetrics[0].expense_ratios?.rd_expense_ratio, format: 'percent' }, { label: '研发费用率', value: financialMetrics[0].expense_ratios?.rd_expense_ratio, format: 'percent' },
].map((item, idx) => ( ].map((item, idx) => (
<Box key={idx} p={3} borderRadius="md" bg={useColorModeValue('gray.50', 'gray.700')}> <Box key={idx} p={3} borderRadius="md" bg="gray.50">
<Text fontSize="xs" color="gray.500">{item.label}</Text> <Text fontSize="xs" color="gray.500">{item.label}</Text>
<Text fontSize="lg" fontWeight="bold"> <Text fontSize="lg" fontWeight="bold">
{item.format === 'percent' ? {item.format === 'percent' ?

View File

@@ -36,7 +36,6 @@ import {
VStack, VStack,
HStack, HStack,
Divider, Divider,
useColorModeValue,
Select, Select,
Button, Button,
Tooltip, Tooltip,
@@ -60,7 +59,6 @@ import {
GridItem, GridItem,
ButtonGroup, ButtonGroup,
Stack, Stack,
useColorMode,
Icon, Icon,
InputGroup, InputGroup,
InputLeftElement, InputLeftElement,
@@ -121,25 +119,6 @@ const themes = {
border: '#CBD5E0', border: '#CBD5E0',
chartBg: '#FFFFFF', chartBg: '#FFFFFF',
}, },
dark: {
// 夜间模式 - 黑+金
primary: '#FFD700',
primaryDark: '#FFA500',
secondary: '#1A1A1A',
secondaryDark: '#000000',
success: '#FF4444', // 涨 - 红色
danger: '#00C851', // 跌 - 绿色
warning: '#FFA500',
info: '#00BFFF',
bgMain: '#0A0A0A',
bgCard: '#141414',
bgDark: '#000000',
textPrimary: '#FFFFFF',
textSecondary: '#FFD700',
textMuted: '#999999',
border: '#333333',
chartBg: '#141414',
}
}; };
// API服务 // API服务
@@ -236,7 +215,7 @@ const ThemedCard = ({ children, theme, ...props }) => {
}; };
// Markdown渲染组件 // Markdown渲染组件
const MarkdownRenderer = ({ children, theme, colorMode }) => { const MarkdownRenderer = ({ children, theme }) => {
return ( return (
<Box <Box
color={theme.textPrimary} color={theme.textPrimary}
@@ -269,7 +248,7 @@ const MarkdownRenderer = ({ children, theme, colorMode }) => {
fontStyle: 'italic' fontStyle: 'italic'
}, },
'& code': { '& code': {
backgroundColor: colorMode === 'light' ? 'rgba(0,0,0,0.05)' : 'rgba(255,255,255,0.1)', backgroundColor: 'rgba(0,0,0,0.05)',
padding: '2px 4px', padding: '2px 4px',
borderRadius: '4px', borderRadius: '4px',
fontSize: '0.9em' fontSize: '0.9em'
@@ -290,13 +269,12 @@ const MarkdownRenderer = ({ children, theme, colorMode }) => {
// 主组件 // 主组件
const MarketDataView = ({ stockCode: propStockCode }) => { const MarketDataView = ({ stockCode: propStockCode }) => {
const { colorMode } = useColorMode();
const toast = useToast(); const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const [modalContent, setModalContent] = useState(null); const [modalContent, setModalContent] = useState(null);
// 获取当前主题 // 获取当前主题
const theme = colorMode === 'light' ? themes.light : themes.dark; const theme = themes.light;
// 状态管理 // 状态管理
const [stockCode, setStockCode] = useState(propStockCode || '600000'); const [stockCode, setStockCode] = useState(propStockCode || '600000');
@@ -464,7 +442,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
opacity: 0.8 opacity: 0.8
} }
}, },
backgroundColor: colorMode === 'light' ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.8)', backgroundColor: 'rgba(255,255,255,0.9)',
borderColor: theme.primary, borderColor: theme.primary,
borderWidth: 1, borderWidth: 1,
textStyle: { textStyle: {
@@ -682,7 +660,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
axisPointer: { type: 'cross' }, axisPointer: { type: 'cross' },
backgroundColor: colorMode === 'light' ? 'rgba(255,255,255,0.95)' : 'rgba(0,0,0,0.85)', backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: theme.primary, borderColor: theme.primary,
borderWidth: 1, borderWidth: 1,
textStyle: { textStyle: {
@@ -889,7 +867,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
backgroundColor: colorMode === 'light' ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.8)', backgroundColor: 'rgba(255,255,255,0.9)',
borderColor: theme.primary, borderColor: theme.primary,
borderWidth: 1, borderWidth: 1,
textStyle: { textStyle: {
@@ -1022,7 +1000,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
backgroundColor: colorMode === 'light' ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.8)', backgroundColor: 'rgba(255,255,255,0.9)',
borderColor: theme.primary, borderColor: theme.primary,
borderWidth: 1, borderWidth: 1,
textStyle: { textStyle: {
@@ -1130,7 +1108,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<Heading size="xl" color={theme.textSecondary}> <Heading size="xl" color={theme.textSecondary}>
{summary.stock_name} {summary.stock_name}
</Heading> </Heading>
<Badge colorScheme={colorMode === 'light' ? 'blue' : 'yellow'} fontSize="lg"> <Badge colorScheme={'blue'} fontSize="lg">
{summary.stock_code} {summary.stock_code}
</Badge> </Badge>
</HStack> </HStack>
@@ -1236,7 +1214,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
) : ( ) : (
<Tabs <Tabs
variant="soft-rounded" variant="soft-rounded"
colorScheme={colorMode === 'light' ? 'blue' : 'yellow'} colorScheme={'blue'}
index={activeTab} index={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
> >
@@ -1249,31 +1227,31 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
> >
<HStack justify="space-between" align="center" spacing={4}> <HStack justify="space-between" align="center" spacing={4}>
<TabList overflowX="auto" border="none" flex="1"> <TabList overflowX="auto" border="none" flex="1">
<Tab color={theme.textMuted} _selected={{ color: colorMode === 'light' ? 'white' : theme.secondary, bg: theme.primary }} fontSize="sm" px={3}> <Tab color={theme.textMuted} _selected={{ color: 'white', bg: theme.primary }} fontSize="sm" px={3}>
<HStack spacing={1}> <HStack spacing={1}>
<Icon as={ChevronUpIcon} boxSize={4} /> <Icon as={ChevronUpIcon} boxSize={4} />
<Text>交易数据</Text> <Text>交易数据</Text>
</HStack> </HStack>
</Tab> </Tab>
<Tab color={theme.textMuted} _selected={{ color: colorMode === 'light' ? 'white' : theme.secondary, bg: theme.primary }} fontSize="sm" px={3}> <Tab color={theme.textMuted} _selected={{ color: 'white', bg: theme.primary }} fontSize="sm" px={3}>
<HStack spacing={1}> <HStack spacing={1}>
<Icon as={UnlockIcon} boxSize={4} /> <Icon as={UnlockIcon} boxSize={4} />
<Text>融资融券</Text> <Text>融资融券</Text>
</HStack> </HStack>
</Tab> </Tab>
<Tab color={theme.textMuted} _selected={{ color: colorMode === 'light' ? 'white' : theme.secondary, bg: theme.primary }} fontSize="sm" px={3}> <Tab color={theme.textMuted} _selected={{ color: 'white', bg: theme.primary }} fontSize="sm" px={3}>
<HStack spacing={1}> <HStack spacing={1}>
<Icon as={ArrowUpIcon} boxSize={4} /> <Icon as={ArrowUpIcon} boxSize={4} />
<Text>大宗交易</Text> <Text>大宗交易</Text>
</HStack> </HStack>
</Tab> </Tab>
<Tab color={theme.textMuted} _selected={{ color: colorMode === 'light' ? 'white' : theme.secondary, bg: theme.primary }} fontSize="sm" px={3}> <Tab color={theme.textMuted} _selected={{ color: 'white', bg: theme.primary }} fontSize="sm" px={3}>
<HStack spacing={1}> <HStack spacing={1}>
<Icon as={StarIcon} boxSize={4} /> <Icon as={StarIcon} boxSize={4} />
<Text>龙虎榜</Text> <Text>龙虎榜</Text>
</HStack> </HStack>
</Tab> </Tab>
<Tab color={theme.textMuted} _selected={{ color: colorMode === 'light' ? 'white' : theme.secondary, bg: theme.primary }} fontSize="sm" px={3}> <Tab color={theme.textMuted} _selected={{ color: 'white', bg: theme.primary }} fontSize="sm" px={3}>
<HStack spacing={1}> <HStack spacing={1}>
<Icon as={LockIcon} boxSize={4} /> <Icon as={LockIcon} boxSize={4} />
<Text>股权质押</Text> <Text>股权质押</Text>
@@ -1301,7 +1279,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<Button <Button
leftIcon={<RepeatIcon />} leftIcon={<RepeatIcon />}
variant="outline" variant="outline"
colorScheme={colorMode === 'light' ? 'blue' : 'yellow'} colorScheme={'blue'}
onClick={loadMarketData} onClick={loadMarketData}
isLoading={loading} isLoading={loading}
size="sm" size="sm"
@@ -1323,7 +1301,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<ReactECharts <ReactECharts
option={getKLineOption()} option={getKLineOption()}
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
theme={colorMode === 'light' ? 'light' : 'dark'} theme={'light'}
onEvents={{ onEvents={{
'click': (params) => { 'click': (params) => {
if (params.seriesName === '涨幅分析' && params.data) { if (params.seriesName === '涨幅分析' && params.data) {
@@ -1343,7 +1321,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
</Box> </Box>
{analysis.main_business && ( {analysis.main_business && (
<Box p={4} bg={colorMode === 'light' ? 'gray.50' : 'gray.900'} borderRadius="md"> <Box p={4} bg={'gray.50'} borderRadius="md">
<Heading size="sm" mb={2} color={theme.primary}>主营业务</Heading> <Heading size="sm" mb={2} color={theme.primary}>主营业务</Heading>
<Text color={theme.textPrimary}>{analysis.main_business}</Text> <Text color={theme.textPrimary}>{analysis.main_business}</Text>
</Box> </Box>
@@ -1351,18 +1329,18 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
{analysis.rise_reason_detail && ( {analysis.rise_reason_detail && (
<Box p={4} bg={colorMode === 'light' ? 'purple.50' : 'purple.900'} borderRadius="md"> <Box p={4} bg={'purple.50'} borderRadius="md">
<Heading size="sm" mb={2} color={theme.primary}>详细分析</Heading> <Heading size="sm" mb={2} color={theme.primary}>详细分析</Heading>
<MarkdownRenderer theme={theme} colorMode={colorMode}> <MarkdownRenderer theme={theme}>
{analysis.rise_reason_detail} {analysis.rise_reason_detail}
</MarkdownRenderer> </MarkdownRenderer>
</Box> </Box>
)} )}
{analysis.announcements && analysis.announcements !== '[]' && ( {analysis.announcements && analysis.announcements !== '[]' && (
<Box p={4} bg={colorMode === 'light' ? 'orange.50' : 'orange.900'} borderRadius="md"> <Box p={4} bg={'orange.50'} borderRadius="md">
<Heading size="sm" mb={2} color={theme.primary}>相关公告</Heading> <Heading size="sm" mb={2} color={theme.primary}>相关公告</Heading>
<MarkdownRenderer theme={theme} colorMode={colorMode}> <MarkdownRenderer theme={theme}>
{analysis.announcements} {analysis.announcements}
</MarkdownRenderer> </MarkdownRenderer>
</Box> </Box>
@@ -1370,7 +1348,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
{/* 研报引用展示 */} {/* 研报引用展示 */}
{analysis.verification_reports && analysis.verification_reports.length > 0 && ( {analysis.verification_reports && analysis.verification_reports.length > 0 && (
<Box p={4} bg={colorMode === 'light' ? 'blue.50' : 'blue.900'} borderRadius="md"> <Box p={4} bg={'blue.50'} borderRadius="md">
<Heading size="sm" mb={3} color={theme.primary}> <Heading size="sm" mb={3} color={theme.primary}>
<HStack spacing={2}> <HStack spacing={2}>
<Icon as={ExternalLinkIcon} /> <Icon as={ExternalLinkIcon} />
@@ -1382,7 +1360,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<Box <Box
key={reportIdx} key={reportIdx}
p={3} p={3}
bg={colorMode === 'light' ? 'white' : 'gray.800'} bg={'white'}
borderRadius="md" borderRadius="md"
border="1px solid" border="1px solid"
borderColor={theme.border} borderColor={theme.border}
@@ -1428,7 +1406,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
{report.verification_item && ( {report.verification_item && (
<Box <Box
p={2} p={2}
bg={colorMode === 'light' ? 'yellow.50' : 'yellow.900'} bg={'yellow.50'}
borderRadius="sm" borderRadius="sm"
mb={2} mb={2}
> >
@@ -1479,7 +1457,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
当日分钟频数据 当日分钟频数据
</Heading> </Heading>
{minuteData && minuteData.trade_date && ( {minuteData && minuteData.trade_date && (
<Badge colorScheme={colorMode === 'light' ? 'blue' : 'yellow'} fontSize="xs"> <Badge colorScheme={'blue'} fontSize="xs">
{minuteData.trade_date} {minuteData.trade_date}
</Badge> </Badge>
)} )}
@@ -1488,7 +1466,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
leftIcon={<RepeatIcon />} leftIcon={<RepeatIcon />}
size="sm" size="sm"
variant="outline" variant="outline"
colorScheme={colorMode === 'light' ? 'blue' : 'yellow'} colorScheme={'blue'}
onClick={loadMinuteData} onClick={loadMinuteData}
isLoading={minuteLoading} isLoading={minuteLoading}
loadingText="获取中" loadingText="获取中"
@@ -1520,7 +1498,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<ReactECharts <ReactECharts
option={getMinuteKLineOption()} option={getMinuteKLineOption()}
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
theme={colorMode === 'light' ? 'light' : 'dark'} theme={'light'}
/> />
</Box> </Box>
@@ -1592,7 +1570,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
{/* 成交量分析 */} {/* 成交量分析 */}
<Box <Box
p={4} p={4}
bg={colorMode === 'light' ? theme.bgDark : 'rgba(255, 215, 0, 0.05)'} bg={theme.bgDark}
borderRadius="lg" borderRadius="lg"
border="1px solid" border="1px solid"
borderColor={theme.border} borderColor={theme.border}
@@ -1687,7 +1665,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
</Thead> </Thead>
<Tbody> <Tbody>
{tradeData.slice(-10).reverse().map((item, idx) => ( {tradeData.slice(-10).reverse().map((item, idx) => (
<Tr key={idx} _hover={{ bg: colorMode === 'light' ? theme.bgDark : 'rgba(255, 215, 0, 0.1)' }}> <Tr key={idx} _hover={{ bg: theme.bgDark }}>
<Td color={theme.textPrimary}>{item.date}</Td> <Td color={theme.textPrimary}>{item.date}</Td>
<Td isNumeric color={theme.textPrimary}>{item.open}</Td> <Td isNumeric color={theme.textPrimary}>{item.open}</Td>
<Td isNumeric color={theme.textPrimary}>{item.high}</Td> <Td isNumeric color={theme.textPrimary}>{item.high}</Td>
@@ -1718,7 +1696,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<ReactECharts <ReactECharts
option={getFundingOption()} option={getFundingOption()}
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
theme={colorMode === 'light' ? 'light' : 'dark'} theme={'light'}
/> />
</Box> </Box>
)} )}
@@ -1735,7 +1713,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<CardBody> <CardBody>
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
{fundingData.slice(-5).reverse().map((item, idx) => ( {fundingData.slice(-5).reverse().map((item, idx) => (
<Box key={idx} p={3} bg={colorMode === 'light' ? 'rgba(255, 68, 68, 0.05)' : 'rgba(255, 68, 68, 0.1)'} borderRadius="md"> <Box key={idx} p={3} bg={'rgba(255, 68, 68, 0.05)'} borderRadius="md">
<HStack justify="space-between"> <HStack justify="space-between">
<Text color={theme.textMuted}>{item.date}</Text> <Text color={theme.textMuted}>{item.date}</Text>
<VStack align="end" spacing={0}> <VStack align="end" spacing={0}>
@@ -1762,7 +1740,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<CardBody> <CardBody>
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
{fundingData.slice(-5).reverse().map((item, idx) => ( {fundingData.slice(-5).reverse().map((item, idx) => (
<Box key={idx} p={3} bg={colorMode === 'light' ? 'rgba(0, 200, 81, 0.05)' : 'rgba(0, 200, 81, 0.1)'} borderRadius="md"> <Box key={idx} p={3} bg={'rgba(0, 200, 81, 0.05)'} borderRadius="md">
<HStack justify="space-between"> <HStack justify="space-between">
<Text color={theme.textMuted}>{item.date}</Text> <Text color={theme.textMuted}>{item.date}</Text>
<VStack align="end" spacing={0}> <VStack align="end" spacing={0}>
@@ -1798,7 +1776,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<Box <Box
key={idx} key={idx}
p={4} p={4}
bg={colorMode === 'light' ? theme.bgDark : 'rgba(255, 215, 0, 0.05)'} bg={theme.bgDark}
borderRadius="lg" borderRadius="lg"
border="1px solid" border="1px solid"
borderColor={theme.border} borderColor={theme.border}
@@ -1808,7 +1786,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
{dayStats.date} {dayStats.date}
</Text> </Text>
<HStack spacing={4}> <HStack spacing={4}>
<Badge colorScheme={colorMode === 'light' ? 'blue' : 'yellow'} fontSize="md"> <Badge colorScheme={'blue'} fontSize="md">
交易笔数: {dayStats.count} 交易笔数: {dayStats.count}
</Badge> </Badge>
<Badge colorScheme="green" fontSize="md"> <Badge colorScheme="green" fontSize="md">
@@ -1838,7 +1816,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
</Thead> </Thead>
<Tbody> <Tbody>
{dayStats.deals.map((deal, i) => ( {dayStats.deals.map((deal, i) => (
<Tr key={i} _hover={{ bg: colorMode === 'light' ? 'rgba(43, 108, 176, 0.05)' : 'rgba(255, 215, 0, 0.1)' }}> <Tr key={i} _hover={{ bg: 'rgba(43, 108, 176, 0.05)' }}>
<Td color={theme.textPrimary} fontSize="xs" maxW="200px" isTruncated> <Td color={theme.textPrimary} fontSize="xs" maxW="200px" isTruncated>
<Tooltip label={deal.buyer_dept || '-'} placement="top"> <Tooltip label={deal.buyer_dept || '-'} placement="top">
<Text>{deal.buyer_dept || '-'}</Text> <Text>{deal.buyer_dept || '-'}</Text>
@@ -1891,7 +1869,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<Box <Box
key={idx} key={idx}
p={4} p={4}
bg={colorMode === 'light' ? theme.bgDark : 'rgba(255, 215, 0, 0.05)'} bg={theme.bgDark}
borderRadius="lg" borderRadius="lg"
border="1px solid" border="1px solid"
borderColor={theme.border} borderColor={theme.border}
@@ -1925,7 +1903,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
key={i} key={i}
justify="space-between" justify="space-between"
p={2} p={2}
bg={colorMode === 'light' ? 'rgba(255, 68, 68, 0.05)' : 'rgba(255, 68, 68, 0.1)'} bg={'rgba(255, 68, 68, 0.05)'}
borderRadius="md" borderRadius="md"
> >
<Text fontSize="sm" color={theme.textPrimary} isTruncated maxW="70%"> <Text fontSize="sm" color={theme.textPrimary} isTruncated maxW="70%">
@@ -1953,7 +1931,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
key={i} key={i}
justify="space-between" justify="space-between"
p={2} p={2}
bg={colorMode === 'light' ? 'rgba(0, 200, 81, 0.05)' : 'rgba(0, 200, 81, 0.1)'} bg={'rgba(0, 200, 81, 0.05)'}
borderRadius="md" borderRadius="md"
> >
<Text fontSize="sm" color={theme.textPrimary} isTruncated maxW="70%"> <Text fontSize="sm" color={theme.textPrimary} isTruncated maxW="70%">
@@ -1975,7 +1953,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<HStack mt={3} spacing={2}> <HStack mt={3} spacing={2}>
<Text fontSize="sm" color={theme.textMuted}>类型:</Text> <Text fontSize="sm" color={theme.textMuted}>类型:</Text>
{dayData.info_types && dayData.info_types.map((type, i) => ( {dayData.info_types && dayData.info_types.map((type, i) => (
<Badge key={i} colorScheme={colorMode === 'light' ? 'blue' : 'yellow'} fontSize="xs"> <Badge key={i} colorScheme={'blue'} fontSize="xs">
{type} {type}
</Badge> </Badge>
))} ))}
@@ -2002,7 +1980,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<ReactECharts <ReactECharts
option={getPledgeOption()} option={getPledgeOption()}
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
theme={colorMode === 'light' ? 'light' : 'dark'} theme={'light'}
/> />
</Box> </Box>
)} )}
@@ -2032,7 +2010,7 @@ const MarketDataView = ({ stockCode: propStockCode }) => {
<Tbody> <Tbody>
{Array.isArray(pledgeData) && pledgeData.length > 0 ? ( {Array.isArray(pledgeData) && pledgeData.length > 0 ? (
pledgeData.map((item, idx) => ( pledgeData.map((item, idx) => (
<Tr key={idx} _hover={{ bg: colorMode === 'light' ? theme.bgDark : 'rgba(255, 215, 0, 0.1)' }}> <Tr key={idx} _hover={{ bg: theme.bgDark }}>
<Td color={theme.textPrimary}>{item.end_date}</Td> <Td color={theme.textPrimary}>{item.end_date}</Td>
<Td isNumeric color={theme.textPrimary}>{formatUtils.formatNumber(item.unrestricted_pledge, 0)}</Td> <Td isNumeric color={theme.textPrimary}>{formatUtils.formatNumber(item.unrestricted_pledge, 0)}</Td>
<Td isNumeric color={theme.textPrimary}>{formatUtils.formatNumber(item.restricted_pledge, 0)}</Td> <Td isNumeric color={theme.textPrimary}>{formatUtils.formatNumber(item.restricted_pledge, 0)}</Td>

View File

@@ -16,7 +16,6 @@ import {
Badge, Badge,
Progress, Progress,
Skeleton, Skeleton,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { StockQuoteCardProps } from './types'; import type { StockQuoteCardProps } from './types';
@@ -53,11 +52,11 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
isLoading = false, isLoading = false,
}) => { }) => {
// 颜色配置 // 颜色配置
const cardBg = useColorModeValue('white', 'gray.800'); const cardBg = 'white';
const borderColor = useColorModeValue('gray.200', 'gray.700'); const borderColor = 'gray.200';
const labelColor = useColorModeValue('gray.500', 'gray.400'); const labelColor = 'gray.500';
const valueColor = useColorModeValue('gray.800', 'gray.100'); const valueColor = 'gray.800';
const sectionTitleColor = useColorModeValue('gray.600', 'gray.300'); const sectionTitleColor = 'gray.600';
// 涨跌颜色 // 涨跌颜色
const priceColor = data.changePercent >= 0 ? 'green.500' : 'red.500'; const priceColor = data.changePercent >= 0 ? 'green.500' : 'red.500';

View File

@@ -1,7 +1,7 @@
// src/views/Company/constants/index.js // src/views/Company/constants/index.js
// 公司详情页面常量配置 // 公司详情页面常量配置
import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle } from 'react-icons/fa'; import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle, FaBrain, FaNewspaper } from 'react-icons/fa';
/** /**
* Tab 配置 * Tab 配置
@@ -9,9 +9,11 @@ import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle } from 'react-ic
*/ */
export const COMPANY_TABS = [ export const COMPANY_TABS = [
{ key: 'overview', name: '公司概览', icon: FaInfoCircle }, { key: 'overview', name: '公司概览', icon: FaInfoCircle },
{ key: 'analysis', name: '深度分析', icon: FaBrain },
{ key: 'market', name: '股票行情', icon: FaChartLine }, { key: 'market', name: '股票行情', icon: FaChartLine },
{ key: 'financial', name: '财务全景', icon: FaMoneyBillWave }, { key: 'financial', name: '财务全景', icon: FaMoneyBillWave },
{ key: 'forecast', name: '盈利预测', icon: FaChartBar }, { key: 'forecast', name: '盈利预测', icon: FaChartBar },
{ key: 'tracking', name: '动态跟踪', icon: FaNewspaper },
]; ];
/** /**

View File

@@ -2,7 +2,7 @@
// 公司详情页面入口 - 纯组合层 // 公司详情页面入口 - 纯组合层
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Container, VStack, useColorModeValue } from '@chakra-ui/react'; import { Container, VStack } from '@chakra-ui/react';
// 自定义 Hooks // 自定义 Hooks
import { useCompanyStock } from './hooks/useCompanyStock'; import { useCompanyStock } from './hooks/useCompanyStock';
@@ -24,8 +24,6 @@ import CompanyTabs from './components/CompanyTabs';
* - PostHog 事件追踪 * - PostHog 事件追踪
*/ */
const CompanyIndex = () => { const CompanyIndex = () => {
const bgColor = useColorModeValue('white', 'gray.800');
// 1. 先获取股票代码(不带追踪回调) // 1. 先获取股票代码(不带追踪回调)
const { const {
stockCode, stockCode,
@@ -78,7 +76,7 @@ const CompanyIndex = () => {
isInWatchlist={isInWatchlist} isInWatchlist={isInWatchlist}
isWatchlistLoading={isWatchlistLoading} isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={handleWatchlistToggle} onWatchlistToggle={handleWatchlistToggle}
bgColor={bgColor} bgColor="white"
/> />
{/* 股票行情卡片:价格、关键指标、主力动态 */} {/* 股票行情卡片:价格、关键指标、主力动态 */}
@@ -88,7 +86,7 @@ const CompanyIndex = () => {
<CompanyTabs <CompanyTabs
stockCode={stockCode} stockCode={stockCode}
onTabChange={trackTabChanged} onTabChange={trackTabChanged}
bgColor={bgColor} bgColor="white"
/> />
</VStack> </VStack>
</Container> </Container>