Compare commits

...

4 Commits

Author SHA1 Message Date
zdl
4e71623477 docs(Company): 添加组件模块总览 README
新增 Company/components/README.md:
- 组件概览表格(7 个核心组件)
- 完整目录结构说明
- 组件层级关系图
- 技术栈和主题系统说明
- 使用示例

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 13:44:03 +08:00
zdl
ce4da40ef6 refactor(DeepAnalysis): TypeScript 重构,提取 useDeepAnalysisData Hook
- 新增 types.ts:API 类型定义、状态接口、Tab 映射常量
- 新增 hooks/useDeepAnalysisData.ts:提取数据获取逻辑
  - 懒加载:按 Tab 按需请求
  - 数据缓存:已加载数据不重复请求
  - 竞态处理:stockCode 变更时防止旧请求覆盖
- 重写 index.tsx:memo 优化,代码行数 229 → 81
- 新增 README.md:组件文档

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 13:42:16 +08:00
zdl
bff440ff8a refactor(TradeDataPanel): 拆分 KLineModule 为独立子组件
- KLineModule: 611行精简至157行,专注状态管理
- 提取 KLineToolbar: 工具栏组件(模式切换、指标选择)
- 提取 DailyKLineChart: 日K图表(useMemo缓存配置)
- 提取 MinuteChartWithOrderBook: 分时图+五档盘口
- 提取 constants.ts: 指标选项常量
- 提取 styles.ts: 按钮样式常量
- 所有组件使用 React.memo 优化
- 更新 README.md 文档

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 13:37:15 +08:00
zdl
9ef206a9e7 docs(Company): 添加组件目录结构文档
为 Company 模块下的主要组件添加 README.md:
- CompanyHeader: 搜索栏组件
- CompanyOverview: 公司概览(基本信息 + 深度分析)
- FinancialPanorama: 财务全景
- ForecastReport: 盈利预测
- MarketDataView: 市场数据
- StockQuoteCard: 股票行情卡片

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 13:22:43 +08:00
21 changed files with 1940 additions and 825 deletions

View 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` | 是 | 股票切换回调 |

View 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" />
```

View 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 变更时忽略旧请求)

View File

@@ -0,0 +1 @@
export { useDeepAnalysisData } from './useDeepAnalysisData';

View File

@@ -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();
// 只加载默认 Tabcomprehensive
loadApiData('comprehensive');
}
}, [stockCode, loadApiData, resetData]);
return {
data,
loading,
loadTabData,
resetData,
};
};
export default useDeepAnalysisData;

View File

@@ -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;

View 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;

View 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';

View 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" />
```

View 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" />
```

View 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 />
```

View File

@@ -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>
<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}
onAddOverlayMetric={handleAddOverlayMetric}
onRemoveOverlayMetric={handleRemoveOverlayMetric}
stockDateRange={stockDateRange}
minuteData={minuteData}
minuteLoading={minuteLoading}
showOrderBook={showOrderBook}
onToggleOrderBook={handleToggleOrderBook}
onRefreshMinuteData={onLoadMinuteData}
/>
<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
overlayMetrics={overlayMetrics}
onAddMetric={handleAddOverlayMetric}
onRemoveMetric={handleRemoveOverlayMetric}
stockDateRange={stockDateRange}
/>
</>
)}
{/* 分时模式下的控制按钮 */}
{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' }}
/>
</Box>
) : (
<EmptyState title="暂无日K线数据" description="该股票暂无交易数据" />
)
// 日K线图
<DailyKLineChart
tradeData={tradeData}
analysisMap={analysisMap}
subIndicator={subIndicator}
mainIndicator={mainIndicator}
showAnalysis={showAnalysis}
drawingType={drawingType}
overlayMetrics={overlayMetrics}
onChartClick={onChartClick}
/>
) : (
// 分时走势图 + 五档盘口
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"
/>
<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="点击刷新按钮获取当日分时数据" />
)
<MinuteChartWithOrderBook
minuteData={minuteData}
minuteLoading={minuteLoading}
stockCode={stockCode}
showOrderBook={showOrderBook}
/>
)}
</Box>
</Box>
);
};
export default KLineModule;
export default memo(KLineModule);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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';

View File

@@ -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: '显示所有参考线' },
];

View File

@@ -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';

View File

@@ -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;

View 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} />
```

View 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` | 否 | 切换自选股回调 |