Compare commits
4 Commits
d7759b1da3
...
4274341ed5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4274341ed5 | ||
|
|
40f6eaced6 | ||
|
|
2dd7dd755a | ||
|
|
04ce16df56 |
@@ -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);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|
||||||
|
|||||||
100
src/views/Company/components/DeepAnalysis/index.js
Normal file
100
src/views/Company/components/DeepAnalysis/index.js
Normal 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;
|
||||||
184
src/views/Company/components/DynamicTracking/index.js
Normal file
184
src/views/Company/components/DynamicTracking/index.js
Normal 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;
|
||||||
@@ -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' ?
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user