Compare commits
4 Commits
92019ca92d
...
4e71623477
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e71623477 | ||
|
|
ce4da40ef6 | ||
|
|
bff440ff8a | ||
|
|
9ef206a9e7 |
41
src/views/Company/components/CompanyHeader/README.md
Normal file
41
src/views/Company/components/CompanyHeader/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# CompanyHeader 组件
|
||||
|
||||
Company 页面顶部搜索栏组件,采用 FUI 科幻风格。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
CompanyHeader/
|
||||
├── index.tsx # 主组件入口
|
||||
├── constants.ts # 样式常量配置
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 股票代码/名称搜索(AutoComplete)
|
||||
- 搜索结果下拉展示
|
||||
- 支持拼音缩写搜索
|
||||
|
||||
## 组件结构
|
||||
|
||||
```
|
||||
CompanyHeader
|
||||
└── SearchBox # 搜索框子组件
|
||||
└── AutoComplete (antd) # 自动完成输入
|
||||
└── Input # 搜索输入框
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
```tsx
|
||||
import CompanyHeader from '@views/Company/components/CompanyHeader';
|
||||
|
||||
<CompanyHeader onStockChange={(code) => handleStockChange(code)} />
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| 属性 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `onStockChange` | `(code: string) => void` | 是 | 股票切换回调 |
|
||||
105
src/views/Company/components/CompanyOverview/README.md
Normal file
105
src/views/Company/components/CompanyOverview/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# CompanyOverview 组件
|
||||
|
||||
公司概览模块,包含基本信息和深度分析两个主要 Tab。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
CompanyOverview/
|
||||
├── index.tsx # 主组件入口
|
||||
├── types.ts # 类型定义
|
||||
├── utils.ts # 工具函数
|
||||
├── README.md # 本文档
|
||||
│
|
||||
├── hooks/ # 数据获取 Hooks
|
||||
│ ├── useBasicInfo.ts # 公司基本信息
|
||||
│ ├── useShareholderData.ts # 股东数据
|
||||
│ ├── useManagementData.ts # 管理层数据
|
||||
│ ├── useBranchesData.ts # 分支机构数据
|
||||
│ ├── useAnnouncementsData.ts # 公告数据
|
||||
│ └── useDisclosureData.ts # 披露日程数据
|
||||
│
|
||||
├── BasicInfoTab/ # 基本信息 Tab
|
||||
│ ├── index.tsx # Tab 入口
|
||||
│ ├── config.ts # 配置
|
||||
│ ├── utils.ts # 工具函数
|
||||
│ └── components/ # 子组件
|
||||
│ ├── BusinessInfoPanel.tsx # 工商信息面板
|
||||
│ ├── ShareholderPanel.tsx # 股东信息面板
|
||||
│ ├── AnnouncementsPanel.tsx # 公告面板
|
||||
│ ├── BranchesPanel.tsx # 分支机构面板
|
||||
│ ├── DisclosureSchedulePanel.tsx # 披露日程面板
|
||||
│ ├── LoadingState.tsx # 加载状态
|
||||
│ └── management/ # 管理层组件
|
||||
│ ├── ManagementPanel.tsx # 管理层面板
|
||||
│ ├── ManagementCard.tsx # 管理层卡片
|
||||
│ ├── CategorySection.tsx # 分类区块
|
||||
│ └── types.ts # 类型定义
|
||||
│
|
||||
├── DeepAnalysisTab/ # 深度分析 Tab
|
||||
│ ├── index.tsx # Tab 入口
|
||||
│ ├── types.ts # 类型定义
|
||||
│ ├── atoms/ # 原子组件
|
||||
│ │ ├── ScoreBar.tsx # 评分条
|
||||
│ │ ├── KeyFactorCard.tsx # 关键因素卡片
|
||||
│ │ ├── BusinessTreeItem.tsx # 业务树节点
|
||||
│ │ ├── ProcessNavigation.tsx # 流程导航
|
||||
│ │ ├── ValueChainFilterBar.tsx # 产业链筛选栏
|
||||
│ │ └── DisclaimerBox.tsx # 免责声明
|
||||
│ ├── components/ # 分子组件
|
||||
│ │ ├── CorePositioningCard/ # 核心定位卡片
|
||||
│ │ ├── BusinessStructureCard.tsx # 业务结构
|
||||
│ │ ├── BusinessSegmentsCard.tsx # 业务板块
|
||||
│ │ ├── CompetitiveAnalysisCard.tsx # 竞争分析
|
||||
│ │ ├── StrategyAnalysisCard.tsx # 战略分析
|
||||
│ │ ├── KeyFactorsCard.tsx # 关键因素
|
||||
│ │ ├── TimelineCard.tsx # 时间线
|
||||
│ │ └── ValueChainCard.tsx # 产业链
|
||||
│ ├── organisms/ # 有机体组件
|
||||
│ │ ├── TimelineComponent/ # 时间线组件
|
||||
│ │ └── ValueChainNodeCard/ # 产业链节点
|
||||
│ ├── tabs/ # 子 Tab
|
||||
│ │ ├── BusinessTab.tsx # 业务分析
|
||||
│ │ ├── StrategyTab.tsx # 战略分析
|
||||
│ │ ├── ValueChainTab.tsx # 产业链分析
|
||||
│ │ └── DevelopmentTab.tsx # 发展历程
|
||||
│ └── utils/
|
||||
│ └── chartOptions.ts # 图表配置
|
||||
│
|
||||
└── components/ # 共享组件
|
||||
└── shareholder/ # 股东相关
|
||||
├── ShareholdersTable.tsx # 股东表格
|
||||
├── ConcentrationCard.tsx # 股权集中度
|
||||
└── ActualControlCard.tsx # 实际控制人
|
||||
```
|
||||
|
||||
## 组件层级
|
||||
|
||||
```
|
||||
CompanyOverview
|
||||
├── SubTabContainer # Tab 容器
|
||||
│ ├── BasicInfoTab # 基本信息
|
||||
│ │ ├── BusinessInfoPanel
|
||||
│ │ ├── ShareholderPanel
|
||||
│ │ │ ├── ShareholdersTable
|
||||
│ │ │ ├── ConcentrationCard
|
||||
│ │ │ └── ActualControlCard
|
||||
│ │ ├── ManagementPanel
|
||||
│ │ ├── AnnouncementsPanel
|
||||
│ │ ├── BranchesPanel
|
||||
│ │ └── DisclosureSchedulePanel
|
||||
│ │
|
||||
│ └── DeepAnalysisTab # 深度分析
|
||||
│ ├── BusinessTab
|
||||
│ ├── StrategyTab
|
||||
│ ├── ValueChainTab
|
||||
│ └── DevelopmentTab
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
```tsx
|
||||
import CompanyOverview from '@views/Company/components/CompanyOverview';
|
||||
|
||||
<CompanyOverview stockCode="600000" />
|
||||
```
|
||||
79
src/views/Company/components/DeepAnalysis/README.md
Normal file
79
src/views/Company/components/DeepAnalysis/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# DeepAnalysis 组件
|
||||
|
||||
深度分析模块,展示公司战略分析、业务分析、产业链分析和发展历程。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
DeepAnalysis/
|
||||
├── index.tsx # 主组件入口(memo 优化)
|
||||
├── types.ts # 类型定义
|
||||
├── README.md # 本文档
|
||||
│
|
||||
└── hooks/
|
||||
├── index.ts # Hooks 导出
|
||||
└── useDeepAnalysisData.ts # 数据获取 Hook(懒加载、缓存)
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| Tab 懒加载 | 切换 Tab 时按需加载数据 |
|
||||
| 数据缓存 | 已加载的数据不重复请求 |
|
||||
| 竞态处理 | stockCode 变更时防止旧请求覆盖新数据 |
|
||||
| memo 优化 | 避免不必要的重渲染 |
|
||||
|
||||
## API 接口映射
|
||||
|
||||
| Tab | API Key | 接口 |
|
||||
|-----|---------|------|
|
||||
| 战略分析 | comprehensive | `/api/company/comprehensive-analysis` |
|
||||
| 业务分析 | comprehensive | 同上(共用) |
|
||||
| 产业链 | valueChain | `/api/company/value-chain-analysis` |
|
||||
| 发展历程 | keyFactors | `/api/company/key-factors-timeline` |
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
DeepAnalysis
|
||||
├── useDeepAnalysisData (Hook)
|
||||
│ ├── data: { comprehensive, valueChain, keyFactors, industryRank }
|
||||
│ ├── loading: { comprehensive, valueChain, keyFactors, industryRank }
|
||||
│ └── loadTabData(tabKey) → loadApiData(apiKey)
|
||||
│
|
||||
└── DeepAnalysisTab (展示组件)
|
||||
├── StrategyTab
|
||||
├── BusinessTab
|
||||
├── ValueChainTab
|
||||
└── DevelopmentTab
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
```tsx
|
||||
import DeepAnalysis from '@views/Company/components/DeepAnalysis';
|
||||
|
||||
<DeepAnalysis stockCode="600000" />
|
||||
```
|
||||
|
||||
## Hook 使用
|
||||
|
||||
```tsx
|
||||
import { useDeepAnalysisData } from '@views/Company/components/DeepAnalysis/hooks';
|
||||
|
||||
const { data, loading, loadTabData, resetData } = useDeepAnalysisData(stockCode);
|
||||
|
||||
// 手动加载某个 Tab
|
||||
loadTabData('valueChain');
|
||||
|
||||
// 重置所有数据
|
||||
resetData();
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
- `React.memo` 包装主组件
|
||||
- `useCallback` 稳定化事件处理函数
|
||||
- `useRef` 追踪已加载状态(避免重复请求)
|
||||
- 竞态条件检测(stockCode 变更时忽略旧请求)
|
||||
1
src/views/Company/components/DeepAnalysis/hooks/index.ts
Normal file
1
src/views/Company/components/DeepAnalysis/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useDeepAnalysisData } from './useDeepAnalysisData';
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* useDeepAnalysisData Hook
|
||||
*
|
||||
* 管理深度分析模块的数据获取逻辑:
|
||||
* - 按 Tab 懒加载数据
|
||||
* - 已加载数据缓存,避免重复请求
|
||||
* - 竞态条件处理
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import axios from '@utils/axiosConfig';
|
||||
import { logger } from '@utils/logger';
|
||||
import type {
|
||||
ApiKey,
|
||||
ApiLoadingState,
|
||||
DataState,
|
||||
UseDeepAnalysisDataReturn,
|
||||
} from '../types';
|
||||
import { TAB_API_MAP } from '../types';
|
||||
|
||||
/** API 端点映射 */
|
||||
const API_ENDPOINTS: Record<ApiKey, string> = {
|
||||
comprehensive: '/api/company/comprehensive-analysis',
|
||||
valueChain: '/api/company/value-chain-analysis',
|
||||
keyFactors: '/api/company/key-factors-timeline',
|
||||
industryRank: '/api/financial/industry-rank',
|
||||
};
|
||||
|
||||
/** 初始数据状态 */
|
||||
const initialDataState: DataState = {
|
||||
comprehensive: null,
|
||||
valueChain: null,
|
||||
keyFactors: null,
|
||||
industryRank: null,
|
||||
};
|
||||
|
||||
/** 初始 loading 状态 */
|
||||
const initialLoadingState: ApiLoadingState = {
|
||||
comprehensive: false,
|
||||
valueChain: false,
|
||||
keyFactors: false,
|
||||
industryRank: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 深度分析数据 Hook
|
||||
*
|
||||
* @param stockCode 股票代码
|
||||
* @returns 数据、loading 状态、加载函数
|
||||
*/
|
||||
export const useDeepAnalysisData = (stockCode: string): UseDeepAnalysisDataReturn => {
|
||||
// 数据状态
|
||||
const [data, setData] = useState<DataState>(initialDataState);
|
||||
|
||||
// Loading 状态
|
||||
const [loading, setLoading] = useState<ApiLoadingState>(initialLoadingState);
|
||||
|
||||
// 已加载的接口记录
|
||||
const loadedApisRef = useRef<Record<ApiKey, boolean>>({
|
||||
comprehensive: false,
|
||||
valueChain: false,
|
||||
keyFactors: false,
|
||||
industryRank: false,
|
||||
});
|
||||
|
||||
// 当前 stockCode(用于竞态条件检测)
|
||||
const currentStockCodeRef = useRef(stockCode);
|
||||
|
||||
/**
|
||||
* 加载指定 API 数据
|
||||
*/
|
||||
const loadApiData = useCallback(
|
||||
async (apiKey: ApiKey) => {
|
||||
if (!stockCode) return;
|
||||
|
||||
// 已加载则跳过
|
||||
if (loadedApisRef.current[apiKey]) return;
|
||||
|
||||
// 设置 loading
|
||||
setLoading((prev) => ({ ...prev, [apiKey]: true }));
|
||||
|
||||
try {
|
||||
const endpoint = `${API_ENDPOINTS[apiKey]}/${stockCode}`;
|
||||
const { data: response } = await axios.get(endpoint);
|
||||
|
||||
// 检查 stockCode 是否已变更(防止竞态)
|
||||
if (currentStockCodeRef.current !== stockCode) return;
|
||||
|
||||
if (response.success) {
|
||||
setData((prev) => ({ ...prev, [apiKey]: response.data }));
|
||||
loadedApisRef.current[apiKey] = true;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('DeepAnalysis', `loadApiData:${apiKey}`, err, { stockCode });
|
||||
} finally {
|
||||
// 清除 loading(再次检查 stockCode)
|
||||
if (currentStockCodeRef.current === stockCode) {
|
||||
setLoading((prev) => ({ ...prev, [apiKey]: false }));
|
||||
}
|
||||
}
|
||||
},
|
||||
[stockCode]
|
||||
);
|
||||
|
||||
/**
|
||||
* 根据 Tab 加载对应数据
|
||||
*/
|
||||
const loadTabData = useCallback(
|
||||
(tabKey: string) => {
|
||||
const apiKey = TAB_API_MAP[tabKey];
|
||||
if (apiKey) {
|
||||
loadApiData(apiKey);
|
||||
}
|
||||
},
|
||||
[loadApiData]
|
||||
);
|
||||
|
||||
/**
|
||||
* 重置所有数据
|
||||
*/
|
||||
const resetData = useCallback(() => {
|
||||
setData(initialDataState);
|
||||
setLoading(initialLoadingState);
|
||||
loadedApisRef.current = {
|
||||
comprehensive: false,
|
||||
valueChain: false,
|
||||
keyFactors: false,
|
||||
industryRank: false,
|
||||
};
|
||||
}, []);
|
||||
|
||||
// stockCode 变更时重置并加载默认数据
|
||||
useEffect(() => {
|
||||
if (stockCode) {
|
||||
currentStockCodeRef.current = stockCode;
|
||||
resetData();
|
||||
// 只加载默认 Tab(comprehensive)
|
||||
loadApiData('comprehensive');
|
||||
}
|
||||
}, [stockCode, loadApiData, resetData]);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
loadTabData,
|
||||
resetData,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDeepAnalysisData;
|
||||
@@ -1,228 +0,0 @@
|
||||
// src/views/Company/components/DeepAnalysis/index.js
|
||||
// 深度分析 - 独立一级 Tab 组件(懒加载版本)
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { logger } from "@utils/logger";
|
||||
import axios from "@utils/axiosConfig";
|
||||
|
||||
// 复用原有的展示组件
|
||||
import DeepAnalysisTab from "../CompanyOverview/DeepAnalysisTab";
|
||||
|
||||
/**
|
||||
* Tab 与 API 接口映射
|
||||
* - strategy 和 business 共用 comprehensive 接口
|
||||
*/
|
||||
const TAB_API_MAP = {
|
||||
strategy: "comprehensive",
|
||||
business: "comprehensive",
|
||||
valueChain: "valueChain",
|
||||
development: "keyFactors",
|
||||
};
|
||||
|
||||
/**
|
||||
* 深度分析组件
|
||||
*
|
||||
* 功能:
|
||||
* - 按 Tab 懒加载数据(默认只加载战略分析)
|
||||
* - 已加载的数据缓存,切换 Tab 不重复请求
|
||||
* - 管理展开状态
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string} props.stockCode - 股票代码
|
||||
*/
|
||||
const DeepAnalysis = ({ stockCode }) => {
|
||||
// 当前 Tab
|
||||
const [activeTab, setActiveTab] = useState("strategy");
|
||||
|
||||
// 数据状态
|
||||
const [comprehensiveData, setComprehensiveData] = useState(null);
|
||||
const [valueChainData, setValueChainData] = useState(null);
|
||||
const [keyFactorsData, setKeyFactorsData] = useState(null);
|
||||
const [industryRankData, setIndustryRankData] = useState(null);
|
||||
|
||||
// 各接口独立的 loading 状态
|
||||
const [comprehensiveLoading, setComprehensiveLoading] = useState(false);
|
||||
const [valueChainLoading, setValueChainLoading] = useState(false);
|
||||
const [keyFactorsLoading, setKeyFactorsLoading] = useState(false);
|
||||
const [industryRankLoading, setIndustryRankLoading] = useState(false);
|
||||
|
||||
// 已加载的接口记录(用于缓存判断)
|
||||
const loadedApisRef = useRef({
|
||||
comprehensive: false,
|
||||
valueChain: false,
|
||||
keyFactors: false,
|
||||
industryRank: false,
|
||||
});
|
||||
|
||||
// 业务板块展开状态
|
||||
const [expandedSegments, setExpandedSegments] = useState({});
|
||||
|
||||
// 用于追踪当前 stockCode,避免竞态条件
|
||||
const currentStockCodeRef = useRef(stockCode);
|
||||
|
||||
// 切换业务板块展开状态
|
||||
const toggleSegmentExpansion = (segmentIndex) => {
|
||||
setExpandedSegments((prev) => ({
|
||||
...prev,
|
||||
[segmentIndex]: !prev[segmentIndex],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载指定接口的数据
|
||||
*/
|
||||
const loadApiData = useCallback(
|
||||
async (apiKey) => {
|
||||
if (!stockCode) return;
|
||||
|
||||
// 已加载则跳过
|
||||
if (loadedApisRef.current[apiKey]) return;
|
||||
|
||||
try {
|
||||
switch (apiKey) {
|
||||
case "comprehensive":
|
||||
setComprehensiveLoading(true);
|
||||
const { data: comprehensiveRes } = await axios.get(
|
||||
`/api/company/comprehensive-analysis/${stockCode}`
|
||||
);
|
||||
// 检查 stockCode 是否已变更(防止竞态)
|
||||
if (currentStockCodeRef.current === stockCode) {
|
||||
if (comprehensiveRes.success)
|
||||
setComprehensiveData(comprehensiveRes.data);
|
||||
loadedApisRef.current.comprehensive = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "valueChain":
|
||||
setValueChainLoading(true);
|
||||
const { data: valueChainRes } = await axios.get(
|
||||
`/api/company/value-chain-analysis/${stockCode}`
|
||||
);
|
||||
if (currentStockCodeRef.current === stockCode) {
|
||||
if (valueChainRes.success) setValueChainData(valueChainRes.data);
|
||||
loadedApisRef.current.valueChain = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "keyFactors":
|
||||
setKeyFactorsLoading(true);
|
||||
const { data: keyFactorsRes } = await axios.get(
|
||||
`/api/company/key-factors-timeline/${stockCode}`
|
||||
);
|
||||
if (currentStockCodeRef.current === stockCode) {
|
||||
if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data);
|
||||
loadedApisRef.current.keyFactors = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "industryRank":
|
||||
setIndustryRankLoading(true);
|
||||
const { data: industryRankRes } = await axios.get(
|
||||
`/api/financial/industry-rank/${stockCode}`
|
||||
);
|
||||
if (currentStockCodeRef.current === stockCode) {
|
||||
if (industryRankRes.success) setIndustryRankData(industryRankRes.data);
|
||||
loadedApisRef.current.industryRank = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("DeepAnalysis", `loadApiData:${apiKey}`, err, {
|
||||
stockCode,
|
||||
});
|
||||
} finally {
|
||||
// 清除 loading 状态
|
||||
if (apiKey === "comprehensive") setComprehensiveLoading(false);
|
||||
if (apiKey === "valueChain") setValueChainLoading(false);
|
||||
if (apiKey === "keyFactors") setKeyFactorsLoading(false);
|
||||
if (apiKey === "industryRank") setIndustryRankLoading(false);
|
||||
}
|
||||
},
|
||||
[stockCode]
|
||||
);
|
||||
|
||||
/**
|
||||
* 根据 Tab 加载对应的数据
|
||||
*/
|
||||
const loadTabData = useCallback(
|
||||
(tabKey) => {
|
||||
const apiKey = TAB_API_MAP[tabKey];
|
||||
if (apiKey) {
|
||||
loadApiData(apiKey);
|
||||
}
|
||||
},
|
||||
[loadApiData]
|
||||
);
|
||||
|
||||
/**
|
||||
* Tab 切换回调
|
||||
*/
|
||||
const handleTabChange = useCallback(
|
||||
(index, tabKey) => {
|
||||
setActiveTab(tabKey);
|
||||
loadTabData(tabKey);
|
||||
},
|
||||
[loadTabData]
|
||||
);
|
||||
|
||||
// stockCode 变更时重置并加载默认 Tab 数据
|
||||
useEffect(() => {
|
||||
if (stockCode) {
|
||||
// 更新 ref
|
||||
currentStockCodeRef.current = stockCode;
|
||||
|
||||
// 重置所有数据和状态
|
||||
setComprehensiveData(null);
|
||||
setValueChainData(null);
|
||||
setKeyFactorsData(null);
|
||||
setIndustryRankData(null);
|
||||
setExpandedSegments({});
|
||||
loadedApisRef.current = {
|
||||
comprehensive: false,
|
||||
valueChain: false,
|
||||
keyFactors: false,
|
||||
industryRank: false,
|
||||
};
|
||||
|
||||
// 重置为默认 Tab 并加载数据
|
||||
setActiveTab("strategy");
|
||||
// 只加载默认 Tab 的核心数据(comprehensive),其他数据按需加载
|
||||
loadApiData("comprehensive");
|
||||
}
|
||||
}, [stockCode, loadApiData]);
|
||||
|
||||
// 计算当前 Tab 的 loading 状态
|
||||
const getCurrentLoading = () => {
|
||||
const apiKey = TAB_API_MAP[activeTab];
|
||||
switch (apiKey) {
|
||||
case "comprehensive":
|
||||
return comprehensiveLoading;
|
||||
case "valueChain":
|
||||
return valueChainLoading;
|
||||
case "keyFactors":
|
||||
return keyFactorsLoading;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeepAnalysisTab
|
||||
comprehensiveData={comprehensiveData}
|
||||
valueChainData={valueChainData}
|
||||
keyFactorsData={keyFactorsData}
|
||||
industryRankData={industryRankData}
|
||||
loading={getCurrentLoading()}
|
||||
cardBg="white"
|
||||
expandedSegments={expandedSegments}
|
||||
onToggleSegment={toggleSegmentExpansion}
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeepAnalysis;
|
||||
81
src/views/Company/components/DeepAnalysis/index.tsx
Normal file
81
src/views/Company/components/DeepAnalysis/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* DeepAnalysis - 深度分析组件
|
||||
*
|
||||
* 独立一级 Tab 组件,支持:
|
||||
* - 按 Tab 懒加载数据
|
||||
* - 已加载数据缓存
|
||||
* - 业务板块展开状态管理
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect, memo } from 'react';
|
||||
import DeepAnalysisTab from '../CompanyOverview/DeepAnalysisTab';
|
||||
import type { DeepAnalysisTabKey } from '../CompanyOverview/DeepAnalysisTab/types';
|
||||
import { useDeepAnalysisData } from './hooks';
|
||||
import { TAB_API_MAP } from './types';
|
||||
import type { DeepAnalysisProps } from './types';
|
||||
|
||||
/**
|
||||
* 深度分析组件
|
||||
*
|
||||
* @param stockCode 股票代码
|
||||
*/
|
||||
const DeepAnalysis: React.FC<DeepAnalysisProps> = memo(({ stockCode }) => {
|
||||
// 当前 Tab
|
||||
const [activeTab, setActiveTab] = useState<DeepAnalysisTabKey>('strategy');
|
||||
|
||||
// 业务板块展开状态
|
||||
const [expandedSegments, setExpandedSegments] = useState<Record<number, boolean>>({});
|
||||
|
||||
// 数据获取 Hook
|
||||
const { data, loading, loadTabData } = useDeepAnalysisData(stockCode);
|
||||
|
||||
// stockCode 变更时重置 UI 状态
|
||||
useEffect(() => {
|
||||
if (stockCode) {
|
||||
setActiveTab('strategy');
|
||||
setExpandedSegments({});
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
// 切换业务板块展开状态
|
||||
const toggleSegmentExpansion = useCallback((segmentIndex: number) => {
|
||||
setExpandedSegments((prev) => ({
|
||||
...prev,
|
||||
[segmentIndex]: !prev[segmentIndex],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Tab 切换回调
|
||||
const handleTabChange = useCallback(
|
||||
(index: number, tabKey: DeepAnalysisTabKey) => {
|
||||
setActiveTab(tabKey);
|
||||
loadTabData(tabKey);
|
||||
},
|
||||
[loadTabData]
|
||||
);
|
||||
|
||||
// 获取当前 Tab 的 loading 状态
|
||||
const currentLoading = (() => {
|
||||
const apiKey = TAB_API_MAP[activeTab];
|
||||
return apiKey ? loading[apiKey] : false;
|
||||
})();
|
||||
|
||||
return (
|
||||
<DeepAnalysisTab
|
||||
comprehensiveData={data.comprehensive}
|
||||
valueChainData={data.valueChain}
|
||||
keyFactorsData={data.keyFactors}
|
||||
industryRankData={data.industryRank}
|
||||
loading={currentLoading}
|
||||
cardBg="white"
|
||||
expandedSegments={expandedSegments}
|
||||
onToggleSegment={toggleSegmentExpansion}
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DeepAnalysis.displayName = 'DeepAnalysis';
|
||||
|
||||
export default DeepAnalysis;
|
||||
72
src/views/Company/components/DeepAnalysis/types.ts
Normal file
72
src/views/Company/components/DeepAnalysis/types.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* DeepAnalysis 组件类型定义
|
||||
*/
|
||||
|
||||
// 复用 DeepAnalysisTab 的数据类型
|
||||
export type {
|
||||
ComprehensiveData,
|
||||
ValueChainData,
|
||||
KeyFactorsData,
|
||||
IndustryRankData,
|
||||
DeepAnalysisTabKey,
|
||||
} from '../CompanyOverview/DeepAnalysisTab/types';
|
||||
|
||||
/** API 接口类型 */
|
||||
export type ApiKey = 'comprehensive' | 'valueChain' | 'keyFactors' | 'industryRank';
|
||||
|
||||
/** Tab 与 API 映射 */
|
||||
export const TAB_API_MAP: Record<string, ApiKey> = {
|
||||
strategy: 'comprehensive',
|
||||
business: 'comprehensive',
|
||||
valueChain: 'valueChain',
|
||||
development: 'keyFactors',
|
||||
} as const;
|
||||
|
||||
/** API 加载状态 */
|
||||
export interface ApiLoadingState {
|
||||
comprehensive: boolean;
|
||||
valueChain: boolean;
|
||||
keyFactors: boolean;
|
||||
industryRank: boolean;
|
||||
}
|
||||
|
||||
/** API 已加载标记 */
|
||||
export interface ApiLoadedState {
|
||||
comprehensive: boolean;
|
||||
valueChain: boolean;
|
||||
keyFactors: boolean;
|
||||
industryRank: boolean;
|
||||
}
|
||||
|
||||
/** 数据状态 */
|
||||
export interface DataState {
|
||||
comprehensive: ComprehensiveData | null;
|
||||
valueChain: ValueChainData | null;
|
||||
keyFactors: KeyFactorsData | null;
|
||||
industryRank: IndustryRankData[] | null;
|
||||
}
|
||||
|
||||
/** Hook 返回值 */
|
||||
export interface UseDeepAnalysisDataReturn {
|
||||
/** 各接口数据 */
|
||||
data: DataState;
|
||||
/** 各接口 loading 状态 */
|
||||
loading: ApiLoadingState;
|
||||
/** 加载指定 Tab 的数据 */
|
||||
loadTabData: (tabKey: string) => void;
|
||||
/** 重置所有数据 */
|
||||
resetData: () => void;
|
||||
}
|
||||
|
||||
/** 组件 Props */
|
||||
export interface DeepAnalysisProps {
|
||||
stockCode: string;
|
||||
}
|
||||
|
||||
// 导入类型用于内部使用
|
||||
import type {
|
||||
ComprehensiveData,
|
||||
ValueChainData,
|
||||
KeyFactorsData,
|
||||
IndustryRankData,
|
||||
} from '../CompanyOverview/DeepAnalysisTab/types';
|
||||
81
src/views/Company/components/FinancialPanorama/README.md
Normal file
81
src/views/Company/components/FinancialPanorama/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# FinancialPanorama 组件
|
||||
|
||||
财务全景模块,展示公司财务报表和指标分析。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
FinancialPanorama/
|
||||
├── index.tsx # 主组件入口
|
||||
├── types.ts # 类型定义
|
||||
├── constants.ts # 常量配置
|
||||
├── README.md # 本文档
|
||||
│
|
||||
├── hooks/
|
||||
│ ├── index.ts # Hooks 导出
|
||||
│ └── useFinancialData.ts # 财务数据获取
|
||||
│
|
||||
├── components/ # 子组件
|
||||
│ ├── index.ts # 统一导出
|
||||
│ ├── PeriodSelector.tsx # 期数选择器
|
||||
│ ├── StockInfoHeader.tsx # 股票信息头部
|
||||
│ ├── KeyMetricsOverview.tsx # 关键指标概览
|
||||
│ ├── FinancialOverviewPanel.tsx # 财务概览面板
|
||||
│ ├── FinancialTable.tsx # 财务表格基础组件
|
||||
│ ├── IncomeStatementTable.tsx # 利润表
|
||||
│ ├── BalanceSheetTable.tsx # 资产负债表
|
||||
│ ├── CashflowTable.tsx # 现金流量表
|
||||
│ ├── FinancialMetricsTable.tsx # 财务指标表
|
||||
│ ├── MainBusinessAnalysis.tsx # 主营业务分析
|
||||
│ ├── IndustryRankingView.tsx # 行业排名视图
|
||||
│ ├── StockComparison.tsx # 股票对比
|
||||
│ └── ComparisonAnalysis.tsx # 对比分析
|
||||
│
|
||||
├── tabs/ # Tab 页面
|
||||
│ ├── index.ts # 统一导出
|
||||
│ ├── IncomeStatementTab.tsx # 利润表 Tab
|
||||
│ ├── BalanceSheetTab.tsx # 资产负债表 Tab
|
||||
│ ├── CashflowTab.tsx # 现金流量表 Tab
|
||||
│ ├── FinancialMetricsTab.tsx # 财务指标 Tab
|
||||
│ └── MetricsCategoryTab.tsx # 指标分类 Tab
|
||||
│
|
||||
└── utils/
|
||||
├── index.ts # 工具函数导出
|
||||
├── calculations.ts # 计算函数
|
||||
└── chartOptions.ts # 图表配置
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| 利润表 | 营收、利润、费用等损益数据 |
|
||||
| 资产负债表 | 资产、负债、权益结构 |
|
||||
| 现金流量表 | 经营、投资、筹资现金流 |
|
||||
| 财务指标 | ROE、毛利率、周转率等 |
|
||||
| 主营分析 | 业务构成、地区分布 |
|
||||
| 行业排名 | 同行业公司对比排名 |
|
||||
|
||||
## 组件层级
|
||||
|
||||
```
|
||||
FinancialPanorama
|
||||
├── PeriodSelector # 期数选择
|
||||
├── SubTabContainer # Tab 容器
|
||||
│ ├── IncomeStatementTab # 利润表
|
||||
│ │ └── IncomeStatementTable
|
||||
│ ├── BalanceSheetTab # 资产负债表
|
||||
│ │ └── BalanceSheetTable
|
||||
│ ├── CashflowTab # 现金流量表
|
||||
│ │ └── CashflowTable
|
||||
│ └── FinancialMetricsTab # 财务指标
|
||||
│ └── FinancialMetricsTable
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
```tsx
|
||||
import FinancialPanorama from '@views/Company/components/FinancialPanorama';
|
||||
|
||||
<FinancialPanorama stockCode="600000" />
|
||||
```
|
||||
54
src/views/Company/components/ForecastReport/README.md
Normal file
54
src/views/Company/components/ForecastReport/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# ForecastReport 组件
|
||||
|
||||
盈利预测模块,展示券商研报预测数据和图表分析。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ForecastReport/
|
||||
├── index.tsx # 主组件入口
|
||||
├── types.ts # 类型定义
|
||||
├── constants.ts # 常量配置
|
||||
├── README.md # 本文档
|
||||
│
|
||||
└── components/ # 子组件
|
||||
├── index.ts # 统一导出
|
||||
├── ChartCard.tsx # 图表卡片容器
|
||||
├── DetailTable.tsx # 详情表格
|
||||
├── EpsChart.tsx # EPS 预测图表
|
||||
├── GrowthChart.tsx # 增长率图表
|
||||
├── IncomeProfitChart.tsx # 营收利润图表
|
||||
├── IncomeProfitGrowthChart.tsx # 营收利润增长图表
|
||||
└── PePegChart.tsx # PE/PEG 估值图表
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| EPS 预测 | 每股收益预测趋势 |
|
||||
| 增长预测 | 营收、利润增速预测 |
|
||||
| 估值分析 | PE、PEG 动态估值 |
|
||||
| 详情表格 | 各机构预测明细 |
|
||||
|
||||
## 组件层级
|
||||
|
||||
```
|
||||
ForecastReport
|
||||
├── ChartCard
|
||||
│ ├── EpsChart # EPS 图表
|
||||
│ ├── GrowthChart # 增长图表
|
||||
│ ├── IncomeProfitChart # 营收利润
|
||||
│ ├── IncomeProfitGrowthChart # 增长率
|
||||
│ └── PePegChart # 估值图表
|
||||
│
|
||||
└── DetailTable # 预测明细表
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
```tsx
|
||||
import ForecastReport from '@views/Company/components/ForecastReport';
|
||||
|
||||
<ForecastReport stockCode="600000" />
|
||||
```
|
||||
176
src/views/Company/components/MarketDataView/README.md
Normal file
176
src/views/Company/components/MarketDataView/README.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# MarketDataView 组件
|
||||
|
||||
市场数据视图模块,展示交易数据、资金流向、大宗交易等市场信息。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
MarketDataView/
|
||||
├── index.tsx # 主组件入口
|
||||
├── types.ts # 类型定义
|
||||
├── constants.ts # 常量配置(黑金主题)
|
||||
├── README.md # 本文档
|
||||
│
|
||||
├── hooks/
|
||||
│ └── useMarketData.ts # 市场数据获取(含 AbortController)
|
||||
│
|
||||
├── services/
|
||||
│ └── marketService.ts # API 服务层
|
||||
│
|
||||
├── components/
|
||||
│ ├── index.ts # 统一导出
|
||||
│ ├── ThemedCard.tsx # 主题卡片
|
||||
│ ├── AnalysisModal.tsx # 分析弹窗
|
||||
│ ├── MarkdownRenderer.tsx # Markdown 渲染
|
||||
│ │
|
||||
│ ├── shared/ # 共享组件
|
||||
│ │ ├── index.ts # 统一导出
|
||||
│ │ ├── styles.ts # 共享样式
|
||||
│ │ ├── DarkGoldCard.tsx # 黑金卡片容器
|
||||
│ │ ├── DarkGoldBadge.tsx # 黑金徽章
|
||||
│ │ └── EmptyState.tsx # 空状态组件
|
||||
│ │
|
||||
│ ├── panels/ # 数据面板
|
||||
│ │ ├── index.ts # 统一导出
|
||||
│ │ │
|
||||
│ │ ├── TradeDataPanel/ # 交易数据面板(K线模块)
|
||||
│ │ │ ├── index.tsx # 统一导出
|
||||
│ │ │ ├── KLineModule.tsx # K线主模块(157行,memo优化)
|
||||
│ │ │ ├── constants.ts # 指标选项常量
|
||||
│ │ │ ├── styles.ts # 按钮样式常量
|
||||
│ │ │ ├── MetricOverlaySearch.tsx # 指标叠加搜索
|
||||
│ │ │ └── components/ # 子组件
|
||||
│ │ │ ├── index.ts # 统一导出
|
||||
│ │ │ ├── KLineToolbar.tsx # 工具栏(模式切换、指标选择)
|
||||
│ │ │ ├── DailyKLineChart.tsx # 日K图表(useMemo缓存)
|
||||
│ │ │ └── MinuteChartWithOrderBook.tsx # 分时图+五档盘口
|
||||
│ │ │
|
||||
│ │ ├── FundingPanel.tsx # 融资融券面板(memo优化)
|
||||
│ │ ├── BigDealPanel.tsx # 大宗交易面板(memo优化)
|
||||
│ │ ├── UnusualPanel.tsx # 龙虎榜面板(memo优化)
|
||||
│ │ └── PledgePanel.tsx # 股权质押面板(memo优化)
|
||||
│ │
|
||||
│ └── StockSummaryCard/ # 股票摘要卡片
|
||||
│ ├── index.tsx # 卡片入口
|
||||
│ ├── utils.ts # 工具函数
|
||||
│ ├── StockHeaderCard.tsx # 头部卡片
|
||||
│ ├── MetricCard.tsx # 指标卡片
|
||||
│ └── atoms/ # 原子组件
|
||||
│ ├── DarkGoldCard.tsx
|
||||
│ ├── PriceDisplay.tsx
|
||||
│ ├── MetricValue.tsx
|
||||
│ ├── StatusTag.tsx
|
||||
│ └── CardTitle.tsx
|
||||
│
|
||||
└── utils/
|
||||
├── formatUtils.ts # 格式化工具
|
||||
└── chartOptions.ts # 图表配置
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| 交易数据 | K线图、分时图、成交量、五档盘口 |
|
||||
| 融资融券 | 融资余额、融券余额、资金趋势 |
|
||||
| 大宗交易 | 大宗交易记录、成交统计 |
|
||||
| 龙虎榜 | 买入卖出席位、净买入额 |
|
||||
| 股权质押 | 质押比例、质押明细 |
|
||||
|
||||
## 主题系统
|
||||
|
||||
使用 `darkGoldTheme` 黑金主题:
|
||||
|
||||
```typescript
|
||||
const darkGoldTheme = {
|
||||
bgCard: 'rgba(26, 32, 44, 0.95)',
|
||||
border: 'rgba(212, 175, 55, 0.3)',
|
||||
gold: '#D4AF37',
|
||||
green: '#00C851',
|
||||
red: '#FF4444',
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## 组件层级
|
||||
|
||||
```
|
||||
MarketDataView
|
||||
├── StockSummaryCard # 股票概览
|
||||
└── SubTabContainer # Tab 容器
|
||||
├── TradeDataPanel # 交易数据
|
||||
│ └── KLineModule
|
||||
│ ├── KLineToolbar # 工具栏
|
||||
│ ├── DailyKLineChart # 日K图表
|
||||
│ └── MinuteChartWithOrderBook # 分时+盘口
|
||||
├── FundingPanel # 融资融券
|
||||
├── BigDealPanel # 大宗交易
|
||||
├── UnusualPanel # 龙虎榜
|
||||
└── PledgePanel # 股权质押
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 已实现的优化
|
||||
|
||||
| 优化项 | 说明 |
|
||||
|--------|------|
|
||||
| React.memo | 所有 Panel 和子组件使用 memo 包装 |
|
||||
| useMemo | 图表配置缓存,避免重复计算技术指标 |
|
||||
| useCallback | 事件处理函数稳定化 |
|
||||
| AbortController | 请求取消,防止内存泄漏 |
|
||||
| Tab 懒加载 | 切换 Tab 时按需加载数据 |
|
||||
|
||||
### TradeDataPanel 重构
|
||||
|
||||
KLineModule 从 611 行精简至 157 行,拆分为独立子组件:
|
||||
|
||||
| 子组件 | 职责 | 行数 |
|
||||
|--------|------|------|
|
||||
| KLineToolbar | 模式切换、指标选择、时间范围 | 275 |
|
||||
| DailyKLineChart | 日K图表渲染、useMemo缓存 | 85 |
|
||||
| MinuteChartWithOrderBook | 分时图、实时行情、五档盘口 | 212 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```tsx
|
||||
import MarketDataView from '@views/Company/components/MarketDataView';
|
||||
|
||||
<MarketDataView stockCode="600000" />
|
||||
```
|
||||
|
||||
### 单独使用 K 线模块
|
||||
|
||||
```tsx
|
||||
import { KLineModule } from '@views/Company/components/MarketDataView/components/panels/TradeDataPanel';
|
||||
|
||||
<KLineModule
|
||||
tradeData={tradeData}
|
||||
minuteData={minuteData}
|
||||
analysisMap={analysisMap}
|
||||
onLoadMinuteData={loadMinuteData}
|
||||
onChartClick={handleChartClick}
|
||||
selectedPeriod={60}
|
||||
onPeriodChange={setPeriod}
|
||||
stockCode="600000"
|
||||
/>
|
||||
```
|
||||
|
||||
### 单独使用子组件
|
||||
|
||||
```tsx
|
||||
import {
|
||||
KLineToolbar,
|
||||
DailyKLineChart,
|
||||
MinuteChartWithOrderBook,
|
||||
} from '@views/Company/components/MarketDataView/components/panels/TradeDataPanel';
|
||||
|
||||
// 仅工具栏
|
||||
<KLineToolbar mode="daily" onModeChange={setMode} ... />
|
||||
|
||||
// 仅日K图表
|
||||
<DailyKLineChart tradeData={data} analysisMap={map} subIndicator="MACD" ... />
|
||||
|
||||
// 仅分时图+盘口
|
||||
<MinuteChartWithOrderBook minuteData={data} stockCode="600000" showOrderBook />
|
||||
```
|
||||
@@ -1,112 +1,24 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx
|
||||
// K线模块 - 日K线/分时图切换展示(黑金主题 + 专业技术指标 + 商品数据叠加 + 分时盘口)
|
||||
|
||||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
VStack,
|
||||
HStack,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Badge,
|
||||
Center,
|
||||
Spinner,
|
||||
Icon,
|
||||
Select,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Tooltip,
|
||||
Grid,
|
||||
GridItem,
|
||||
} from '@chakra-ui/react';
|
||||
import { RepeatIcon, InfoIcon, ChevronDownIcon, ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
|
||||
import { BarChart2, Clock, TrendingUp, Calendar, LineChart, Activity, Pencil } from 'lucide-react';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import React, { useState, useMemo, useCallback, memo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
// 导入实时行情 Hook 和五档盘口组件
|
||||
import { useRealtimeQuote } from '@views/StockOverview/components/FlexScreen/hooks';
|
||||
import OrderBookPanel from '@views/StockOverview/components/FlexScreen/components/OrderBookPanel';
|
||||
|
||||
import { darkGoldTheme, PERIOD_OPTIONS } from '../../../constants';
|
||||
import {
|
||||
getKLineDarkGoldOption,
|
||||
getMinuteKLineDarkGoldOption,
|
||||
type IndicatorType,
|
||||
type MainIndicatorType,
|
||||
type DrawingType,
|
||||
} from '../../../utils/chartOptions';
|
||||
import { darkGoldTheme } from '../../../constants';
|
||||
import type { IndicatorType, MainIndicatorType, DrawingType } from '../../../utils/chartOptions';
|
||||
import type { KLineModuleProps, OverlayMetricData } from '../../../types';
|
||||
import MetricOverlaySearch from './MetricOverlaySearch';
|
||||
import type { ChartMode } from './constants';
|
||||
|
||||
// 空状态组件(内联)
|
||||
const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => (
|
||||
<Center h="300px">
|
||||
<VStack spacing={4}>
|
||||
<Icon as={InfoIcon} color={darkGoldTheme.textMuted} boxSize={12} />
|
||||
<VStack spacing={2}>
|
||||
<Text color={darkGoldTheme.textMuted} fontSize="lg">{title}</Text>
|
||||
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center">{description}</Text>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
// 子组件导入
|
||||
import { KLineToolbar, DailyKLineChart, MinuteChartWithOrderBook } from './components';
|
||||
|
||||
// 重新导出类型供外部使用
|
||||
export type { KLineModuleProps } from '../../../types';
|
||||
|
||||
type ChartMode = 'daily' | 'minute';
|
||||
|
||||
// 副图指标选项
|
||||
const SUB_INDICATOR_OPTIONS: { value: IndicatorType; label: string; description: string }[] = [
|
||||
{ value: 'MACD', label: 'MACD', description: '平滑异同移动平均线' },
|
||||
{ value: 'KDJ', label: 'KDJ', description: '随机指标' },
|
||||
{ value: 'RSI', label: 'RSI', description: '相对强弱指标' },
|
||||
{ value: 'WR', label: 'WR', description: '威廉指标(超买超卖)' },
|
||||
{ value: 'CCI', label: 'CCI', description: '商品通道指标' },
|
||||
{ value: 'BIAS', label: 'BIAS', description: '乖离率' },
|
||||
{ value: 'VOL', label: '仅成交量', description: '不显示副图指标' },
|
||||
];
|
||||
|
||||
// 主图指标选项
|
||||
const MAIN_INDICATOR_OPTIONS: { value: MainIndicatorType; label: string; description: string }[] = [
|
||||
{ value: 'MA', label: 'MA均线', description: 'MA5/MA10/MA20' },
|
||||
{ value: 'BOLL', label: '布林带', description: '布林通道指标' },
|
||||
{ value: 'NONE', label: '无', description: '不显示主图指标' },
|
||||
];
|
||||
|
||||
// 绘图工具选项
|
||||
const DRAWING_OPTIONS: { value: DrawingType; label: string; description: string }[] = [
|
||||
{ value: 'NONE', label: '无', description: '不显示绘图工具' },
|
||||
{ value: 'SUPPORT_RESISTANCE', label: '支撑/阻力', description: '自动识别支撑位和阻力位' },
|
||||
{ value: 'TREND_LINE', label: '趋势线', description: '基于线性回归的趋势线' },
|
||||
{ value: 'ALL', label: '全部显示', description: '显示所有参考线' },
|
||||
];
|
||||
|
||||
// 黑金主题按钮样式(提取到组件外部避免每次渲染重建)
|
||||
const ACTIVE_BUTTON_STYLE = {
|
||||
bg: `linear-gradient(135deg, ${darkGoldTheme.gold} 0%, ${darkGoldTheme.orange} 100%)`,
|
||||
color: '#1a1a2e',
|
||||
borderColor: darkGoldTheme.gold,
|
||||
_hover: {
|
||||
bg: `linear-gradient(135deg, ${darkGoldTheme.goldLight} 0%, ${darkGoldTheme.gold} 100%)`,
|
||||
},
|
||||
} as const;
|
||||
|
||||
const INACTIVE_BUTTON_STYLE = {
|
||||
bg: 'transparent',
|
||||
color: darkGoldTheme.textMuted,
|
||||
borderColor: darkGoldTheme.border,
|
||||
_hover: {
|
||||
bg: 'rgba(212, 175, 55, 0.1)',
|
||||
borderColor: darkGoldTheme.gold,
|
||||
color: darkGoldTheme.gold,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* K线模块主组件
|
||||
* 职责:状态管理、组合子组件、事件处理
|
||||
*/
|
||||
const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
theme,
|
||||
tradeData,
|
||||
@@ -119,6 +31,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
onPeriodChange,
|
||||
stockCode,
|
||||
}) => {
|
||||
// ========== 状态管理 ==========
|
||||
const [mode, setMode] = useState<ChartMode>('daily');
|
||||
const [subIndicator, setSubIndicator] = useState<IndicatorType>('MACD');
|
||||
const [mainIndicator, setMainIndicator] = useState<MainIndicatorType>('MA');
|
||||
@@ -126,33 +39,10 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
const [drawingType, setDrawingType] = useState<DrawingType>('NONE');
|
||||
const [overlayMetrics, setOverlayMetrics] = useState<OverlayMetricData[]>([]);
|
||||
const [showOrderBook, setShowOrderBook] = useState<boolean>(true);
|
||||
|
||||
// ========== 计算属性 ==========
|
||||
const hasMinuteData = minuteData && minuteData.data && minuteData.data.length > 0;
|
||||
|
||||
// 实时行情数据(用于五档盘口)
|
||||
const subscribedCodes = useMemo(() => {
|
||||
if (!stockCode || mode !== 'minute') return [];
|
||||
return [stockCode];
|
||||
}, [stockCode, mode]);
|
||||
|
||||
const { quotes, connected } = useRealtimeQuote(subscribedCodes);
|
||||
|
||||
// 获取当前股票的行情数据
|
||||
const currentQuote = useMemo(() => {
|
||||
if (!stockCode) return null;
|
||||
// 尝试不同的代码格式
|
||||
return quotes[stockCode] || quotes[`${stockCode}.SH`] || quotes[`${stockCode}.SZ`] || null;
|
||||
}, [quotes, stockCode]);
|
||||
|
||||
// 判断是否在交易时间
|
||||
const isInTradingHours = useMemo(() => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours();
|
||||
const minutes = now.getMinutes();
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
// 9:15-11:30 或 13:00-15:00
|
||||
return (totalMinutes >= 555 && totalMinutes <= 690) || (totalMinutes >= 780 && totalMinutes <= 900);
|
||||
}, []);
|
||||
|
||||
// 计算股票数据的日期范围(用于查询商品数据)
|
||||
const stockDateRange = useMemo(() => {
|
||||
if (tradeData.length === 0) return undefined;
|
||||
@@ -162,6 +52,26 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
};
|
||||
}, [tradeData]);
|
||||
|
||||
// ========== 事件处理 ==========
|
||||
|
||||
// 切换到分时模式时自动加载数据
|
||||
const handleModeChange = useCallback((newMode: ChartMode) => {
|
||||
setMode(newMode);
|
||||
if (newMode === 'minute' && !hasMinuteData && !minuteLoading) {
|
||||
onLoadMinuteData();
|
||||
}
|
||||
}, [hasMinuteData, minuteLoading, onLoadMinuteData]);
|
||||
|
||||
// 切换显示/隐藏分析
|
||||
const handleToggleAnalysis = useCallback(() => {
|
||||
setShowAnalysis(prev => !prev);
|
||||
}, []);
|
||||
|
||||
// 切换显示/隐藏盘口
|
||||
const handleToggleOrderBook = useCallback(() => {
|
||||
setShowOrderBook(prev => !prev);
|
||||
}, []);
|
||||
|
||||
// 添加叠加指标
|
||||
const handleAddOverlayMetric = useCallback((metric: OverlayMetricData) => {
|
||||
setOverlayMetrics(prev => [...prev, metric]);
|
||||
@@ -172,440 +82,75 @@ const KLineModule: React.FC<KLineModuleProps> = ({
|
||||
setOverlayMetrics(prev => prev.filter(m => m.metric_id !== metricId));
|
||||
}, []);
|
||||
|
||||
// 切换到分时模式时自动加载数据(使用 useCallback 避免不必要的重渲染)
|
||||
const handleModeChange = useCallback((newMode: ChartMode) => {
|
||||
setMode(newMode);
|
||||
if (newMode === 'minute' && !hasMinuteData && !minuteLoading) {
|
||||
onLoadMinuteData();
|
||||
}
|
||||
}, [hasMinuteData, minuteLoading, onLoadMinuteData]);
|
||||
// 主图指标变更
|
||||
const handleMainIndicatorChange = useCallback((indicator: MainIndicatorType) => {
|
||||
setMainIndicator(indicator);
|
||||
}, []);
|
||||
|
||||
// 副图指标变更
|
||||
const handleSubIndicatorChange = useCallback((indicator: IndicatorType) => {
|
||||
setSubIndicator(indicator);
|
||||
}, []);
|
||||
|
||||
// 绘图工具变更
|
||||
const handleDrawingTypeChange = useCallback((type: DrawingType) => {
|
||||
setDrawingType(type);
|
||||
}, []);
|
||||
|
||||
// ========== 渲染 ==========
|
||||
return (
|
||||
<Box
|
||||
bg="transparent"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* 卡片头部 */}
|
||||
<Box py={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
||||
<HStack justify="space-between" align="center" flexWrap="wrap" gap={2}>
|
||||
<HStack spacing={3}>
|
||||
<Box
|
||||
p={2}
|
||||
borderRadius="lg"
|
||||
bg={darkGoldTheme.tagBg}
|
||||
>
|
||||
{mode === 'daily' ? (
|
||||
<TrendingUp size={20} color={darkGoldTheme.gold} />
|
||||
) : (
|
||||
<LineChart size={20} color={darkGoldTheme.gold} />
|
||||
)}
|
||||
</Box>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
bgGradient={`linear(to-r, ${darkGoldTheme.gold}, ${darkGoldTheme.orange})`}
|
||||
bgClip="text"
|
||||
>
|
||||
{mode === 'daily' ? '日K线图' : '分时走势'}
|
||||
</Text>
|
||||
{mode === 'minute' && minuteData?.trade_date && (
|
||||
<Badge
|
||||
bg={darkGoldTheme.tagBg}
|
||||
color={darkGoldTheme.gold}
|
||||
fontSize="xs"
|
||||
px={2}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
>
|
||||
{minuteData.trade_date}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{/* 日K模式下显示时间范围选择器和指标选择 */}
|
||||
{mode === 'daily' && (
|
||||
<>
|
||||
{/* 时间范围选择器 */}
|
||||
{onPeriodChange && (
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Calendar} boxSize={4} color={darkGoldTheme.textMuted} />
|
||||
<Select
|
||||
size="sm"
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => onPeriodChange(Number(e.target.value))}
|
||||
bg="transparent"
|
||||
borderColor={darkGoldTheme.border}
|
||||
color={darkGoldTheme.textPrimary}
|
||||
maxW="85px"
|
||||
_hover={{ borderColor: darkGoldTheme.gold }}
|
||||
_focus={{ borderColor: darkGoldTheme.gold, boxShadow: 'none' }}
|
||||
sx={{
|
||||
option: {
|
||||
background: '#1a1a2e',
|
||||
color: darkGoldTheme.textPrimary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{PERIOD_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 隐藏/显示涨幅分析 */}
|
||||
<Tooltip label={showAnalysis ? '隐藏涨幅分析标记' : '显示涨幅分析标记'} placement="top" hasArrow>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
leftIcon={showAnalysis ? <ViewOffIcon /> : <ViewIcon />}
|
||||
onClick={() => setShowAnalysis(!showAnalysis)}
|
||||
{...(showAnalysis ? INACTIVE_BUTTON_STYLE : ACTIVE_BUTTON_STYLE)}
|
||||
minW="90px"
|
||||
>
|
||||
{showAnalysis ? '隐藏分析' : '显示分析'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* 主图指标选择 */}
|
||||
<Menu>
|
||||
<Tooltip label="主图指标" placement="top" hasArrow>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
minW="90px"
|
||||
>
|
||||
{MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList
|
||||
bg="#1a1a2e"
|
||||
borderColor={darkGoldTheme.border}
|
||||
boxShadow="0 4px 20px rgba(0,0,0,0.5)"
|
||||
>
|
||||
{MAIN_INDICATOR_OPTIONS.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
onClick={() => setMainIndicator(option.value)}
|
||||
bg={mainIndicator === option.value ? 'rgba(212, 175, 55, 0.2)' : 'transparent'}
|
||||
color={mainIndicator === option.value ? darkGoldTheme.gold : darkGoldTheme.textPrimary}
|
||||
_hover={{ bg: 'rgba(212, 175, 55, 0.1)' }}
|
||||
>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontSize="sm" fontWeight={mainIndicator === option.value ? 'bold' : 'normal'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
{/* 副图指标选择 */}
|
||||
<Menu>
|
||||
<Tooltip label="副图指标" placement="top" hasArrow>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
leftIcon={<Activity size={14} />}
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
minW="100px"
|
||||
>
|
||||
{SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList
|
||||
bg="#1a1a2e"
|
||||
borderColor={darkGoldTheme.border}
|
||||
boxShadow="0 4px 20px rgba(0,0,0,0.5)"
|
||||
>
|
||||
{SUB_INDICATOR_OPTIONS.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
onClick={() => setSubIndicator(option.value)}
|
||||
bg={subIndicator === option.value ? 'rgba(212, 175, 55, 0.2)' : 'transparent'}
|
||||
color={subIndicator === option.value ? darkGoldTheme.gold : darkGoldTheme.textPrimary}
|
||||
_hover={{ bg: 'rgba(212, 175, 55, 0.1)' }}
|
||||
>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontSize="sm" fontWeight={subIndicator === option.value ? 'bold' : 'normal'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
{/* 绘图工具选择 */}
|
||||
<Menu>
|
||||
<Tooltip label="绘图工具" placement="top" hasArrow>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
leftIcon={<Pencil size={14} />}
|
||||
{...(drawingType !== 'NONE' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
minW="90px"
|
||||
>
|
||||
{DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList
|
||||
bg="#1a1a2e"
|
||||
borderColor={darkGoldTheme.border}
|
||||
boxShadow="0 4px 20px rgba(0,0,0,0.5)"
|
||||
>
|
||||
{DRAWING_OPTIONS.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
onClick={() => setDrawingType(option.value)}
|
||||
bg={drawingType === option.value ? 'rgba(212, 175, 55, 0.2)' : 'transparent'}
|
||||
color={drawingType === option.value ? darkGoldTheme.gold : darkGoldTheme.textPrimary}
|
||||
_hover={{ bg: 'rgba(212, 175, 55, 0.1)' }}
|
||||
>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontSize="sm" fontWeight={drawingType === option.value ? 'bold' : 'normal'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
{/* 商品数据叠加搜索 */}
|
||||
<MetricOverlaySearch
|
||||
<Box bg="transparent" overflow="hidden">
|
||||
{/* 工具栏 */}
|
||||
<KLineToolbar
|
||||
mode={mode}
|
||||
onModeChange={handleModeChange}
|
||||
selectedPeriod={selectedPeriod}
|
||||
onPeriodChange={onPeriodChange}
|
||||
showAnalysis={showAnalysis}
|
||||
onToggleAnalysis={handleToggleAnalysis}
|
||||
mainIndicator={mainIndicator}
|
||||
onMainIndicatorChange={handleMainIndicatorChange}
|
||||
subIndicator={subIndicator}
|
||||
onSubIndicatorChange={handleSubIndicatorChange}
|
||||
drawingType={drawingType}
|
||||
onDrawingTypeChange={handleDrawingTypeChange}
|
||||
overlayMetrics={overlayMetrics}
|
||||
onAddMetric={handleAddOverlayMetric}
|
||||
onRemoveMetric={handleRemoveOverlayMetric}
|
||||
onAddOverlayMetric={handleAddOverlayMetric}
|
||||
onRemoveOverlayMetric={handleRemoveOverlayMetric}
|
||||
stockDateRange={stockDateRange}
|
||||
minuteData={minuteData}
|
||||
minuteLoading={minuteLoading}
|
||||
showOrderBook={showOrderBook}
|
||||
onToggleOrderBook={handleToggleOrderBook}
|
||||
onRefreshMinuteData={onLoadMinuteData}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 分时模式下的控制按钮 */}
|
||||
{mode === 'minute' && (
|
||||
<>
|
||||
{/* 显示/隐藏盘口 */}
|
||||
<Tooltip label={showOrderBook ? '隐藏盘口' : '显示盘口'} placement="top" hasArrow>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowOrderBook(!showOrderBook)}
|
||||
{...(showOrderBook ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
minW="80px"
|
||||
>
|
||||
{showOrderBook ? '隐藏盘口' : '显示盘口'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<Button
|
||||
leftIcon={<RepeatIcon />}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onLoadMinuteData}
|
||||
isLoading={minuteLoading}
|
||||
loadingText="获取中"
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 模式切换按钮组 */}
|
||||
<ButtonGroup size="sm" isAttached>
|
||||
<Button
|
||||
leftIcon={<BarChart2 size={14} />}
|
||||
onClick={() => handleModeChange('daily')}
|
||||
{...(mode === 'daily' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
>
|
||||
日K
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<LineChart size={14} />}
|
||||
onClick={() => handleModeChange('minute')}
|
||||
{...(mode === 'minute' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
>
|
||||
分时
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* 卡片内容 */}
|
||||
{/* 图表内容区域 */}
|
||||
<Box pt={4}>
|
||||
{mode === 'daily' ? (
|
||||
// 日K线图(带技术指标)
|
||||
tradeData.length > 0 ? (
|
||||
<Box h="650px">
|
||||
<ReactECharts
|
||||
option={getKLineDarkGoldOption(tradeData, analysisMap, subIndicator, mainIndicator, showAnalysis, drawingType, overlayMetrics)}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
theme="dark"
|
||||
notMerge={true}
|
||||
onEvents={{ click: onChartClick }}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
// 日K线图
|
||||
<DailyKLineChart
|
||||
tradeData={tradeData}
|
||||
analysisMap={analysisMap}
|
||||
subIndicator={subIndicator}
|
||||
mainIndicator={mainIndicator}
|
||||
showAnalysis={showAnalysis}
|
||||
drawingType={drawingType}
|
||||
overlayMetrics={overlayMetrics}
|
||||
onChartClick={onChartClick}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<EmptyState title="暂无日K线数据" description="该股票暂无交易数据" />
|
||||
)
|
||||
) : (
|
||||
// 分时走势图 + 五档盘口
|
||||
minuteLoading ? (
|
||||
<Center h="450px">
|
||||
<VStack spacing={4}>
|
||||
<Spinner
|
||||
thickness="4px"
|
||||
speed="0.65s"
|
||||
emptyColor="rgba(212, 175, 55, 0.2)"
|
||||
color={darkGoldTheme.gold}
|
||||
size="lg"
|
||||
<MinuteChartWithOrderBook
|
||||
minuteData={minuteData}
|
||||
minuteLoading={minuteLoading}
|
||||
stockCode={stockCode}
|
||||
showOrderBook={showOrderBook}
|
||||
/>
|
||||
<Text color={darkGoldTheme.textMuted} fontSize="sm">
|
||||
加载分时数据中...
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : hasMinuteData ? (
|
||||
<Grid templateColumns={showOrderBook ? '1fr 220px' : '1fr'} gap={4} h="450px">
|
||||
{/* 分时图表 */}
|
||||
<GridItem>
|
||||
<Box h="100%">
|
||||
<ReactECharts
|
||||
option={getMinuteKLineDarkGoldOption(minuteData)}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
theme="dark"
|
||||
notMerge={true}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
/>
|
||||
</Box>
|
||||
</GridItem>
|
||||
|
||||
{/* 五档盘口 */}
|
||||
{showOrderBook && (
|
||||
<GridItem>
|
||||
<Box
|
||||
h="100%"
|
||||
bg="rgba(0, 0, 0, 0.3)"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={darkGoldTheme.border}
|
||||
p={3}
|
||||
overflowY="auto"
|
||||
>
|
||||
{/* 盘口标题 */}
|
||||
<HStack justify="space-between" mb={3}>
|
||||
<Text fontSize="sm" fontWeight="bold" color={darkGoldTheme.gold}>
|
||||
五档盘口
|
||||
</Text>
|
||||
{/* 连接状态指示 */}
|
||||
<HStack spacing={1}>
|
||||
{isInTradingHours && (
|
||||
<Badge
|
||||
bg={connected.SSE || connected.SZSE ? 'green.500' : 'gray.500'}
|
||||
color="white"
|
||||
fontSize="2xs"
|
||||
px={1}
|
||||
>
|
||||
{connected.SSE || connected.SZSE ? '实时' : '离线'}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 当前价格信息 */}
|
||||
{currentQuote && (
|
||||
<VStack spacing={1} mb={3} align="stretch">
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>当前价</Text>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
color={
|
||||
currentQuote.changePct > 0 ? '#ff4d4d' :
|
||||
currentQuote.changePct < 0 ? '#22c55e' :
|
||||
darkGoldTheme.textPrimary
|
||||
}
|
||||
>
|
||||
{currentQuote.price?.toFixed(2) || '-'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>涨跌幅</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={
|
||||
currentQuote.changePct > 0 ? '#ff4d4d' :
|
||||
currentQuote.changePct < 0 ? '#22c55e' :
|
||||
darkGoldTheme.textMuted
|
||||
}
|
||||
>
|
||||
{currentQuote.changePct > 0 ? '+' : ''}{currentQuote.changePct?.toFixed(2) || '0.00'}%
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* 五档盘口面板 */}
|
||||
{currentQuote && (currentQuote.bidPrices?.length > 0 || currentQuote.askPrices?.length > 0) ? (
|
||||
<OrderBookPanel
|
||||
bidPrices={currentQuote.bidPrices || []}
|
||||
bidVolumes={currentQuote.bidVolumes || []}
|
||||
askPrices={currentQuote.askPrices || []}
|
||||
askVolumes={currentQuote.askVolumes || []}
|
||||
prevClose={currentQuote.prevClose}
|
||||
upperLimit={'upperLimit' in currentQuote ? currentQuote.upperLimit : undefined}
|
||||
lowerLimit={'lowerLimit' in currentQuote ? currentQuote.lowerLimit : undefined}
|
||||
defaultLevels={5}
|
||||
/>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
<VStack spacing={2}>
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||
{isInTradingHours ? '获取盘口数据中...' : '非交易时间'}
|
||||
</Text>
|
||||
{!isInTradingHours && (
|
||||
<Text fontSize="2xs" color={darkGoldTheme.textMuted}>
|
||||
交易时间: 9:30-11:30, 13:00-15:00
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</GridItem>
|
||||
)}
|
||||
</Grid>
|
||||
) : (
|
||||
<EmptyState title="暂无分时数据" description="点击刷新按钮获取当日分时数据" />
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default KLineModule;
|
||||
export default memo(KLineModule);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/DailyKLineChart.tsx
|
||||
// 日K线图表组件
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Box, Text, VStack, Center, Icon } from '@chakra-ui/react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
|
||||
import { darkGoldTheme } from '../../../../constants';
|
||||
import {
|
||||
getKLineDarkGoldOption,
|
||||
type IndicatorType,
|
||||
type MainIndicatorType,
|
||||
type DrawingType,
|
||||
} from '../../../../utils/chartOptions';
|
||||
import type { TradeDayData, RiseAnalysis, OverlayMetricData } from '../../../../types';
|
||||
|
||||
export interface DailyKLineChartProps {
|
||||
tradeData: TradeDayData[];
|
||||
analysisMap: Record<number, RiseAnalysis>;
|
||||
subIndicator: IndicatorType;
|
||||
mainIndicator: MainIndicatorType;
|
||||
showAnalysis: boolean;
|
||||
drawingType: DrawingType;
|
||||
overlayMetrics: OverlayMetricData[];
|
||||
onChartClick?: (params: { seriesName?: string; data?: [number, number] }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 空状态组件
|
||||
*/
|
||||
const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => (
|
||||
<Center h="300px">
|
||||
<VStack spacing={4}>
|
||||
<Icon as={InfoIcon} color={darkGoldTheme.textMuted} boxSize={12} />
|
||||
<VStack spacing={2}>
|
||||
<Text color={darkGoldTheme.textMuted} fontSize="lg">{title}</Text>
|
||||
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center">{description}</Text>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
|
||||
/**
|
||||
* 日K线图表组件
|
||||
*/
|
||||
const DailyKLineChart: React.FC<DailyKLineChartProps> = ({
|
||||
tradeData,
|
||||
analysisMap,
|
||||
subIndicator,
|
||||
mainIndicator,
|
||||
showAnalysis,
|
||||
drawingType,
|
||||
overlayMetrics,
|
||||
onChartClick,
|
||||
}) => {
|
||||
// 缓存图表配置
|
||||
const chartOption = useMemo(() => {
|
||||
if (tradeData.length === 0) return {};
|
||||
return getKLineDarkGoldOption(
|
||||
tradeData,
|
||||
analysisMap,
|
||||
subIndicator,
|
||||
mainIndicator,
|
||||
showAnalysis,
|
||||
drawingType,
|
||||
overlayMetrics
|
||||
);
|
||||
}, [tradeData, analysisMap, subIndicator, mainIndicator, showAnalysis, drawingType, overlayMetrics]);
|
||||
|
||||
// 空数据状态
|
||||
if (tradeData.length === 0) {
|
||||
return <EmptyState title="暂无日K线数据" description="该股票暂无交易数据" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box h="650px">
|
||||
<ReactECharts
|
||||
option={chartOption}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
theme="dark"
|
||||
notMerge={true}
|
||||
onEvents={onChartClick ? { click: onChartClick } : undefined}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DailyKLineChart);
|
||||
@@ -0,0 +1,335 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/KLineToolbar.tsx
|
||||
// K线工具栏组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
VStack,
|
||||
HStack,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Badge,
|
||||
Icon,
|
||||
Select,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { RepeatIcon, ChevronDownIcon, ViewIcon, ViewOffIcon } from '@chakra-ui/icons';
|
||||
import { BarChart2, TrendingUp, Calendar, LineChart, Activity, Pencil } from 'lucide-react';
|
||||
|
||||
import { darkGoldTheme, PERIOD_OPTIONS } from '../../../../constants';
|
||||
import type { IndicatorType, MainIndicatorType, DrawingType } from '../../../../utils/chartOptions';
|
||||
import type { OverlayMetricData, MinuteData } from '../../../../types';
|
||||
import {
|
||||
SUB_INDICATOR_OPTIONS,
|
||||
MAIN_INDICATOR_OPTIONS,
|
||||
DRAWING_OPTIONS,
|
||||
type ChartMode,
|
||||
} from '../constants';
|
||||
import {
|
||||
ACTIVE_BUTTON_STYLE,
|
||||
INACTIVE_BUTTON_STYLE,
|
||||
MENU_LIST_STYLE,
|
||||
getMenuItemStyle,
|
||||
SELECT_STYLE,
|
||||
} from '../styles';
|
||||
import MetricOverlaySearch from '../MetricOverlaySearch';
|
||||
|
||||
export interface KLineToolbarProps {
|
||||
// 模式相关
|
||||
mode: ChartMode;
|
||||
onModeChange: (mode: ChartMode) => void;
|
||||
|
||||
// 日K模式
|
||||
selectedPeriod?: number;
|
||||
onPeriodChange?: (period: number) => void;
|
||||
showAnalysis: boolean;
|
||||
onToggleAnalysis: () => void;
|
||||
mainIndicator: MainIndicatorType;
|
||||
onMainIndicatorChange: (indicator: MainIndicatorType) => void;
|
||||
subIndicator: IndicatorType;
|
||||
onSubIndicatorChange: (indicator: IndicatorType) => void;
|
||||
drawingType: DrawingType;
|
||||
onDrawingTypeChange: (type: DrawingType) => void;
|
||||
overlayMetrics: OverlayMetricData[];
|
||||
onAddOverlayMetric: (metric: OverlayMetricData) => void;
|
||||
onRemoveOverlayMetric: (metricId: string) => void;
|
||||
stockDateRange?: { startDate: string; endDate: string };
|
||||
|
||||
// 分时模式
|
||||
minuteData?: MinuteData | null;
|
||||
minuteLoading: boolean;
|
||||
showOrderBook: boolean;
|
||||
onToggleOrderBook: () => void;
|
||||
onRefreshMinuteData: () => void;
|
||||
}
|
||||
|
||||
const KLineToolbar: React.FC<KLineToolbarProps> = ({
|
||||
mode,
|
||||
onModeChange,
|
||||
selectedPeriod,
|
||||
onPeriodChange,
|
||||
showAnalysis,
|
||||
onToggleAnalysis,
|
||||
mainIndicator,
|
||||
onMainIndicatorChange,
|
||||
subIndicator,
|
||||
onSubIndicatorChange,
|
||||
drawingType,
|
||||
onDrawingTypeChange,
|
||||
overlayMetrics,
|
||||
onAddOverlayMetric,
|
||||
onRemoveOverlayMetric,
|
||||
stockDateRange,
|
||||
minuteData,
|
||||
minuteLoading,
|
||||
showOrderBook,
|
||||
onToggleOrderBook,
|
||||
onRefreshMinuteData,
|
||||
}) => {
|
||||
return (
|
||||
<Box py={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
|
||||
<HStack justify="space-between" align="center" flexWrap="wrap" gap={2}>
|
||||
{/* 左侧标题区域 */}
|
||||
<HStack spacing={3}>
|
||||
<Box p={2} borderRadius="lg" bg={darkGoldTheme.tagBg}>
|
||||
{mode === 'daily' ? (
|
||||
<TrendingUp size={20} color={darkGoldTheme.gold} />
|
||||
) : (
|
||||
<LineChart size={20} color={darkGoldTheme.gold} />
|
||||
)}
|
||||
</Box>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
bgGradient={`linear(to-r, ${darkGoldTheme.gold}, ${darkGoldTheme.orange})`}
|
||||
bgClip="text"
|
||||
>
|
||||
{mode === 'daily' ? '日K线图' : '分时走势'}
|
||||
</Text>
|
||||
{mode === 'minute' && minuteData?.trade_date && (
|
||||
<Badge
|
||||
bg={darkGoldTheme.tagBg}
|
||||
color={darkGoldTheme.gold}
|
||||
fontSize="xs"
|
||||
px={2}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
>
|
||||
{minuteData.trade_date}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{/* 右侧控制按钮区域 */}
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{/* 日K模式下的控制按钮 */}
|
||||
{mode === 'daily' && (
|
||||
<>
|
||||
{/* 时间范围选择器 */}
|
||||
{onPeriodChange && (
|
||||
<HStack spacing={1}>
|
||||
<Icon as={Calendar} boxSize={4} color={darkGoldTheme.textMuted} />
|
||||
<Select
|
||||
size="sm"
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => onPeriodChange(Number(e.target.value))}
|
||||
maxW="85px"
|
||||
{...SELECT_STYLE}
|
||||
>
|
||||
{PERIOD_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{/* 隐藏/显示涨幅分析 */}
|
||||
<Tooltip label={showAnalysis ? '隐藏涨幅分析标记' : '显示涨幅分析标记'} placement="top" hasArrow>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
leftIcon={showAnalysis ? <ViewOffIcon /> : <ViewIcon />}
|
||||
onClick={onToggleAnalysis}
|
||||
{...(showAnalysis ? INACTIVE_BUTTON_STYLE : ACTIVE_BUTTON_STYLE)}
|
||||
minW="90px"
|
||||
>
|
||||
{showAnalysis ? '隐藏分析' : '显示分析'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* 主图指标选择 */}
|
||||
<Menu>
|
||||
<Tooltip label="主图指标" placement="top" hasArrow>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
minW="90px"
|
||||
>
|
||||
{MAIN_INDICATOR_OPTIONS.find(o => o.value === mainIndicator)?.label || 'MA均线'}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList {...MENU_LIST_STYLE}>
|
||||
{MAIN_INDICATOR_OPTIONS.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
onClick={() => onMainIndicatorChange(option.value)}
|
||||
{...getMenuItemStyle(mainIndicator === option.value)}
|
||||
>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontSize="sm" fontWeight={mainIndicator === option.value ? 'bold' : 'normal'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
{/* 副图指标选择 */}
|
||||
<Menu>
|
||||
<Tooltip label="副图指标" placement="top" hasArrow>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
leftIcon={<Activity size={14} />}
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
minW="100px"
|
||||
>
|
||||
{SUB_INDICATOR_OPTIONS.find(o => o.value === subIndicator)?.label || 'MACD'}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList {...MENU_LIST_STYLE}>
|
||||
{SUB_INDICATOR_OPTIONS.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
onClick={() => onSubIndicatorChange(option.value)}
|
||||
{...getMenuItemStyle(subIndicator === option.value)}
|
||||
>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontSize="sm" fontWeight={subIndicator === option.value ? 'bold' : 'normal'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
{/* 绘图工具选择 */}
|
||||
<Menu>
|
||||
<Tooltip label="绘图工具" placement="top" hasArrow>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
leftIcon={<Pencil size={14} />}
|
||||
{...(drawingType !== 'NONE' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
minW="90px"
|
||||
>
|
||||
{DRAWING_OPTIONS.find(o => o.value === drawingType)?.label || '绘图'}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList {...MENU_LIST_STYLE}>
|
||||
{DRAWING_OPTIONS.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
onClick={() => onDrawingTypeChange(option.value)}
|
||||
{...getMenuItemStyle(drawingType === option.value)}
|
||||
>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontSize="sm" fontWeight={drawingType === option.value ? 'bold' : 'normal'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
{/* 商品数据叠加搜索 */}
|
||||
<MetricOverlaySearch
|
||||
overlayMetrics={overlayMetrics}
|
||||
onAddMetric={onAddOverlayMetric}
|
||||
onRemoveMetric={onRemoveOverlayMetric}
|
||||
stockDateRange={stockDateRange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 分时模式下的控制按钮 */}
|
||||
{mode === 'minute' && (
|
||||
<>
|
||||
{/* 显示/隐藏盘口 */}
|
||||
<Tooltip label={showOrderBook ? '隐藏盘口' : '显示盘口'} placement="top" hasArrow>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onToggleOrderBook}
|
||||
{...(showOrderBook ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
minW="80px"
|
||||
>
|
||||
{showOrderBook ? '隐藏盘口' : '显示盘口'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<Button
|
||||
leftIcon={<RepeatIcon />}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onRefreshMinuteData}
|
||||
isLoading={minuteLoading}
|
||||
loadingText="获取中"
|
||||
{...INACTIVE_BUTTON_STYLE}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 模式切换按钮组 */}
|
||||
<ButtonGroup size="sm" isAttached>
|
||||
<Button
|
||||
leftIcon={<BarChart2 size={14} />}
|
||||
onClick={() => onModeChange('daily')}
|
||||
{...(mode === 'daily' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
>
|
||||
日K
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<LineChart size={14} />}
|
||||
onClick={() => onModeChange('minute')}
|
||||
{...(mode === 'minute' ? ACTIVE_BUTTON_STYLE : INACTIVE_BUTTON_STYLE)}
|
||||
>
|
||||
分时
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(KLineToolbar);
|
||||
@@ -0,0 +1,229 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/MinuteChartWithOrderBook.tsx
|
||||
// 分时图 + 五档盘口组件
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
VStack,
|
||||
HStack,
|
||||
Center,
|
||||
Spinner,
|
||||
Badge,
|
||||
Grid,
|
||||
GridItem,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
|
||||
// 导入实时行情 Hook 和五档盘口组件
|
||||
import { useRealtimeQuote } from '@views/StockOverview/components/FlexScreen/hooks';
|
||||
import OrderBookPanel from '@views/StockOverview/components/FlexScreen/components/OrderBookPanel';
|
||||
|
||||
import { darkGoldTheme } from '../../../../constants';
|
||||
import { getMinuteKLineDarkGoldOption } from '../../../../utils/chartOptions';
|
||||
import type { MinuteData } from '../../../../types';
|
||||
|
||||
export interface MinuteChartWithOrderBookProps {
|
||||
minuteData: MinuteData | null;
|
||||
minuteLoading: boolean;
|
||||
stockCode?: string;
|
||||
showOrderBook: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 空状态组件
|
||||
*/
|
||||
const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => (
|
||||
<Center h="300px">
|
||||
<VStack spacing={4}>
|
||||
<Icon as={InfoIcon} color={darkGoldTheme.textMuted} boxSize={12} />
|
||||
<VStack spacing={2}>
|
||||
<Text color={darkGoldTheme.textMuted} fontSize="lg">{title}</Text>
|
||||
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center">{description}</Text>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
|
||||
/**
|
||||
* 分时图 + 五档盘口组件
|
||||
*/
|
||||
const MinuteChartWithOrderBook: React.FC<MinuteChartWithOrderBookProps> = ({
|
||||
minuteData,
|
||||
minuteLoading,
|
||||
stockCode,
|
||||
showOrderBook,
|
||||
}) => {
|
||||
const hasMinuteData = minuteData && minuteData.data && minuteData.data.length > 0;
|
||||
|
||||
// 实时行情订阅
|
||||
const subscribedCodes = useMemo(() => {
|
||||
if (!stockCode) return [];
|
||||
return [stockCode];
|
||||
}, [stockCode]);
|
||||
|
||||
const { quotes, connected } = useRealtimeQuote(subscribedCodes);
|
||||
|
||||
// 获取当前股票的行情数据
|
||||
const currentQuote = useMemo(() => {
|
||||
if (!stockCode) return null;
|
||||
return quotes[stockCode] || quotes[`${stockCode}.SH`] || quotes[`${stockCode}.SZ`] || null;
|
||||
}, [quotes, stockCode]);
|
||||
|
||||
// 判断是否在交易时间
|
||||
const isInTradingHours = useMemo(() => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours();
|
||||
const minutes = now.getMinutes();
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
// 9:15-11:30 或 13:00-15:00
|
||||
return (totalMinutes >= 555 && totalMinutes <= 690) || (totalMinutes >= 780 && totalMinutes <= 900);
|
||||
}, []);
|
||||
|
||||
// 缓存图表配置
|
||||
const chartOption = useMemo(() => {
|
||||
if (!minuteData) return {};
|
||||
return getMinuteKLineDarkGoldOption(minuteData);
|
||||
}, [minuteData]);
|
||||
|
||||
// 加载中状态
|
||||
if (minuteLoading) {
|
||||
return (
|
||||
<Center h="450px">
|
||||
<VStack spacing={4}>
|
||||
<Spinner
|
||||
thickness="4px"
|
||||
speed="0.65s"
|
||||
emptyColor="rgba(212, 175, 55, 0.2)"
|
||||
color={darkGoldTheme.gold}
|
||||
size="lg"
|
||||
/>
|
||||
<Text color={darkGoldTheme.textMuted} fontSize="sm">
|
||||
加载分时数据中...
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
// 无数据状态
|
||||
if (!hasMinuteData) {
|
||||
return <EmptyState title="暂无分时数据" description="点击刷新按钮获取当日分时数据" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid templateColumns={showOrderBook ? '1fr 220px' : '1fr'} gap={4} h="450px">
|
||||
{/* 分时图表 */}
|
||||
<GridItem>
|
||||
<Box h="100%">
|
||||
<ReactECharts
|
||||
option={chartOption}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
theme="dark"
|
||||
notMerge={true}
|
||||
opts={{ renderer: 'canvas' }}
|
||||
/>
|
||||
</Box>
|
||||
</GridItem>
|
||||
|
||||
{/* 五档盘口 */}
|
||||
{showOrderBook && (
|
||||
<GridItem>
|
||||
<Box
|
||||
h="100%"
|
||||
bg="rgba(0, 0, 0, 0.3)"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={darkGoldTheme.border}
|
||||
p={3}
|
||||
overflowY="auto"
|
||||
>
|
||||
{/* 盘口标题 */}
|
||||
<HStack justify="space-between" mb={3}>
|
||||
<Text fontSize="sm" fontWeight="bold" color={darkGoldTheme.gold}>
|
||||
五档盘口
|
||||
</Text>
|
||||
{/* 连接状态指示 */}
|
||||
<HStack spacing={1}>
|
||||
{isInTradingHours && (
|
||||
<Badge
|
||||
bg={connected.SSE || connected.SZSE ? 'green.500' : 'gray.500'}
|
||||
color="white"
|
||||
fontSize="2xs"
|
||||
px={1}
|
||||
>
|
||||
{connected.SSE || connected.SZSE ? '实时' : '离线'}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 当前价格信息 */}
|
||||
{currentQuote && (
|
||||
<VStack spacing={1} mb={3} align="stretch">
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>当前价</Text>
|
||||
<Text
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
color={
|
||||
currentQuote.changePct > 0 ? '#ff4d4d' :
|
||||
currentQuote.changePct < 0 ? '#22c55e' :
|
||||
darkGoldTheme.textPrimary
|
||||
}
|
||||
>
|
||||
{currentQuote.price?.toFixed(2) || '-'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>涨跌幅</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={
|
||||
currentQuote.changePct > 0 ? '#ff4d4d' :
|
||||
currentQuote.changePct < 0 ? '#22c55e' :
|
||||
darkGoldTheme.textMuted
|
||||
}
|
||||
>
|
||||
{currentQuote.changePct > 0 ? '+' : ''}{currentQuote.changePct?.toFixed(2) || '0.00'}%
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* 五档盘口面板 */}
|
||||
{currentQuote && (currentQuote.bidPrices?.length > 0 || currentQuote.askPrices?.length > 0) ? (
|
||||
<OrderBookPanel
|
||||
bidPrices={currentQuote.bidPrices || []}
|
||||
bidVolumes={currentQuote.bidVolumes || []}
|
||||
askPrices={currentQuote.askPrices || []}
|
||||
askVolumes={currentQuote.askVolumes || []}
|
||||
prevClose={currentQuote.prevClose}
|
||||
upperLimit={'upperLimit' in currentQuote ? currentQuote.upperLimit : undefined}
|
||||
lowerLimit={'lowerLimit' in currentQuote ? currentQuote.lowerLimit : undefined}
|
||||
defaultLevels={5}
|
||||
/>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
<VStack spacing={2}>
|
||||
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
|
||||
{isInTradingHours ? '获取盘口数据中...' : '非交易时间'}
|
||||
</Text>
|
||||
{!isInTradingHours && (
|
||||
<Text fontSize="2xs" color={darkGoldTheme.textMuted}>
|
||||
交易时间: 9:30-11:30, 13:00-15:00
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</GridItem>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MinuteChartWithOrderBook);
|
||||
@@ -0,0 +1,11 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/components/index.ts
|
||||
// 子组件统一导出
|
||||
|
||||
export { default as KLineToolbar } from './KLineToolbar';
|
||||
export type { KLineToolbarProps } from './KLineToolbar';
|
||||
|
||||
export { default as DailyKLineChart } from './DailyKLineChart';
|
||||
export type { DailyKLineChartProps } from './DailyKLineChart';
|
||||
|
||||
export { default as MinuteChartWithOrderBook } from './MinuteChartWithOrderBook';
|
||||
export type { MinuteChartWithOrderBookProps } from './MinuteChartWithOrderBook';
|
||||
@@ -0,0 +1,41 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/constants.ts
|
||||
// K线模块常量定义
|
||||
|
||||
import type { IndicatorType, MainIndicatorType, DrawingType } from '../../../utils/chartOptions';
|
||||
|
||||
/**
|
||||
* 图表模式类型
|
||||
*/
|
||||
export type ChartMode = 'daily' | 'minute';
|
||||
|
||||
/**
|
||||
* 副图指标选项
|
||||
*/
|
||||
export const SUB_INDICATOR_OPTIONS: { value: IndicatorType; label: string; description: string }[] = [
|
||||
{ value: 'MACD', label: 'MACD', description: '平滑异同移动平均线' },
|
||||
{ value: 'KDJ', label: 'KDJ', description: '随机指标' },
|
||||
{ value: 'RSI', label: 'RSI', description: '相对强弱指标' },
|
||||
{ value: 'WR', label: 'WR', description: '威廉指标(超买超卖)' },
|
||||
{ value: 'CCI', label: 'CCI', description: '商品通道指标' },
|
||||
{ value: 'BIAS', label: 'BIAS', description: '乖离率' },
|
||||
{ value: 'VOL', label: '仅成交量', description: '不显示副图指标' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 主图指标选项
|
||||
*/
|
||||
export const MAIN_INDICATOR_OPTIONS: { value: MainIndicatorType; label: string; description: string }[] = [
|
||||
{ value: 'MA', label: 'MA均线', description: 'MA5/MA10/MA20' },
|
||||
{ value: 'BOLL', label: '布林带', description: '布林通道指标' },
|
||||
{ value: 'NONE', label: '无', description: '不显示主图指标' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 绘图工具选项
|
||||
*/
|
||||
export const DRAWING_OPTIONS: { value: DrawingType; label: string; description: string }[] = [
|
||||
{ value: 'NONE', label: '无', description: '不显示绘图工具' },
|
||||
{ value: 'SUPPORT_RESISTANCE', label: '支撑/阻力', description: '自动识别支撑位和阻力位' },
|
||||
{ value: 'TREND_LINE', label: '趋势线', description: '基于线性回归的趋势线' },
|
||||
{ value: 'ALL', label: '全部显示', description: '显示所有参考线' },
|
||||
];
|
||||
@@ -1,54 +1,20 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/index.tsx
|
||||
// 交易数据面板 - K线模块(日K/分钟切换)
|
||||
// 交易数据面板 - 统一导出
|
||||
|
||||
import React from 'react';
|
||||
// 默认导出 KLineModule 作为 TradeDataPanel
|
||||
export { default } from './KLineModule';
|
||||
|
||||
import KLineModule from './KLineModule';
|
||||
import type { Theme, TradeDayData, MinuteData, RiseAnalysis } from '../../../types';
|
||||
|
||||
export interface TradeDataPanelProps {
|
||||
theme: Theme;
|
||||
tradeData: TradeDayData[];
|
||||
minuteData: MinuteData | null;
|
||||
minuteLoading: boolean;
|
||||
analysisMap: Record<number, RiseAnalysis>;
|
||||
onLoadMinuteData: () => void;
|
||||
onChartClick: (params: { seriesName?: string; data?: [number, number] }) => void;
|
||||
selectedPeriod?: number;
|
||||
onPeriodChange?: (period: number) => void;
|
||||
stockCode?: string;
|
||||
}
|
||||
|
||||
const TradeDataPanel: React.FC<TradeDataPanelProps> = ({
|
||||
theme,
|
||||
tradeData,
|
||||
minuteData,
|
||||
minuteLoading,
|
||||
analysisMap,
|
||||
onLoadMinuteData,
|
||||
onChartClick,
|
||||
selectedPeriod,
|
||||
onPeriodChange,
|
||||
stockCode,
|
||||
}) => {
|
||||
return (
|
||||
<KLineModule
|
||||
theme={theme}
|
||||
tradeData={tradeData}
|
||||
minuteData={minuteData}
|
||||
minuteLoading={minuteLoading}
|
||||
analysisMap={analysisMap}
|
||||
onLoadMinuteData={onLoadMinuteData}
|
||||
onChartClick={onChartClick}
|
||||
selectedPeriod={selectedPeriod}
|
||||
onPeriodChange={onPeriodChange}
|
||||
stockCode={stockCode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeDataPanel;
|
||||
|
||||
// 导出子组件供外部按需使用
|
||||
// 导出 KLineModule 及其类型
|
||||
export { default as KLineModule } from './KLineModule';
|
||||
export type { KLineModuleProps } from './KLineModule';
|
||||
|
||||
// 导出子组件供外部按需使用
|
||||
export { KLineToolbar, DailyKLineChart, MinuteChartWithOrderBook } from './components';
|
||||
export type { KLineToolbarProps, DailyKLineChartProps, MinuteChartWithOrderBookProps } from './components';
|
||||
|
||||
// 导出常量和样式
|
||||
export * from './constants';
|
||||
export * from './styles';
|
||||
|
||||
// 保持向后兼容的类型别名
|
||||
export type { KLineModuleProps as TradeDataPanelProps } from './KLineModule';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/styles.ts
|
||||
// K线模块样式常量
|
||||
|
||||
import { darkGoldTheme } from '../../../constants';
|
||||
|
||||
/**
|
||||
* 激活状态按钮样式
|
||||
*/
|
||||
export const ACTIVE_BUTTON_STYLE = {
|
||||
bg: `linear-gradient(135deg, ${darkGoldTheme.gold} 0%, ${darkGoldTheme.orange} 100%)`,
|
||||
color: '#1a1a2e',
|
||||
borderColor: darkGoldTheme.gold,
|
||||
_hover: {
|
||||
bg: `linear-gradient(135deg, ${darkGoldTheme.goldLight} 0%, ${darkGoldTheme.gold} 100%)`,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 非激活状态按钮样式
|
||||
*/
|
||||
export const INACTIVE_BUTTON_STYLE = {
|
||||
bg: 'transparent',
|
||||
color: darkGoldTheme.textMuted,
|
||||
borderColor: darkGoldTheme.border,
|
||||
_hover: {
|
||||
bg: 'rgba(212, 175, 55, 0.1)',
|
||||
borderColor: darkGoldTheme.gold,
|
||||
color: darkGoldTheme.gold,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 菜单项样式
|
||||
*/
|
||||
export const MENU_LIST_STYLE = {
|
||||
bg: '#1a1a2e',
|
||||
borderColor: darkGoldTheme.border,
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.5)',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 获取菜单项样式
|
||||
*/
|
||||
export const getMenuItemStyle = (isActive: boolean) => ({
|
||||
bg: isActive ? 'rgba(212, 175, 55, 0.2)' : 'transparent',
|
||||
color: isActive ? darkGoldTheme.gold : darkGoldTheme.textPrimary,
|
||||
_hover: { bg: 'rgba(212, 175, 55, 0.1)' },
|
||||
});
|
||||
|
||||
/**
|
||||
* Select 下拉框样式
|
||||
*/
|
||||
export const SELECT_STYLE = {
|
||||
bg: 'transparent',
|
||||
borderColor: darkGoldTheme.border,
|
||||
color: darkGoldTheme.textPrimary,
|
||||
_hover: { borderColor: darkGoldTheme.gold },
|
||||
_focus: { borderColor: darkGoldTheme.gold, boxShadow: 'none' },
|
||||
sx: {
|
||||
option: {
|
||||
background: '#1a1a2e',
|
||||
color: darkGoldTheme.textPrimary,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
130
src/views/Company/components/README.md
Normal file
130
src/views/Company/components/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Company 组件模块
|
||||
|
||||
公司详情页的核心组件集合,提供全面的上市公司分析功能。
|
||||
|
||||
## 组件概览
|
||||
|
||||
| 组件 | 说明 | 文档 |
|
||||
|------|------|------|
|
||||
| [CompanyHeader](./CompanyHeader/) | 公司头部信息(名称、代码、搜索) | [README](./CompanyHeader/README.md) |
|
||||
| [StockQuoteCard](./StockQuoteCard/) | 股票行情卡片(价格、涨跌、指标) | [README](./StockQuoteCard/README.md) |
|
||||
| [CompanyOverview](./CompanyOverview/) | 公司概览(基本信息、深度分析) | [README](./CompanyOverview/README.md) |
|
||||
| [DeepAnalysis](./DeepAnalysis/) | 深度分析(战略、业务、产业链) | [README](./DeepAnalysis/README.md) |
|
||||
| [MarketDataView](./MarketDataView/) | 市场数据(K线、融资、大宗交易) | [README](./MarketDataView/README.md) |
|
||||
| [FinancialPanorama](./FinancialPanorama/) | 财务全景(三大报表、指标分析) | [README](./FinancialPanorama/README.md) |
|
||||
| [ForecastReport](./ForecastReport/) | 盈利预测(EPS、增长率、估值) | [README](./ForecastReport/README.md) |
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
components/
|
||||
├── README.md # 本文档
|
||||
│
|
||||
├── CompanyHeader/ # 公司头部
|
||||
│ ├── index.tsx
|
||||
│ └── README.md
|
||||
│
|
||||
├── StockQuoteCard/ # 行情卡片
|
||||
│ ├── index.tsx
|
||||
│ ├── components/ # 子组件(PriceDisplay, StockHeader, MetricRow)
|
||||
│ └── README.md
|
||||
│
|
||||
├── CompanyOverview/ # 公司概览
|
||||
│ ├── index.tsx
|
||||
│ ├── BasicInfoTab/ # 基本信息 Tab
|
||||
│ ├── DeepAnalysisTab/ # 深度分析 Tab(展示组件)
|
||||
│ └── README.md
|
||||
│
|
||||
├── DeepAnalysis/ # 深度分析(独立模块)
|
||||
│ ├── index.tsx # 主组件(memo 优化)
|
||||
│ ├── types.ts # 类型定义
|
||||
│ ├── hooks/ # 数据获取 Hook
|
||||
│ └── README.md
|
||||
│
|
||||
├── MarketDataView/ # 市场数据
|
||||
│ ├── index.tsx
|
||||
│ ├── hooks/ # 数据获取
|
||||
│ ├── services/ # API 服务
|
||||
│ ├── components/ # 子组件(面板、卡片)
|
||||
│ └── README.md
|
||||
│
|
||||
├── FinancialPanorama/ # 财务全景
|
||||
│ ├── index.tsx
|
||||
│ ├── hooks/ # 财务数据获取
|
||||
│ ├── components/ # 表格、图表组件
|
||||
│ ├── tabs/ # Tab 页面
|
||||
│ └── README.md
|
||||
│
|
||||
├── ForecastReport/ # 盈利预测
|
||||
│ ├── index.tsx
|
||||
│ ├── components/ # 图表组件
|
||||
│ └── README.md
|
||||
│
|
||||
├── CompanyTabs/ # Tab 容器配置
|
||||
├── DynamicTracking/ # 动态追踪
|
||||
├── EChartsWrapper.tsx # ECharts 封装
|
||||
└── LoadingState.tsx # 加载状态组件
|
||||
```
|
||||
|
||||
## 组件关系
|
||||
|
||||
```
|
||||
Company Page (src/views/Company/index.tsx)
|
||||
│
|
||||
├── CompanyHeader # 顶部搜索栏
|
||||
│
|
||||
├── StockQuoteCard # 行情概览卡片
|
||||
│
|
||||
└── CompanyTabs # 主内容 Tab 切换
|
||||
├── CompanyOverview # 公司概览
|
||||
│ ├── BasicInfoTab # 基本信息
|
||||
│ └── DeepAnalysisTab # 深度分析(展示)
|
||||
│
|
||||
├── DeepAnalysis # 深度分析(独立模块)
|
||||
│
|
||||
├── MarketDataView # 市场数据
|
||||
│ ├── TradeDataPanel # 交易数据(K线)
|
||||
│ ├── FundingPanel # 融资融券
|
||||
│ ├── BigDealPanel # 大宗交易
|
||||
│ ├── UnusualPanel # 龙虎榜
|
||||
│ └── PledgePanel # 股权质押
|
||||
│
|
||||
├── FinancialPanorama # 财务全景
|
||||
│ ├── IncomeStatementTab
|
||||
│ ├── BalanceSheetTab
|
||||
│ ├── CashflowTab
|
||||
│ └── FinancialMetricsTab
|
||||
│
|
||||
└── ForecastReport # 盈利预测
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 技术 | 用途 |
|
||||
|------|------|
|
||||
| TypeScript | 类型安全(渐进迁移中) |
|
||||
| Chakra UI | 布局、主题 |
|
||||
| Ant Design | 表格、表单 |
|
||||
| ECharts | K线图、财务图表 |
|
||||
| React.memo | 性能优化 |
|
||||
|
||||
## 主题系统
|
||||
|
||||
- **StockQuoteCard**: DEEP_SPACE_THEME(深空主题)
|
||||
- **MarketDataView**: darkGoldTheme(黑金主题)
|
||||
- **其他组件**: 标准 Chakra UI 主题
|
||||
|
||||
## 使用示例
|
||||
|
||||
```tsx
|
||||
import CompanyHeader from '@views/Company/components/CompanyHeader';
|
||||
import StockQuoteCard from '@views/Company/components/StockQuoteCard';
|
||||
import MarketDataView from '@views/Company/components/MarketDataView';
|
||||
import DeepAnalysis from '@views/Company/components/DeepAnalysis';
|
||||
|
||||
// 在 Company 页面中组合使用
|
||||
<CompanyHeader stockCode={stockCode} onStockChange={handleChange} />
|
||||
<StockQuoteCard stockCode={stockCode} />
|
||||
<DeepAnalysis stockCode={stockCode} />
|
||||
<MarketDataView stockCode={stockCode} />
|
||||
```
|
||||
91
src/views/Company/components/StockQuoteCard/README.md
Normal file
91
src/views/Company/components/StockQuoteCard/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# StockQuoteCard 组件
|
||||
|
||||
股票行情卡片组件,采用深空 FUI 设计风格(Glassmorphism + Ash Thorp + James Turrell)。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
StockQuoteCard/
|
||||
├── index.tsx # 主组件入口 (180行)
|
||||
├── types.ts # 类型定义
|
||||
├── README.md # 本文档
|
||||
│
|
||||
├── components/ # 子组件
|
||||
│ ├── index.ts # 统一导出
|
||||
│ ├── theme.ts # 深空主题配置 (DEEP_SPACE_THEME)
|
||||
│ ├── formatters.ts # 格式化工具函数
|
||||
│ │
|
||||
│ ├── StockHeader.tsx # 头部:股票名称、代码、行业标签、操作按钮
|
||||
│ ├── PriceDisplay.tsx # 价格区域:当前价格、涨跌幅 Badge
|
||||
│ ├── SecondaryQuote.tsx # 次要行情:今开、昨收、最高、最低
|
||||
│ ├── MetricRow.tsx # 指标行:标签 + 值的单行展示
|
||||
│ ├── GlassSection.tsx # 玻璃容器:包装数据区块
|
||||
│ ├── MainForceInfo.tsx # 主力动态:资金流向、机构持仓
|
||||
│ ├── LoadingSkeleton.tsx # 加载骨架屏
|
||||
│ │
|
||||
│ ├── CompareStockInput.tsx # 股票对比输入框
|
||||
│ └── StockCompareModal.tsx # 股票对比弹窗
|
||||
│
|
||||
└── hooks/ # 自定义 Hooks
|
||||
├── index.ts # 统一导出
|
||||
├── useStockQuoteData.ts # 获取股票行情数据
|
||||
└── useStockCompare.ts # 股票对比逻辑
|
||||
```
|
||||
|
||||
## 组件层级
|
||||
|
||||
```
|
||||
StockQuoteCard
|
||||
├── LoadingSkeleton # 加载中状态
|
||||
│
|
||||
└── Box (glassCardStyle) # 玻璃态容器
|
||||
├── CardGlow # 装饰光效 (@components/FUI)
|
||||
│
|
||||
└── VStack # 内容区域
|
||||
├── StockHeader # 头部
|
||||
├── PriceDisplay # 价格
|
||||
├── SecondaryQuote # 次要行情
|
||||
│
|
||||
└── Flex # 三列数据区块
|
||||
├── GlassSection # 估值指标
|
||||
│ └── MetricRow × 3
|
||||
├── GlassSection # 市值股本
|
||||
│ └── MetricRow × 3
|
||||
└── GlassSection # 主力动态
|
||||
└── MainForceInfo
|
||||
```
|
||||
|
||||
## 主题系统
|
||||
|
||||
使用 `DEEP_SPACE_THEME` 深空主题:
|
||||
|
||||
| 类别 | 变量 | 说明 |
|
||||
|------|------|------|
|
||||
| 背景 | `bgGlass` | 玻璃态半透明背景 |
|
||||
| 边框 | `borderGold` | 金色边框 |
|
||||
| 文字 | `textPrimary` | 主文字(亮金色) |
|
||||
| 涨跌 | `upColor` / `downColor` | 红涨绿跌 |
|
||||
| 强调 | `gold` / `cyan` / `purple` | 强调色 |
|
||||
| 发光 | `upGlow` / `downGlow` | 涨跌发光效果 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```tsx
|
||||
import StockQuoteCard from '@views/Company/components/StockQuoteCard';
|
||||
|
||||
<StockQuoteCard
|
||||
stockCode="600000"
|
||||
isInWatchlist={true}
|
||||
isWatchlistLoading={false}
|
||||
onWatchlistToggle={() => handleToggle()}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| 属性 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `stockCode` | `string` | 是 | 股票代码 |
|
||||
| `isInWatchlist` | `boolean` | 否 | 是否在自选股中 |
|
||||
| `isWatchlistLoading` | `boolean` | 否 | 自选股操作加载中 |
|
||||
| `onWatchlistToggle` | `() => void` | 否 | 切换自选股回调 |
|
||||
Reference in New Issue
Block a user