refactor: MarketDataView TypeScript 重构 - 2060 行拆分为 12 个模块
- 将原 index.js (2060 行) 重构为 TypeScript 模块化架构 - 新增 types.ts: 383 行类型定义 (Theme, TradeDayData, MinuteData 等) - 新增 services/marketService.ts: API 服务层封装 - 新增 hooks/useMarketData.ts: 数据获取 Hook - 新增 utils/formatUtils.ts: 格式化工具函数 - 新增 utils/chartOptions.ts: ECharts 图表配置生成器 (698 行) - 新增 components/: ThemedCard, MarkdownRenderer, StockSummaryCard, AnalysisModal - 添加 Company/STRUCTURE.md 目录结构文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
460
src/views/Company/STRUCTURE.md
Normal file
460
src/views/Company/STRUCTURE.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# Company 目录结构说明
|
||||
|
||||
> 最后更新:2025-12-10
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/views/Company/
|
||||
├── index.js # 页面入口(95行,纯组合层)
|
||||
├── STRUCTURE.md # 本文档
|
||||
│
|
||||
├── components/ # UI 组件
|
||||
│ ├── CompanyHeader/ # 页面头部
|
||||
│ │ ├── index.js # 组合导出
|
||||
│ │ ├── SearchBar.js # 股票搜索栏
|
||||
│ │ └── WatchlistButton.js # 自选股按钮
|
||||
│ │
|
||||
│ ├── CompanyTabs/ # Tab 切换容器
|
||||
│ │ ├── index.js # Tab 容器(状态管理 + 内容渲染)
|
||||
│ │ └── TabNavigation.js # Tab 导航栏
|
||||
│ │
|
||||
│ ├── CompanyOverview/ # Tab: 公司概览(TypeScript 拆分)
|
||||
│ │ ├── index.tsx # 主组件(组合层,约 50 行)
|
||||
│ │ ├── CompanyHeaderCard.tsx # 头部卡片组件(约 130 行)
|
||||
│ │ ├── BasicInfoTab.js # 基本信息 Tab(暂保持 JS)
|
||||
│ │ ├── DeepAnalysisTab.js # 深度分析 Tab
|
||||
│ │ ├── NewsEventsTab.js # 新闻事件 Tab
|
||||
│ │ ├── types.ts # 类型定义(约 50 行)
|
||||
│ │ ├── utils.ts # 格式化工具(约 20 行)
|
||||
│ │ └── hooks/
|
||||
│ │ └── useCompanyOverviewData.ts # 数据 Hook(约 100 行)
|
||||
│ │
|
||||
│ ├── MarketDataView/ # Tab: 股票行情(TypeScript 拆分)
|
||||
│ │ ├── index.tsx # 主组件入口(~1049 行)
|
||||
│ │ ├── types.ts # 类型定义(~383 行)
|
||||
│ │ ├── constants.ts # 主题配置、常量
|
||||
│ │ ├── services/
|
||||
│ │ │ └── marketService.ts # API 服务层
|
||||
│ │ ├── hooks/
|
||||
│ │ │ └── useMarketData.ts # 数据获取 Hook
|
||||
│ │ ├── utils/
|
||||
│ │ │ ├── formatUtils.ts # 格式化工具函数
|
||||
│ │ │ └── chartOptions.ts # ECharts 图表配置生成器
|
||||
│ │ └── components/
|
||||
│ │ ├── index.ts # 组件导出
|
||||
│ │ ├── ThemedCard.tsx # 主题化卡片
|
||||
│ │ ├── MarkdownRenderer.tsx # Markdown 渲染
|
||||
│ │ ├── StockSummaryCard.tsx # 股票概览卡片
|
||||
│ │ └── AnalysisModal.tsx # 涨幅分析模态框
|
||||
│ │
|
||||
│ ├── FinancialPanorama/ # Tab: 财务全景(2153 行,待拆分)
|
||||
│ │ └── index.js
|
||||
│ │
|
||||
│ └── ForecastReport/ # Tab: 盈利预测(161 行,待拆分)
|
||||
│ └── index.js
|
||||
│
|
||||
├── hooks/ # 自定义 Hooks
|
||||
│ ├── useCompanyStock.js # 股票代码管理(URL 同步)
|
||||
│ ├── useCompanyWatchlist.js # 自选股管理(Redux 集成)
|
||||
│ └── useCompanyEvents.js # PostHog 事件追踪
|
||||
│
|
||||
└── constants/ # 常量定义
|
||||
└── index.js # Tab 配置、Toast 消息、默认值
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件职责说明
|
||||
|
||||
### 入口文件
|
||||
|
||||
#### `index.js` - 页面入口
|
||||
- **职责**:纯组合层,协调 Hooks 和 Components
|
||||
- **代码行数**:95 行
|
||||
- **依赖**:
|
||||
- `useCompanyStock` - 股票代码状态
|
||||
- `useCompanyWatchlist` - 自选股状态
|
||||
- `useCompanyEvents` - 事件追踪
|
||||
- `CompanyHeader` - 页面头部
|
||||
- `CompanyTabs` - Tab 切换区
|
||||
|
||||
---
|
||||
|
||||
### Hooks 目录
|
||||
|
||||
#### `useCompanyStock.js` - 股票代码管理
|
||||
- **功能**:
|
||||
- 管理当前股票代码状态
|
||||
- 双向同步 URL 参数(支持浏览器前进/后退)
|
||||
- 处理搜索输入和提交
|
||||
- **返回值**:
|
||||
```js
|
||||
{
|
||||
stockCode, // 当前确认的股票代码
|
||||
inputCode, // 输入框中的值(未确认)
|
||||
setInputCode, // 更新输入框
|
||||
handleSearch, // 执行搜索
|
||||
handleKeyPress, // 处理回车键
|
||||
}
|
||||
```
|
||||
- **依赖**:`react-router-dom` (useSearchParams)
|
||||
|
||||
#### `useCompanyWatchlist.js` - 自选股管理
|
||||
- **功能**:
|
||||
- 检查当前股票是否在自选股中
|
||||
- 提供添加/移除自选股功能
|
||||
- 与 Redux stockSlice 同步
|
||||
- **返回值**:
|
||||
```js
|
||||
{
|
||||
isInWatchlist, // 是否在自选股中
|
||||
isLoading, // 操作进行中
|
||||
toggle, // 切换自选状态
|
||||
}
|
||||
```
|
||||
- **依赖**:Redux (`stockSlice`)、`AuthContext`、Chakra UI (useToast)
|
||||
|
||||
#### `useCompanyEvents.js` - 事件追踪
|
||||
- **功能**:
|
||||
- 页面浏览追踪
|
||||
- 股票搜索追踪
|
||||
- Tab 切换追踪
|
||||
- 自选股操作追踪
|
||||
- **返回值**:
|
||||
```js
|
||||
{
|
||||
trackStockSearched, // 追踪股票搜索
|
||||
trackTabChanged, // 追踪 Tab 切换
|
||||
trackWatchlistAdded, // 追踪添加自选
|
||||
trackWatchlistRemoved, // 追踪移除自选
|
||||
}
|
||||
```
|
||||
- **依赖**:PostHog (`usePostHogTrack`)
|
||||
|
||||
---
|
||||
|
||||
### Components 目录
|
||||
|
||||
#### `CompanyHeader/` - 页面头部
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `index.js` | 组合 SearchBar 和 WatchlistButton |
|
||||
| `SearchBar.js` | 股票代码搜索输入框 |
|
||||
| `WatchlistButton.js` | 自选股添加/移除按钮 |
|
||||
|
||||
**Props 接口**:
|
||||
```js
|
||||
<CompanyHeader
|
||||
stockCode={string} // 当前股票代码
|
||||
inputCode={string} // 输入框值
|
||||
onInputChange={func} // 输入变化回调
|
||||
onSearch={func} // 搜索回调
|
||||
onKeyPress={func} // 键盘事件回调
|
||||
isInWatchlist={bool} // 是否在自选中
|
||||
isWatchlistLoading={bool} // 自选操作加载中
|
||||
onWatchlistToggle={func} // 自选切换回调
|
||||
bgColor={string} // 背景色
|
||||
/>
|
||||
```
|
||||
|
||||
#### `CompanyTabs/` - Tab 切换
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `index.js` | Tab 容器,管理切换状态,渲染 Tab 内容 |
|
||||
| `TabNavigation.js` | Tab 导航栏(4个 Tab 按钮) |
|
||||
|
||||
**Props 接口**:
|
||||
```js
|
||||
<CompanyTabs
|
||||
stockCode={string} // 当前股票代码
|
||||
onTabChange={func} // Tab 变更回调 (index, tabName, prevIndex)
|
||||
bgColor={string} // 背景色
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Constants 目录
|
||||
|
||||
#### `constants/index.js` - 常量配置
|
||||
- `COMPANY_TABS` - Tab 配置数组(key, name, icon)
|
||||
- `TAB_SELECTED_STYLE` - Tab 选中样式
|
||||
- `TOAST_MESSAGES` - Toast 消息配置
|
||||
- `DEFAULT_STOCK_CODE` - 默认股票代码 ('000001')
|
||||
- `URL_PARAM_NAME` - URL 参数名 ('scode')
|
||||
- `getTabNameByIndex()` - 根据索引获取 Tab 名称
|
||||
|
||||
---
|
||||
|
||||
### Tab 内容组件(`components/` 目录下)
|
||||
|
||||
| 组件 | Tab 名称 | 职责 | 代码行数 |
|
||||
|------|----------|------|----------|
|
||||
| `CompanyOverview/` | 公司概览 | 公司基本信息、相关事件 | - |
|
||||
| `MarketDataView/` | 股票行情 | K线图、实时行情 | - |
|
||||
| `FinancialPanorama/` | 财务全景 | 财务报表、指标分析 | 2153 行 |
|
||||
| `ForecastReport/` | 盈利预测 | 分析师预测、目标价 | 161 行 |
|
||||
|
||||
> 📌 所有 Tab 内容组件已文件夹化并统一放置在 `components/` 目录下
|
||||
|
||||
---
|
||||
|
||||
## 数据流示意
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ index.js (页面入口) │
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ useCompanyStock │ │useCompanyWatchlist│ │useCompanyEvents│
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • stockCode │ │ • isInWatchlist │ │ • track* │ │
|
||||
│ │ • inputCode │ │ • toggle │ │ functions │ │
|
||||
│ │ • handleSearch │ │ │ │ │ │
|
||||
│ └────────┬────────┘ └────────┬────────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────────┬─────────┴───────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ CompanyHeader │ │
|
||||
│ │ ┌─────────────┐ ┌──────────────────┐ │ │
|
||||
│ │ │ SearchBar │ │ WatchlistButton │ │ │
|
||||
│ │ └─────────────┘ └──────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ CompanyTabs │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ TabNavigation │ │ │
|
||||
│ │ │ [概览] [行情] [财务] [预测] │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ TabPanels │ │ │
|
||||
│ │ │ • CompanyOverview │ │ │
|
||||
│ │ │ • MarketDataView │ │ │
|
||||
│ │ │ • FinancialPanorama │ │ │
|
||||
│ │ │ • ForecastReport │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 重构记录
|
||||
|
||||
### 2025-12-09 重构
|
||||
|
||||
**改动概述**:
|
||||
- `index.js` 从 **349 行** 精简至 **95 行**(减少 73%)
|
||||
- 提取 **3 个自定义 Hooks**
|
||||
- 提取 **2 个组件目录**(CompanyHeader、CompanyTabs)
|
||||
- 抽离常量到 `constants/index.js`
|
||||
|
||||
**修复的问题**:
|
||||
1. **无限循环 Bug**:`useCompanyWatchlist` 中使用 `useRef` 防止重复初始化
|
||||
2. **Hook 调用顺序**:确保 `useCompanyEvents` 在 `useCompanyStock` 之后调用(依赖 stockCode)
|
||||
3. **类型检查**:`CompanyOverview.js` 中 `event.keywords` 渲染时添加类型检查,支持字符串和对象两种格式
|
||||
|
||||
**设计原则**:
|
||||
- **关注点分离**:每个 Hook 只负责单一职责
|
||||
- **纯组合层**:index.js 不包含业务逻辑,只负责组合
|
||||
- **Props 透传**:通过 Props 将状态和回调传递给子组件
|
||||
|
||||
### 2025-12-09 文件夹化
|
||||
|
||||
**改动概述**:
|
||||
- 所有 4 个 Tab 内容组件统一移动到 `components/` 目录
|
||||
- `CompanyOverview.js` → `components/CompanyOverview/index.js`
|
||||
- `MarketDataView.js` → `components/MarketDataView/index.js`
|
||||
- `FinancialPanorama.js` → `components/FinancialPanorama/index.js`(2153 行)
|
||||
- `ForecastReport.js` → `components/ForecastReport/index.js`(161 行)
|
||||
- 更新 `CompanyTabs/index.js` 中的导入路径
|
||||
|
||||
**目的**:
|
||||
- 统一目录结构,所有组件都在 `components/` 下
|
||||
- 为后期组件拆分做准备,便于添加子组件、hooks、utils 等
|
||||
|
||||
### 2025-12-10 CompanyOverview 拆分(TypeScript)
|
||||
|
||||
**改动概述**:
|
||||
- `CompanyOverview/index.js` 从 **330 行** 精简至 **50 行**(减少 85%)
|
||||
- 采用 **TypeScript** 进行拆分,提高类型安全性
|
||||
- 提取 **1 个自定义 Hook**(`useCompanyOverviewData`)
|
||||
- 提取 **1 个子组件**(`CompanyHeaderCard`)
|
||||
- 抽离类型定义到 `types.ts`
|
||||
- 抽离工具函数到 `utils.ts`
|
||||
|
||||
**拆分后文件结构**:
|
||||
```
|
||||
CompanyOverview/
|
||||
├── index.tsx # 主组件(组合层,约 60 行)
|
||||
├── CompanyHeaderCard.tsx # 头部卡片组件(约 130 行)
|
||||
├── BasicInfoTab.js # 基本信息 Tab(懒加载版本,约 994 行)
|
||||
├── DeepAnalysisTab.js # 深度分析 Tab
|
||||
├── NewsEventsTab.js # 新闻事件 Tab
|
||||
├── types.ts # 类型定义(约 50 行)
|
||||
├── utils.ts # 格式化工具(约 20 行)
|
||||
└── hooks/
|
||||
├── useBasicInfo.ts # 基本信息 Hook(1 API)
|
||||
├── useShareholderData.ts # 股权结构 Hook(4 APIs)
|
||||
├── useManagementData.ts # 管理团队 Hook(1 API)
|
||||
├── useAnnouncementsData.ts # 公告数据 Hook(1 API)
|
||||
├── useBranchesData.ts # 分支机构 Hook(1 API)
|
||||
├── useDisclosureData.ts # 披露日程 Hook(1 API)
|
||||
└── useCompanyOverviewData.ts # [已废弃] 原合并 Hook
|
||||
```
|
||||
|
||||
**懒加载架构**(2025-12-10 优化):
|
||||
- `index.tsx` 只加载 `useBasicInfo`(1 个 API)用于头部卡片
|
||||
- `BasicInfoTab.js` 使用 `isLazy` + 独立子组件实现懒加载
|
||||
- 每个内层 Tab 使用独立 Hook,点击时才加载数据
|
||||
|
||||
**Hooks 说明**:
|
||||
| Hook | API 数量 | 用途 |
|
||||
|------|----------|------|
|
||||
| `useBasicInfo` | 1 | 公司基本信息(头部卡片 + 工商信息 Tab) |
|
||||
| `useShareholderData` | 4 | 实控人、股权集中度、十大股东、十大流通股东 |
|
||||
| `useManagementData` | 1 | 管理团队数据 |
|
||||
| `useAnnouncementsData` | 1 | 公司公告列表 |
|
||||
| `useBranchesData` | 1 | 分支机构列表 |
|
||||
| `useDisclosureData` | 1 | 财报披露日程 |
|
||||
|
||||
**类型定义**(`types.ts`):
|
||||
- `BasicInfo` - 公司基本信息
|
||||
- `ActualControl` - 实际控制人
|
||||
- `Concentration` - 股权集中度
|
||||
- `Management` - 管理层信息
|
||||
- `Shareholder` - 股东信息
|
||||
- `Branch` - 分支机构
|
||||
- `Announcement` - 公告信息
|
||||
- `DisclosureSchedule` - 披露计划
|
||||
- `CompanyOverviewData` - Hook 返回值类型
|
||||
- `CompanyOverviewProps` - 组件 Props 类型
|
||||
- `CompanyHeaderCardProps` - 头部卡片 Props 类型
|
||||
|
||||
**工具函数**(`utils.ts`):
|
||||
- `formatRegisteredCapital(value)` - 格式化注册资本(万元/亿元)
|
||||
- `formatDate(dateString)` - 格式化日期
|
||||
|
||||
**设计原则**:
|
||||
- **渐进式 TypeScript 迁移**:新拆分的文件使用 TypeScript,旧文件暂保持 JS
|
||||
- **关注点分离**:数据加载逻辑提取到 Hook,UI 逻辑保留在组件
|
||||
- **类型复用**:统一的类型定义便于在多个文件间共享
|
||||
- **懒加载优化**:减少首屏 API 请求,按需加载数据
|
||||
|
||||
### 2025-12-10 懒加载优化
|
||||
|
||||
**改动概述**:
|
||||
- 将 `useCompanyOverviewData`(9 个 API)拆分为 6 个独立 Hook
|
||||
- `CompanyOverview/index.tsx` 只加载 `useBasicInfo`(1 个 API)
|
||||
- `BasicInfoTab.js` 使用 5 个懒加载子组件,配合 `isLazy` 实现按需加载
|
||||
- 页面初次加载从 **9 个 API** 减少到 **1 个 API**
|
||||
|
||||
**懒加载子组件**(BasicInfoTab.js 内部):
|
||||
| 子组件 | Hook | 功能 |
|
||||
|--------|------|------|
|
||||
| `ShareholderTabPanel` | `useShareholderData` | 股权结构(4 APIs) |
|
||||
| `ManagementTabPanel` | `useManagementData` | 管理团队 |
|
||||
| `AnnouncementsTabPanel` | `useAnnouncementsData` + `useDisclosureData` | 公告 + 披露日程 |
|
||||
| `BranchesTabPanel` | `useBranchesData` | 分支机构 |
|
||||
| `BusinessInfoTabPanel` | - | 工商信息(使用父组件传入的 basicInfo) |
|
||||
|
||||
**实现原理**:
|
||||
- Chakra UI `Tabs` 的 `isLazy` 属性延迟渲染 TabPanel
|
||||
- 每个 TabPanel 使用独立子组件,组件内调用 Hook
|
||||
- 子组件只在首次激活时渲染,此时 Hook 才执行并发起 API 请求
|
||||
|
||||
| Tab 模块 | 中文名称 | 功能说明 |
|
||||
|-------------------|------|----------------------------|
|
||||
| CompanyOverview | 公司概览 | 公司基本信息、股权结构、管理层、公告等(9个接口) |
|
||||
| DeepAnalysis | 深度分析 | 公司深度研究报告、投资逻辑分析 |
|
||||
| MarketDataView | 股票行情 | K线图、实时行情、技术指标 |
|
||||
| FinancialPanorama | 财务全景 | 财务报表(资产负债表、利润表、现金流)、财务指标分析 |
|
||||
| ForecastReport | 盈利预测 | 分析师预测、目标价、评级 |
|
||||
| DynamicTracking | 动态跟踪 | 相关事件、新闻动态、投资日历 |
|
||||
|
||||
### 2025-12-10 MarketDataView TypeScript 拆分
|
||||
|
||||
**改动概述**:
|
||||
- `MarketDataView/index.js` 从 **2060 行** 拆分为 **12 个 TypeScript 文件**
|
||||
- 采用 **TypeScript** 进行重构,提高类型安全性
|
||||
- 提取 **1 个自定义 Hook**(`useMarketData`)
|
||||
- 提取 **4 个子组件**(ThemedCard、MarkdownRenderer、StockSummaryCard、AnalysisModal)
|
||||
- 抽离 API 服务到 `services/marketService.ts`
|
||||
- 抽离图表配置到 `utils/chartOptions.ts`
|
||||
|
||||
**拆分后文件结构**:
|
||||
```
|
||||
MarketDataView/
|
||||
├── index.tsx # 主组件入口(~1049 行)
|
||||
├── types.ts # 类型定义(~383 行)
|
||||
├── constants.ts # 主题配置、常量(~49 行)
|
||||
├── services/
|
||||
│ └── marketService.ts # API 服务层(~173 行)
|
||||
├── hooks/
|
||||
│ └── useMarketData.ts # 数据获取 Hook(~193 行)
|
||||
├── utils/
|
||||
│ ├── formatUtils.ts # 格式化工具函数(~175 行)
|
||||
│ └── chartOptions.ts # ECharts 图表配置生成器(~698 行)
|
||||
└── components/
|
||||
├── index.ts # 组件导出(~8 行)
|
||||
├── ThemedCard.tsx # 主题化卡片(~32 行)
|
||||
├── MarkdownRenderer.tsx # Markdown 渲染(~65 行)
|
||||
├── StockSummaryCard.tsx # 股票概览卡片(~133 行)
|
||||
└── AnalysisModal.tsx # 涨幅分析模态框(~188 行)
|
||||
```
|
||||
|
||||
**文件职责说明**:
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `index.tsx` | ~1049 | 主组件,包含 5 个 Tab 面板(交易数据、融资融券、大宗交易、龙虎榜、股权质押) |
|
||||
| `types.ts` | ~383 | 所有 TypeScript 类型定义(Theme、TradeDayData、MinuteData、FundingData 等) |
|
||||
| `constants.ts` | ~49 | 主题配置(light/dark)、周期选项常量 |
|
||||
| `marketService.ts` | ~173 | API 服务封装(getMarketData、getMinuteData、getBigDealData 等) |
|
||||
| `useMarketData.ts` | ~193 | 数据获取 Hook,管理所有市场数据状态 |
|
||||
| `formatUtils.ts` | ~175 | 数字/日期/涨跌幅格式化工具 |
|
||||
| `chartOptions.ts` | ~698 | ECharts 配置生成器(K线图、分钟图、融资融券图、质押图) |
|
||||
| `ThemedCard.tsx` | ~32 | 主题化卡片容器组件 |
|
||||
| `MarkdownRenderer.tsx` | ~65 | Markdown 内容渲染组件 |
|
||||
| `StockSummaryCard.tsx` | ~133 | 股票概览卡片(价格、涨跌幅、成交量等) |
|
||||
| `AnalysisModal.tsx` | ~188 | 涨幅分析详情模态框 |
|
||||
|
||||
**类型定义**(`types.ts`):
|
||||
- `Theme` - 主题配置类型
|
||||
- `TradeDayData` - 日线交易数据
|
||||
- `MinuteData` - 分钟线数据
|
||||
- `FundingDayData` - 融资融券数据
|
||||
- `BigDealData` / `BigDealDayStats` - 大宗交易数据
|
||||
- `UnusualData` / `UnusualDayData` - 龙虎榜数据
|
||||
- `PledgeData` - 股权质押数据
|
||||
- `RiseAnalysis` - 涨幅分析数据
|
||||
- `MarketSummary` - 市场概览数据
|
||||
- `VerificationReport` - 验证报告数据
|
||||
- 各组件 Props 类型
|
||||
|
||||
**Hook 返回值**(`useMarketData`):
|
||||
```typescript
|
||||
{
|
||||
loading: boolean;
|
||||
summary: MarketSummary | null;
|
||||
tradeData: TradeDayData[];
|
||||
minuteData: MinuteData | null;
|
||||
minuteLoading: boolean;
|
||||
fundingData: FundingDayData[];
|
||||
bigDealData: BigDealData | null;
|
||||
unusualData: UnusualData | null;
|
||||
pledgeData: PledgeData | null;
|
||||
analysisMap: Record<number, RiseAnalysis>;
|
||||
refetch: () => Promise<void>;
|
||||
loadMinuteData: () => Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
**设计原则**:
|
||||
- **TypeScript 类型安全**:所有数据结构有完整类型定义
|
||||
- **服务层分离**:API 调用统一在 `marketService.ts` 中管理
|
||||
- **图表配置抽离**:复杂的 ECharts 配置集中在 `chartOptions.ts`
|
||||
- **组件复用**:通用组件(ThemedCard、MarkdownRenderer)可在其他模块使用
|
||||
@@ -0,0 +1,188 @@
|
||||
// src/views/Company/components/MarketDataView/components/AnalysisModal.tsx
|
||||
// 涨幅分析模态框组件
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
VStack,
|
||||
HStack,
|
||||
Box,
|
||||
Heading,
|
||||
Text,
|
||||
Tag,
|
||||
Badge,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import MarkdownRenderer from './MarkdownRenderer';
|
||||
import { formatNumber } from '../utils/formatUtils';
|
||||
import type { AnalysisModalProps, RiseAnalysis, Theme } from '../types';
|
||||
|
||||
/**
|
||||
* 涨幅分析内容组件
|
||||
*/
|
||||
interface AnalysisContentProps {
|
||||
analysis: RiseAnalysis;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
export const AnalysisContent: React.FC<AnalysisContentProps> = ({ analysis, theme }) => {
|
||||
return (
|
||||
<VStack align="stretch" spacing={4}>
|
||||
{/* 头部信息 */}
|
||||
<Box>
|
||||
<Heading size="md" mb={2}>
|
||||
{analysis.stock_name} ({analysis.stock_code})
|
||||
</Heading>
|
||||
<HStack spacing={4} mb={4}>
|
||||
<Tag colorScheme="blue">日期: {analysis.trade_date}</Tag>
|
||||
<Tag colorScheme="red">涨幅: {analysis.rise_rate}%</Tag>
|
||||
<Tag colorScheme="green">收盘价: {analysis.close_price}</Tag>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* 主营业务 */}
|
||||
{analysis.main_business && (
|
||||
<Box p={4} bg="gray.50" borderRadius="md">
|
||||
<Heading size="sm" mb={2} color={theme.primary}>
|
||||
主营业务
|
||||
</Heading>
|
||||
<Text color={theme.textPrimary}>{analysis.main_business}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 详细分析 */}
|
||||
{analysis.rise_reason_detail && (
|
||||
<Box p={4} bg="purple.50" borderRadius="md">
|
||||
<Heading size="sm" mb={2} color={theme.primary}>
|
||||
详细分析
|
||||
</Heading>
|
||||
<MarkdownRenderer theme={theme}>{analysis.rise_reason_detail}</MarkdownRenderer>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 相关公告 */}
|
||||
{analysis.announcements && analysis.announcements !== '[]' && (
|
||||
<Box p={4} bg="orange.50" borderRadius="md">
|
||||
<Heading size="sm" mb={2} color={theme.primary}>
|
||||
相关公告
|
||||
</Heading>
|
||||
<MarkdownRenderer theme={theme}>{analysis.announcements}</MarkdownRenderer>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 研报引用 */}
|
||||
{analysis.verification_reports && analysis.verification_reports.length > 0 && (
|
||||
<Box p={4} bg="blue.50" borderRadius="md">
|
||||
<Heading size="sm" mb={3} color={theme.primary}>
|
||||
<HStack spacing={2}>
|
||||
<Icon as={ExternalLinkIcon} />
|
||||
<Text>研报引用 ({analysis.verification_reports.length})</Text>
|
||||
</HStack>
|
||||
</Heading>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{analysis.verification_reports.map((report, reportIdx) => (
|
||||
<Box
|
||||
key={reportIdx}
|
||||
p={3}
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor={theme.border}
|
||||
>
|
||||
<HStack justify="space-between" mb={2}>
|
||||
<HStack spacing={2}>
|
||||
<Badge colorScheme="blue" fontSize="xs">
|
||||
{report.publisher || '未知机构'}
|
||||
</Badge>
|
||||
{report.match_score && (
|
||||
<Badge
|
||||
colorScheme={
|
||||
report.match_score === '好'
|
||||
? 'green'
|
||||
: report.match_score === '中'
|
||||
? 'yellow'
|
||||
: 'gray'
|
||||
}
|
||||
fontSize="xs"
|
||||
>
|
||||
匹配度: {report.match_score}
|
||||
</Badge>
|
||||
)}
|
||||
{report.match_ratio != null && report.match_ratio > 0 && (
|
||||
<Badge colorScheme="purple" fontSize="xs">
|
||||
{(report.match_ratio * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
{report.declare_date && (
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
{report.declare_date.substring(0, 10)}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{report.report_title && (
|
||||
<Text fontWeight="bold" fontSize="sm" color={theme.textPrimary} mb={1}>
|
||||
《{report.report_title}》
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{report.author && (
|
||||
<Text fontSize="xs" color={theme.textMuted} mb={2}>
|
||||
分析师: {report.author}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{report.verification_item && (
|
||||
<Box p={2} bg="yellow.50" borderRadius="sm" mb={2}>
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
<strong>验证项:</strong> {report.verification_item}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{report.content && (
|
||||
<Text fontSize="sm" color={theme.textSecondary} noOfLines={4}>
|
||||
{report.content}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 底部统计 */}
|
||||
<Box mt={4}>
|
||||
<Text fontSize="sm" color={theme.textMuted}>
|
||||
成交量: {formatNumber(analysis.volume)} | 成交额: {formatNumber(analysis.amount)} | 更新时间:{' '}
|
||||
{analysis.update_time || analysis.create_time || '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 涨幅分析模态框组件
|
||||
*/
|
||||
const AnalysisModal: React.FC<AnalysisModalProps> = ({ isOpen, onClose, content, theme }) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent bg={theme.bgCard}>
|
||||
<ModalHeader color={theme.textPrimary}>涨幅分析详情</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6}>{content}</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisModal;
|
||||
@@ -0,0 +1,65 @@
|
||||
// src/views/Company/components/MarketDataView/components/MarkdownRenderer.tsx
|
||||
// Markdown 渲染组件
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import type { MarkdownRendererProps } from '../types';
|
||||
|
||||
/**
|
||||
* Markdown 渲染组件
|
||||
* 提供统一的 Markdown 样式
|
||||
*/
|
||||
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ children, theme }) => {
|
||||
return (
|
||||
<Box
|
||||
color={theme.textPrimary}
|
||||
sx={{
|
||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
||||
color: theme.primary,
|
||||
marginTop: 4,
|
||||
marginBottom: 2,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
'& h1': { fontSize: '1.5em' },
|
||||
'& h2': { fontSize: '1.3em' },
|
||||
'& h3': { fontSize: '1.1em' },
|
||||
'& p': {
|
||||
marginBottom: 3,
|
||||
lineHeight: 1.6,
|
||||
},
|
||||
'& ul, & ol': {
|
||||
paddingLeft: 4,
|
||||
marginBottom: 3,
|
||||
},
|
||||
'& li': {
|
||||
marginBottom: 1,
|
||||
},
|
||||
'& strong': {
|
||||
fontWeight: 'bold',
|
||||
color: theme.textSecondary,
|
||||
},
|
||||
'& em': {
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
'& code': {
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.9em',
|
||||
},
|
||||
'& blockquote': {
|
||||
borderLeft: `3px solid ${theme.primary}`,
|
||||
paddingLeft: 4,
|
||||
marginLeft: 2,
|
||||
fontStyle: 'italic',
|
||||
opacity: 0.9,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown>{children}</ReactMarkdown>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownRenderer;
|
||||
@@ -0,0 +1,133 @@
|
||||
// src/views/Company/components/MarketDataView/components/StockSummaryCard.tsx
|
||||
// 股票概览卡片组件
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
CardBody,
|
||||
Grid,
|
||||
GridItem,
|
||||
VStack,
|
||||
HStack,
|
||||
Heading,
|
||||
Badge,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
StatArrow,
|
||||
SimpleGrid,
|
||||
} from '@chakra-ui/react';
|
||||
import ThemedCard from './ThemedCard';
|
||||
import { formatNumber, formatPercent } from '../utils/formatUtils';
|
||||
import type { StockSummaryCardProps } from '../types';
|
||||
|
||||
/**
|
||||
* 股票概览卡片组件
|
||||
* 显示股票基本信息、最新交易数据和融资融券数据
|
||||
*/
|
||||
const StockSummaryCard: React.FC<StockSummaryCardProps> = ({ summary, theme }) => {
|
||||
if (!summary) return null;
|
||||
|
||||
const { latest_trade, latest_funding, latest_pledge } = summary;
|
||||
|
||||
return (
|
||||
<ThemedCard theme={theme}>
|
||||
<CardBody>
|
||||
<Grid templateColumns="repeat(12, 1fr)" gap={6}>
|
||||
{/* 左侧:股票名称和涨跌 */}
|
||||
<GridItem colSpan={{ base: 12, md: 4 }}>
|
||||
<VStack align="start" spacing={2}>
|
||||
<HStack>
|
||||
<Heading size="xl" color={theme.textSecondary}>
|
||||
{summary.stock_name}
|
||||
</Heading>
|
||||
<Badge colorScheme="blue" fontSize="lg">
|
||||
{summary.stock_code}
|
||||
</Badge>
|
||||
</HStack>
|
||||
{latest_trade && (
|
||||
<HStack spacing={4}>
|
||||
<Stat>
|
||||
<StatNumber fontSize="4xl" color={theme.textPrimary}>
|
||||
{latest_trade.close}
|
||||
</StatNumber>
|
||||
<StatHelpText fontSize="lg">
|
||||
<StatArrow
|
||||
type={latest_trade.change_percent >= 0 ? 'increase' : 'decrease'}
|
||||
color={latest_trade.change_percent >= 0 ? theme.success : theme.danger}
|
||||
/>
|
||||
{Math.abs(latest_trade.change_percent).toFixed(2)}%
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</GridItem>
|
||||
|
||||
{/* 右侧:详细指标 */}
|
||||
<GridItem colSpan={{ base: 12, md: 8 }}>
|
||||
{/* 交易指标 */}
|
||||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
|
||||
{latest_trade && (
|
||||
<>
|
||||
<Stat>
|
||||
<StatLabel color={theme.textMuted}>成交量</StatLabel>
|
||||
<StatNumber color={theme.textSecondary}>
|
||||
{formatNumber(latest_trade.volume, 0)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={theme.textMuted}>成交额</StatLabel>
|
||||
<StatNumber color={theme.textSecondary}>
|
||||
{formatNumber(latest_trade.amount)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={theme.textMuted}>换手率</StatLabel>
|
||||
<StatNumber color={theme.textSecondary}>
|
||||
{formatPercent(latest_trade.turnover_rate)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={theme.textMuted}>市盈率</StatLabel>
|
||||
<StatNumber color={theme.textSecondary}>
|
||||
{latest_trade.pe_ratio || '-'}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
</>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 融资融券和质押指标 */}
|
||||
{latest_funding && (
|
||||
<SimpleGrid columns={{ base: 2, md: 3 }} spacing={4} mt={4}>
|
||||
<Stat>
|
||||
<StatLabel color={theme.textMuted}>融资余额</StatLabel>
|
||||
<StatNumber color={theme.success} fontSize="lg">
|
||||
{formatNumber(latest_funding.financing_balance)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color={theme.textMuted}>融券余额</StatLabel>
|
||||
<StatNumber color={theme.danger} fontSize="lg">
|
||||
{formatNumber(latest_funding.securities_balance)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
{latest_pledge && (
|
||||
<Stat>
|
||||
<StatLabel color={theme.textMuted}>质押比例</StatLabel>
|
||||
<StatNumber color={theme.warning} fontSize="lg">
|
||||
{formatPercent(latest_pledge.pledge_ratio)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</CardBody>
|
||||
</ThemedCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default StockSummaryCard;
|
||||
@@ -0,0 +1,32 @@
|
||||
// src/views/Company/components/MarketDataView/components/ThemedCard.tsx
|
||||
// 主题化卡片组件
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@chakra-ui/react';
|
||||
import type { ThemedCardProps } from '../types';
|
||||
|
||||
/**
|
||||
* 主题化卡片组件
|
||||
* 提供统一的卡片样式和悬停效果
|
||||
*/
|
||||
const ThemedCard: React.FC<ThemedCardProps> = ({ children, theme, ...props }) => {
|
||||
return (
|
||||
<Card
|
||||
bg={theme.bgCard}
|
||||
border="1px solid"
|
||||
borderColor={theme.border}
|
||||
borderRadius="xl"
|
||||
boxShadow="lg"
|
||||
transition="all 0.3s ease"
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: 'xl',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemedCard;
|
||||
@@ -0,0 +1,7 @@
|
||||
// src/views/Company/components/MarketDataView/components/index.ts
|
||||
// 组件导出索引
|
||||
|
||||
export { default as ThemedCard } from './ThemedCard';
|
||||
export { default as MarkdownRenderer } from './MarkdownRenderer';
|
||||
export { default as StockSummaryCard } from './StockSummaryCard';
|
||||
export { default as AnalysisModal, AnalysisContent } from './AnalysisModal';
|
||||
49
src/views/Company/components/MarketDataView/constants.ts
Normal file
49
src/views/Company/components/MarketDataView/constants.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// src/views/Company/components/MarketDataView/constants.ts
|
||||
// MarketDataView 常量配置
|
||||
|
||||
import type { Theme } from './types';
|
||||
|
||||
/**
|
||||
* 主题配置
|
||||
*/
|
||||
export const themes: Record<'light', Theme> = {
|
||||
light: {
|
||||
// 日间模式 - 白+蓝
|
||||
primary: '#2B6CB0',
|
||||
primaryDark: '#1E4E8C',
|
||||
secondary: '#FFFFFF',
|
||||
secondaryDark: '#F7FAFC',
|
||||
success: '#FF4444', // 涨 - 红色
|
||||
danger: '#00C851', // 跌 - 绿色
|
||||
warning: '#FF9800',
|
||||
info: '#00BCD4',
|
||||
bgMain: '#F7FAFC',
|
||||
bgCard: '#FFFFFF',
|
||||
bgDark: '#EDF2F7',
|
||||
textPrimary: '#2D3748',
|
||||
textSecondary: '#4A5568',
|
||||
textMuted: '#718096',
|
||||
border: '#CBD5E0',
|
||||
chartBg: '#FFFFFF',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认股票代码
|
||||
*/
|
||||
export const DEFAULT_STOCK_CODE = '600000';
|
||||
|
||||
/**
|
||||
* 默认时间周期(天)
|
||||
*/
|
||||
export const DEFAULT_PERIOD = 60;
|
||||
|
||||
/**
|
||||
* 时间周期选项
|
||||
*/
|
||||
export const PERIOD_OPTIONS = [
|
||||
{ value: 30, label: '30天' },
|
||||
{ value: 60, label: '60天' },
|
||||
{ value: 120, label: '120天' },
|
||||
{ value: 250, label: '250天' },
|
||||
];
|
||||
@@ -0,0 +1,193 @@
|
||||
// src/views/Company/components/MarketDataView/hooks/useMarketData.ts
|
||||
// MarketDataView 数据获取 Hook
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { logger } from '@utils/logger';
|
||||
import { marketService } from '../services/marketService';
|
||||
import { DEFAULT_PERIOD } from '../constants';
|
||||
import type {
|
||||
MarketSummary,
|
||||
TradeDayData,
|
||||
FundingDayData,
|
||||
BigDealData,
|
||||
UnusualData,
|
||||
PledgeData,
|
||||
RiseAnalysis,
|
||||
MinuteData,
|
||||
UseMarketDataReturn,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* 市场数据获取 Hook
|
||||
* @param stockCode 股票代码
|
||||
* @param period 时间周期(天数)
|
||||
*/
|
||||
export const useMarketData = (
|
||||
stockCode: string,
|
||||
period: number = DEFAULT_PERIOD
|
||||
): UseMarketDataReturn => {
|
||||
// 主数据状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [summary, setSummary] = useState<MarketSummary | null>(null);
|
||||
const [tradeData, setTradeData] = useState<TradeDayData[]>([]);
|
||||
const [fundingData, setFundingData] = useState<FundingDayData[]>([]);
|
||||
const [bigDealData, setBigDealData] = useState<BigDealData>({ data: [], daily_stats: [] });
|
||||
const [unusualData, setUnusualData] = useState<UnusualData>({ data: [], grouped_data: [] });
|
||||
const [pledgeData, setPledgeData] = useState<PledgeData[]>([]);
|
||||
const [analysisMap, setAnalysisMap] = useState<Record<number, RiseAnalysis>>({});
|
||||
|
||||
// 分钟数据状态
|
||||
const [minuteData, setMinuteData] = useState<MinuteData | null>(null);
|
||||
const [minuteLoading, setMinuteLoading] = useState(false);
|
||||
|
||||
/**
|
||||
* 加载所有市场数据
|
||||
*/
|
||||
const loadMarketData = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
logger.debug('useMarketData', '开始加载市场数据', { stockCode, period });
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const [
|
||||
summaryRes,
|
||||
tradeRes,
|
||||
fundingRes,
|
||||
bigDealRes,
|
||||
unusualRes,
|
||||
pledgeRes,
|
||||
riseAnalysisRes,
|
||||
] = await Promise.all([
|
||||
marketService.getMarketSummary(stockCode),
|
||||
marketService.getTradeData(stockCode, period),
|
||||
marketService.getFundingData(stockCode, 30),
|
||||
marketService.getBigDealData(stockCode, 30),
|
||||
marketService.getUnusualData(stockCode, 30),
|
||||
marketService.getPledgeData(stockCode),
|
||||
marketService.getRiseAnalysis(stockCode),
|
||||
]);
|
||||
|
||||
// 设置概览数据
|
||||
if (summaryRes.success) {
|
||||
setSummary(summaryRes.data);
|
||||
}
|
||||
|
||||
// 设置交易数据
|
||||
if (tradeRes.success) {
|
||||
setTradeData(tradeRes.data);
|
||||
}
|
||||
|
||||
// 设置融资融券数据
|
||||
if (fundingRes.success) {
|
||||
setFundingData(fundingRes.data);
|
||||
}
|
||||
|
||||
// 设置大宗交易数据(包含 daily_stats)
|
||||
if (bigDealRes.success) {
|
||||
setBigDealData(bigDealRes);
|
||||
}
|
||||
|
||||
// 设置龙虎榜数据(包含 grouped_data)
|
||||
if (unusualRes.success) {
|
||||
setUnusualData(unusualRes);
|
||||
}
|
||||
|
||||
// 设置股权质押数据
|
||||
if (pledgeRes.success) {
|
||||
setPledgeData(pledgeRes.data);
|
||||
}
|
||||
|
||||
// 设置涨幅分析数据并创建映射
|
||||
if (riseAnalysisRes.success) {
|
||||
const tempAnalysisMap: Record<number, RiseAnalysis> = {};
|
||||
|
||||
if (tradeRes.success && tradeRes.data && riseAnalysisRes.data) {
|
||||
riseAnalysisRes.data.forEach((analysis) => {
|
||||
const dateIndex = tradeRes.data.findIndex(
|
||||
(item) => item.date.substring(0, 10) === analysis.trade_date
|
||||
);
|
||||
if (dateIndex !== -1) {
|
||||
tempAnalysisMap[dateIndex] = analysis;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setAnalysisMap(tempAnalysisMap);
|
||||
}
|
||||
|
||||
logger.info('useMarketData', '市场数据加载成功', { stockCode });
|
||||
} catch (error) {
|
||||
logger.error('useMarketData', 'loadMarketData', error, { stockCode, period });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [stockCode, period]);
|
||||
|
||||
/**
|
||||
* 加载分钟K线数据
|
||||
*/
|
||||
const loadMinuteData = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
|
||||
logger.debug('useMarketData', '开始加载分钟频数据', { stockCode });
|
||||
setMinuteLoading(true);
|
||||
|
||||
try {
|
||||
const data = await marketService.getMinuteData(stockCode);
|
||||
setMinuteData(data);
|
||||
|
||||
if (data.data && data.data.length > 0) {
|
||||
logger.info('useMarketData', '分钟频数据加载成功', {
|
||||
stockCode,
|
||||
dataPoints: data.data.length,
|
||||
});
|
||||
} else {
|
||||
logger.warn('useMarketData', '分钟频数据为空', { stockCode });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('useMarketData', 'loadMinuteData', error, { stockCode });
|
||||
setMinuteData({
|
||||
data: [],
|
||||
code: stockCode,
|
||||
name: '',
|
||||
trade_date: '',
|
||||
type: 'minute',
|
||||
});
|
||||
} finally {
|
||||
setMinuteLoading(false);
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
/**
|
||||
* 刷新所有数据
|
||||
*/
|
||||
const refetch = useCallback(async () => {
|
||||
await Promise.all([loadMarketData(), loadMinuteData()]);
|
||||
}, [loadMarketData, loadMinuteData]);
|
||||
|
||||
// 监听股票代码和周期变化,自动加载数据
|
||||
useEffect(() => {
|
||||
if (stockCode) {
|
||||
loadMarketData();
|
||||
loadMinuteData();
|
||||
}
|
||||
}, [stockCode, period, loadMarketData, loadMinuteData]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
summary,
|
||||
tradeData,
|
||||
fundingData,
|
||||
bigDealData,
|
||||
unusualData,
|
||||
pledgeData,
|
||||
minuteData,
|
||||
minuteLoading,
|
||||
analysisMap,
|
||||
refetch,
|
||||
loadMinuteData,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMarketData;
|
||||
File diff suppressed because it is too large
Load Diff
1049
src/views/Company/components/MarketDataView/index.tsx
Normal file
1049
src/views/Company/components/MarketDataView/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,173 @@
|
||||
// src/views/Company/components/MarketDataView/services/marketService.ts
|
||||
// MarketDataView API 服务层
|
||||
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
import { logger } from '@utils/logger';
|
||||
import type {
|
||||
MarketSummary,
|
||||
TradeDayData,
|
||||
FundingDayData,
|
||||
BigDealData,
|
||||
UnusualData,
|
||||
PledgeData,
|
||||
RiseAnalysis,
|
||||
MinuteData,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* API 响应包装类型
|
||||
*/
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 基础 URL
|
||||
*/
|
||||
const getBaseUrl = (): string => getApiBase();
|
||||
|
||||
/**
|
||||
* 通用 API 请求函数
|
||||
*/
|
||||
const apiRequest = async <T>(url: string): Promise<ApiResponse<T>> => {
|
||||
try {
|
||||
const response = await fetch(`${getBaseUrl()}${url}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
logger.error('marketService', 'apiRequest', error, { url });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 市场数据服务
|
||||
*/
|
||||
export const marketService = {
|
||||
/**
|
||||
* 获取市场概览数据
|
||||
* @param stockCode 股票代码
|
||||
*/
|
||||
async getMarketSummary(stockCode: string): Promise<ApiResponse<MarketSummary>> {
|
||||
return apiRequest<MarketSummary>(`/api/market/summary/${stockCode}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取交易日数据
|
||||
* @param stockCode 股票代码
|
||||
* @param days 天数,默认 60 天
|
||||
*/
|
||||
async getTradeData(stockCode: string, days: number = 60): Promise<ApiResponse<TradeDayData[]>> {
|
||||
return apiRequest<TradeDayData[]>(`/api/market/trade/${stockCode}?days=${days}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取融资融券数据
|
||||
* @param stockCode 股票代码
|
||||
* @param days 天数,默认 30 天
|
||||
*/
|
||||
async getFundingData(stockCode: string, days: number = 30): Promise<ApiResponse<FundingDayData[]>> {
|
||||
return apiRequest<FundingDayData[]>(`/api/market/funding/${stockCode}?days=${days}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取大宗交易数据
|
||||
* @param stockCode 股票代码
|
||||
* @param days 天数,默认 30 天
|
||||
*/
|
||||
async getBigDealData(stockCode: string, days: number = 30): Promise<BigDealData> {
|
||||
const response = await fetch(`${getBaseUrl()}/api/market/bigdeal/${stockCode}?days=${days}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取龙虎榜数据
|
||||
* @param stockCode 股票代码
|
||||
* @param days 天数,默认 30 天
|
||||
*/
|
||||
async getUnusualData(stockCode: string, days: number = 30): Promise<UnusualData> {
|
||||
const response = await fetch(`${getBaseUrl()}/api/market/unusual/${stockCode}?days=${days}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取股权质押数据
|
||||
* @param stockCode 股票代码
|
||||
*/
|
||||
async getPledgeData(stockCode: string): Promise<ApiResponse<PledgeData[]>> {
|
||||
return apiRequest<PledgeData[]>(`/api/market/pledge/${stockCode}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取涨幅分析数据
|
||||
* @param stockCode 股票代码
|
||||
* @param startDate 开始日期(可选)
|
||||
* @param endDate 结束日期(可选)
|
||||
*/
|
||||
async getRiseAnalysis(
|
||||
stockCode: string,
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
): Promise<ApiResponse<RiseAnalysis[]>> {
|
||||
let url = `/api/market/rise-analysis/${stockCode}`;
|
||||
if (startDate && endDate) {
|
||||
url += `?start_date=${startDate}&end_date=${endDate}`;
|
||||
}
|
||||
return apiRequest<RiseAnalysis[]>(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取分钟K线数据
|
||||
* @param stockCode 股票代码
|
||||
*/
|
||||
async getMinuteData(stockCode: string): Promise<MinuteData> {
|
||||
try {
|
||||
const response = await fetch(`${getBaseUrl()}/api/stock/${stockCode}/latest-minute`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch minute data');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 返回空数据结构
|
||||
return {
|
||||
data: [],
|
||||
code: stockCode,
|
||||
name: '',
|
||||
trade_date: '',
|
||||
type: 'minute',
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('marketService', 'getMinuteData', error, { stockCode });
|
||||
// 返回空数据结构
|
||||
return {
|
||||
data: [],
|
||||
code: stockCode,
|
||||
name: '',
|
||||
trade_date: '',
|
||||
type: 'minute',
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default marketService;
|
||||
383
src/views/Company/components/MarketDataView/types.ts
Normal file
383
src/views/Company/components/MarketDataView/types.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
// src/views/Company/components/MarketDataView/types.ts
|
||||
// MarketDataView 组件类型定义
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* 主题配置类型
|
||||
*/
|
||||
export interface Theme {
|
||||
primary: string;
|
||||
primaryDark: string;
|
||||
secondary: string;
|
||||
secondaryDark: string;
|
||||
success: string; // 涨色 - 红色
|
||||
danger: string; // 跌色 - 绿色
|
||||
warning: string;
|
||||
info: string;
|
||||
bgMain: string;
|
||||
bgCard: string;
|
||||
bgDark: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
textMuted: string;
|
||||
border: string;
|
||||
chartBg: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 交易日数据
|
||||
*/
|
||||
export interface TradeDayData {
|
||||
date: string;
|
||||
open: number;
|
||||
close: number;
|
||||
high: number;
|
||||
low: number;
|
||||
volume: number;
|
||||
amount: number;
|
||||
change_percent: number;
|
||||
turnover_rate?: number;
|
||||
pe_ratio?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分钟K线数据点
|
||||
*/
|
||||
export interface MinuteDataPoint {
|
||||
time: string;
|
||||
open: number;
|
||||
close: number;
|
||||
high: number;
|
||||
low: number;
|
||||
volume: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分钟K线数据
|
||||
*/
|
||||
export interface MinuteData {
|
||||
data: MinuteDataPoint[];
|
||||
code: string;
|
||||
name: string;
|
||||
trade_date: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 融资数据
|
||||
*/
|
||||
export interface FinancingInfo {
|
||||
balance: number;
|
||||
buy: number;
|
||||
repay: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 融券数据
|
||||
*/
|
||||
export interface SecuritiesInfo {
|
||||
balance: number;
|
||||
balance_amount: number;
|
||||
sell: number;
|
||||
repay: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 融资融券日数据
|
||||
*/
|
||||
export interface FundingDayData {
|
||||
date: string;
|
||||
financing: FinancingInfo;
|
||||
securities: SecuritiesInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 大宗交易明细
|
||||
*/
|
||||
export interface BigDealItem {
|
||||
buyer_dept?: string;
|
||||
seller_dept?: string;
|
||||
price?: number;
|
||||
volume?: number;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 大宗交易日统计
|
||||
*/
|
||||
export interface BigDealDayStats {
|
||||
date: string;
|
||||
count: number;
|
||||
total_volume: number;
|
||||
total_amount: number;
|
||||
avg_price?: number;
|
||||
deals?: BigDealItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 大宗交易数据
|
||||
*/
|
||||
export interface BigDealData {
|
||||
success?: boolean;
|
||||
data: BigDealItem[];
|
||||
daily_stats: BigDealDayStats[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 龙虎榜买卖方
|
||||
*/
|
||||
export interface UnusualTrader {
|
||||
dept_name: string;
|
||||
buy_amount?: number;
|
||||
sell_amount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 龙虎榜日数据
|
||||
*/
|
||||
export interface UnusualDayData {
|
||||
date: string;
|
||||
total_buy: number;
|
||||
total_sell: number;
|
||||
net_amount: number;
|
||||
buyers?: UnusualTrader[];
|
||||
sellers?: UnusualTrader[];
|
||||
info_types?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 龙虎榜数据
|
||||
*/
|
||||
export interface UnusualData {
|
||||
success?: boolean;
|
||||
data: unknown[];
|
||||
grouped_data: UnusualDayData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 股权质押数据
|
||||
*/
|
||||
export interface PledgeData {
|
||||
end_date: string;
|
||||
unrestricted_pledge: number;
|
||||
restricted_pledge: number;
|
||||
total_pledge: number;
|
||||
total_shares: number;
|
||||
pledge_ratio: number;
|
||||
pledge_count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新交易数据
|
||||
*/
|
||||
export interface LatestTrade {
|
||||
close: number;
|
||||
change_percent: number;
|
||||
volume: number;
|
||||
amount: number;
|
||||
turnover_rate: number;
|
||||
pe_ratio?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新融资融券数据
|
||||
*/
|
||||
export interface LatestFunding {
|
||||
financing_balance: number;
|
||||
securities_balance: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最新质押数据
|
||||
*/
|
||||
export interface LatestPledge {
|
||||
pledge_ratio: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 市场概览数据
|
||||
*/
|
||||
export interface MarketSummary {
|
||||
stock_code: string;
|
||||
stock_name: string;
|
||||
latest_trade?: LatestTrade;
|
||||
latest_funding?: LatestFunding;
|
||||
latest_pledge?: LatestPledge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 涨幅分析研报
|
||||
*/
|
||||
export interface VerificationReport {
|
||||
publisher?: string;
|
||||
match_score?: string;
|
||||
match_ratio?: number;
|
||||
declare_date?: string;
|
||||
report_title?: string;
|
||||
author?: string;
|
||||
verification_item?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 涨幅分析数据
|
||||
*/
|
||||
export interface RiseAnalysis {
|
||||
stock_code: string;
|
||||
stock_name: string;
|
||||
trade_date: string;
|
||||
rise_rate: number;
|
||||
close_price: number;
|
||||
volume: number;
|
||||
amount: number;
|
||||
main_business?: string;
|
||||
rise_reason_brief?: string;
|
||||
rise_reason_detail?: string;
|
||||
announcements?: string;
|
||||
verification_reports?: VerificationReport[];
|
||||
update_time?: string;
|
||||
create_time?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* MarketDataView 组件 Props
|
||||
*/
|
||||
export interface MarketDataViewProps {
|
||||
stockCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemedCard 组件 Props
|
||||
*/
|
||||
export interface ThemedCardProps {
|
||||
children: ReactNode;
|
||||
theme: Theme;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* MarkdownRenderer 组件 Props
|
||||
*/
|
||||
export interface MarkdownRendererProps {
|
||||
children: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* StockSummaryCard 组件 Props
|
||||
*/
|
||||
export interface StockSummaryCardProps {
|
||||
summary: MarketSummary;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* TradeDataTab 组件 Props
|
||||
*/
|
||||
export interface TradeDataTabProps {
|
||||
theme: Theme;
|
||||
tradeData: TradeDayData[];
|
||||
minuteData: MinuteData | null;
|
||||
minuteLoading: boolean;
|
||||
analysisMap: Record<number, RiseAnalysis>;
|
||||
onLoadMinuteData: () => void;
|
||||
onAnalysisClick: (analysis: RiseAnalysis) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* KLineChart 组件 Props
|
||||
*/
|
||||
export interface KLineChartProps {
|
||||
theme: Theme;
|
||||
tradeData: TradeDayData[];
|
||||
analysisMap: Record<number, RiseAnalysis>;
|
||||
onAnalysisClick: (analysis: RiseAnalysis) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* MinuteKLineChart 组件 Props
|
||||
*/
|
||||
export interface MinuteKLineChartProps {
|
||||
theme: Theme;
|
||||
minuteData: MinuteData | null;
|
||||
loading: boolean;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* TradeTable 组件 Props
|
||||
*/
|
||||
export interface TradeTableProps {
|
||||
theme: Theme;
|
||||
tradeData: TradeDayData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* FundingTab 组件 Props
|
||||
*/
|
||||
export interface FundingTabProps {
|
||||
theme: Theme;
|
||||
fundingData: FundingDayData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* BigDealTab 组件 Props
|
||||
*/
|
||||
export interface BigDealTabProps {
|
||||
theme: Theme;
|
||||
bigDealData: BigDealData;
|
||||
}
|
||||
|
||||
/**
|
||||
* UnusualTab 组件 Props
|
||||
*/
|
||||
export interface UnusualTabProps {
|
||||
theme: Theme;
|
||||
unusualData: UnusualData;
|
||||
}
|
||||
|
||||
/**
|
||||
* PledgeTab 组件 Props
|
||||
*/
|
||||
export interface PledgeTabProps {
|
||||
theme: Theme;
|
||||
pledgeData: PledgeData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* AnalysisModal 组件 Props
|
||||
*/
|
||||
export interface AnalysisModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
content: ReactNode;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* AnalysisModalContent 组件 Props
|
||||
*/
|
||||
export interface AnalysisModalContentProps {
|
||||
analysis: RiseAnalysis;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* useMarketData Hook 返回值
|
||||
*/
|
||||
export interface UseMarketDataReturn {
|
||||
loading: boolean;
|
||||
summary: MarketSummary | null;
|
||||
tradeData: TradeDayData[];
|
||||
fundingData: FundingDayData[];
|
||||
bigDealData: BigDealData;
|
||||
unusualData: UnusualData;
|
||||
pledgeData: PledgeData[];
|
||||
minuteData: MinuteData | null;
|
||||
minuteLoading: boolean;
|
||||
analysisMap: Record<number, RiseAnalysis>;
|
||||
refetch: () => Promise<void>;
|
||||
loadMinuteData: () => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
// src/views/Company/components/MarketDataView/utils/chartOptions.ts
|
||||
// MarketDataView ECharts 图表配置生成器
|
||||
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import type {
|
||||
Theme,
|
||||
TradeDayData,
|
||||
MinuteData,
|
||||
FundingDayData,
|
||||
PledgeData,
|
||||
RiseAnalysis,
|
||||
} from '../types';
|
||||
import { formatNumber } from './formatUtils';
|
||||
|
||||
/**
|
||||
* 计算移动平均线
|
||||
* @param data 收盘价数组
|
||||
* @param period 周期
|
||||
*/
|
||||
export const calculateMA = (data: number[], period: number): (number | null)[] => {
|
||||
const result: (number | null)[] = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (i < period - 1) {
|
||||
result.push(null);
|
||||
continue;
|
||||
}
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sum += data[i - j];
|
||||
}
|
||||
result.push(sum / period);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成日K线图配置
|
||||
*/
|
||||
export const getKLineOption = (
|
||||
theme: Theme,
|
||||
tradeData: TradeDayData[],
|
||||
analysisMap: Record<number, RiseAnalysis>
|
||||
): EChartsOption => {
|
||||
if (!tradeData || tradeData.length === 0) return {};
|
||||
|
||||
const dates = tradeData.map((item) => item.date.substring(5, 10));
|
||||
const kData = tradeData.map((item) => [item.open, item.close, item.low, item.high]);
|
||||
const volumes = tradeData.map((item) => item.volume);
|
||||
const closePrices = tradeData.map((item) => item.close);
|
||||
const ma5 = calculateMA(closePrices, 5);
|
||||
const ma10 = calculateMA(closePrices, 10);
|
||||
const ma20 = calculateMA(closePrices, 20);
|
||||
|
||||
// 创建涨幅分析标记点
|
||||
const scatterData: [number, number][] = [];
|
||||
Object.keys(analysisMap).forEach((dateIndex) => {
|
||||
const idx = parseInt(dateIndex);
|
||||
if (tradeData[idx]) {
|
||||
const value = tradeData[idx].high * 1.02;
|
||||
scatterData.push([idx, value]);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
backgroundColor: theme.chartBg,
|
||||
animation: true,
|
||||
legend: {
|
||||
data: ['K线', 'MA5', 'MA10', 'MA20'],
|
||||
top: 10,
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
lineStyle: {
|
||||
color: theme.primary,
|
||||
width: 1,
|
||||
opacity: 0.8,
|
||||
},
|
||||
},
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
borderColor: theme.primary,
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
},
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: dates,
|
||||
boundaryGap: false,
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted },
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
gridIndex: 1,
|
||||
data: dates,
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: false, lineStyle: { color: theme.textMuted } },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: theme.border,
|
||||
},
|
||||
},
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted },
|
||||
},
|
||||
{
|
||||
scale: true,
|
||||
gridIndex: 1,
|
||||
splitNumber: 2,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
grid: [
|
||||
{
|
||||
left: '10%',
|
||||
right: '10%',
|
||||
height: '50%',
|
||||
},
|
||||
{
|
||||
left: '10%',
|
||||
right: '10%',
|
||||
top: '65%',
|
||||
height: '20%',
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'K线',
|
||||
type: 'candlestick',
|
||||
data: kData,
|
||||
itemStyle: {
|
||||
color: theme.success,
|
||||
color0: theme.danger,
|
||||
borderColor: theme.success,
|
||||
borderColor0: theme.danger,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MA5',
|
||||
type: 'line',
|
||||
data: ma5,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: theme.primary,
|
||||
width: 1,
|
||||
},
|
||||
itemStyle: {
|
||||
color: theme.primary,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MA10',
|
||||
type: 'line',
|
||||
data: ma10,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: theme.info,
|
||||
width: 1,
|
||||
},
|
||||
itemStyle: {
|
||||
color: theme.info,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MA20',
|
||||
type: 'line',
|
||||
data: ma20,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: theme.warning,
|
||||
width: 1,
|
||||
},
|
||||
itemStyle: {
|
||||
color: theme.warning,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '涨幅分析',
|
||||
type: 'scatter',
|
||||
data: scatterData,
|
||||
symbolSize: 30,
|
||||
symbol: 'pin',
|
||||
itemStyle: {
|
||||
color: '#FFD700',
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(255, 215, 0, 0.5)',
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '★',
|
||||
fontSize: 20,
|
||||
position: 'inside',
|
||||
color: '#FF6B6B',
|
||||
},
|
||||
emphasis: {
|
||||
scale: 1.5,
|
||||
itemStyle: {
|
||||
color: '#FFA500',
|
||||
},
|
||||
},
|
||||
z: 100,
|
||||
},
|
||||
{
|
||||
name: '成交量',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: volumes,
|
||||
itemStyle: {
|
||||
color: (params: { dataIndex: number }) => {
|
||||
const item = tradeData[params.dataIndex];
|
||||
return item.change_percent >= 0
|
||||
? 'rgba(255, 68, 68, 0.6)'
|
||||
: 'rgba(0, 200, 81, 0.6)';
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成分钟K线图配置
|
||||
*/
|
||||
export const getMinuteKLineOption = (theme: Theme, minuteData: MinuteData | null): EChartsOption => {
|
||||
if (!minuteData || !minuteData.data || minuteData.data.length === 0) return {};
|
||||
|
||||
const times = minuteData.data.map((item) => item.time);
|
||||
const kData = minuteData.data.map((item) => [item.open, item.close, item.low, item.high]);
|
||||
const volumes = minuteData.data.map((item) => item.volume);
|
||||
const closePrices = minuteData.data.map((item) => item.close);
|
||||
const avgPrice = calculateMA(closePrices, 5);
|
||||
|
||||
const openPrice = minuteData.data.length > 0 ? minuteData.data[0].open : 0;
|
||||
|
||||
return {
|
||||
backgroundColor: theme.chartBg,
|
||||
title: {
|
||||
text: `${minuteData.name} 分钟K线 (${minuteData.trade_date})`,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
subtextStyle: {
|
||||
color: theme.textMuted,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' },
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
borderColor: theme.primary,
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
fontSize: 12,
|
||||
},
|
||||
formatter: (params: unknown) => {
|
||||
const paramsArr = params as { name: string; marker: string; seriesName: string; data: number[] | number; value: number }[];
|
||||
let result = paramsArr[0].name + '<br/>';
|
||||
paramsArr.forEach((param) => {
|
||||
if (param.seriesName === '分钟K线') {
|
||||
const [open, close, , high] = param.data as number[];
|
||||
const low = (param.data as number[])[2];
|
||||
const changePercent =
|
||||
openPrice > 0 ? (((close - openPrice) / openPrice) * 100).toFixed(2) : '0.00';
|
||||
result += `${param.marker} ${param.seriesName}<br/>`;
|
||||
result += `开盘: <span style="font-weight: bold">${open.toFixed(2)}</span><br/>`;
|
||||
result += `收盘: <span style="font-weight: bold; color: ${close >= open ? theme.success : theme.danger}">${close.toFixed(2)}</span><br/>`;
|
||||
result += `最高: <span style="font-weight: bold">${high.toFixed(2)}</span><br/>`;
|
||||
result += `最低: <span style="font-weight: bold">${low.toFixed(2)}</span><br/>`;
|
||||
result += `涨跌: <span style="font-weight: bold; color: ${close >= openPrice ? theme.success : theme.danger}">${changePercent}%</span><br/>`;
|
||||
} else if (param.seriesName === '均价线') {
|
||||
result += `${param.marker} ${param.seriesName}: <span style="font-weight: bold">${(param.value as number).toFixed(2)}</span><br/>`;
|
||||
} else if (param.seriesName === '成交量') {
|
||||
result += `${param.marker} ${param.seriesName}: <span style="font-weight: bold">${formatNumber(param.value as number, 0)}</span><br/>`;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['分钟K线', '均价线', '成交量'],
|
||||
top: 35,
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
fontSize: 12,
|
||||
},
|
||||
itemWidth: 25,
|
||||
itemHeight: 14,
|
||||
},
|
||||
grid: [
|
||||
{
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
top: '20%',
|
||||
height: '60%',
|
||||
},
|
||||
{
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
top: '83%',
|
||||
height: '12%',
|
||||
},
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: times,
|
||||
boundaryGap: false,
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: {
|
||||
color: theme.textMuted,
|
||||
fontSize: 10,
|
||||
interval: 'auto',
|
||||
},
|
||||
splitLine: { show: false },
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
gridIndex: 1,
|
||||
data: times,
|
||||
boundaryGap: false,
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: {
|
||||
color: theme.textMuted,
|
||||
fontSize: 10,
|
||||
},
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted, fontSize: 10 },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: theme.border,
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
gridIndex: 1,
|
||||
scale: true,
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted, fontSize: 10 },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
xAxisIndex: [0, 1],
|
||||
start: 70,
|
||||
end: 100,
|
||||
minValueSpan: 20,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
xAxisIndex: [0, 1],
|
||||
type: 'slider',
|
||||
top: '95%',
|
||||
start: 70,
|
||||
end: 100,
|
||||
height: 20,
|
||||
handleSize: '100%',
|
||||
handleStyle: {
|
||||
color: theme.primary,
|
||||
},
|
||||
textStyle: {
|
||||
color: theme.textMuted,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '分钟K线',
|
||||
type: 'candlestick',
|
||||
data: kData,
|
||||
itemStyle: {
|
||||
color: theme.success,
|
||||
color0: theme.danger,
|
||||
borderColor: theme.success,
|
||||
borderColor0: theme.danger,
|
||||
borderWidth: 1,
|
||||
},
|
||||
barWidth: '60%',
|
||||
},
|
||||
{
|
||||
name: '均价线',
|
||||
type: 'line',
|
||||
data: avgPrice,
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: theme.info,
|
||||
width: 2,
|
||||
opacity: 0.8,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '成交量',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: volumes,
|
||||
barWidth: '50%',
|
||||
itemStyle: {
|
||||
color: (params: { dataIndex: number }) => {
|
||||
const item = minuteData.data[params.dataIndex];
|
||||
return item.close >= item.open
|
||||
? 'rgba(255, 68, 68, 0.6)'
|
||||
: 'rgba(0, 200, 81, 0.6)';
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成融资融券图表配置
|
||||
*/
|
||||
export const getFundingOption = (theme: Theme, fundingData: FundingDayData[]): EChartsOption => {
|
||||
if (!fundingData || fundingData.length === 0) return {};
|
||||
|
||||
const dates = fundingData.map((item) => item.date.substring(5, 10));
|
||||
const financing = fundingData.map((item) => item.financing.balance / 100000000);
|
||||
const securities = fundingData.map((item) => item.securities.balance_amount / 100000000);
|
||||
|
||||
return {
|
||||
backgroundColor: theme.chartBg,
|
||||
title: {
|
||||
text: '融资融券余额走势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
borderColor: theme.primary,
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
},
|
||||
formatter: (params: unknown) => {
|
||||
const paramsArr = params as { name: string; marker: string; seriesName: string; value: number }[];
|
||||
let result = paramsArr[0].name + '<br/>';
|
||||
paramsArr.forEach((param) => {
|
||||
result += `${param.marker} ${param.seriesName}: ${param.value.toFixed(2)}亿<br/>`;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['融资余额', '融券余额'],
|
||||
bottom: 10,
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '金额(亿)',
|
||||
nameTextStyle: { color: theme.textMuted },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: theme.border,
|
||||
},
|
||||
},
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '融资余额',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(255, 68, 68, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(255, 68, 68, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: theme.success,
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: theme.success,
|
||||
borderColor: theme.success,
|
||||
borderWidth: 2,
|
||||
},
|
||||
data: financing,
|
||||
},
|
||||
{
|
||||
name: '融券余额',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'diamond',
|
||||
symbolSize: 8,
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(0, 200, 81, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(0, 200, 81, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: theme.danger,
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: theme.danger,
|
||||
borderColor: theme.danger,
|
||||
borderWidth: 2,
|
||||
},
|
||||
data: securities,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成股权质押图表配置
|
||||
*/
|
||||
export const getPledgeOption = (theme: Theme, pledgeData: PledgeData[]): EChartsOption => {
|
||||
if (!pledgeData || pledgeData.length === 0) return {};
|
||||
|
||||
const dates = pledgeData.map((item) => item.end_date.substring(5, 10));
|
||||
const ratios = pledgeData.map((item) => item.pledge_ratio);
|
||||
const counts = pledgeData.map((item) => item.pledge_count);
|
||||
|
||||
return {
|
||||
backgroundColor: theme.chartBg,
|
||||
title: {
|
||||
text: '股权质押趋势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
fontSize: 16,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
borderColor: theme.primary,
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['质押比例', '质押笔数'],
|
||||
bottom: 10,
|
||||
textStyle: {
|
||||
color: theme.textPrimary,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted },
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '质押比例(%)',
|
||||
nameTextStyle: { color: theme.textMuted },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: theme.border,
|
||||
},
|
||||
},
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted },
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '质押笔数',
|
||||
nameTextStyle: { color: theme.textMuted },
|
||||
axisLine: { lineStyle: { color: theme.textMuted } },
|
||||
axisLabel: { color: theme.textMuted },
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '质押比例',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
color: theme.warning,
|
||||
width: 2,
|
||||
shadowBlur: 10,
|
||||
shadowColor: theme.warning,
|
||||
},
|
||||
itemStyle: {
|
||||
color: theme.warning,
|
||||
borderColor: theme.bgCard,
|
||||
borderWidth: 2,
|
||||
},
|
||||
data: ratios,
|
||||
},
|
||||
{
|
||||
name: '质押笔数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
barWidth: '50%',
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: theme.primary },
|
||||
{ offset: 1, color: theme.primaryDark },
|
||||
],
|
||||
},
|
||||
borderRadius: [5, 5, 0, 0],
|
||||
},
|
||||
data: counts,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
calculateMA,
|
||||
getKLineOption,
|
||||
getMinuteKLineOption,
|
||||
getFundingOption,
|
||||
getPledgeOption,
|
||||
};
|
||||
175
src/views/Company/components/MarketDataView/utils/formatUtils.ts
Normal file
175
src/views/Company/components/MarketDataView/utils/formatUtils.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
// src/views/Company/components/MarketDataView/utils/formatUtils.ts
|
||||
// MarketDataView 格式化工具函数
|
||||
|
||||
/**
|
||||
* 格式化数字(自动转换为万/亿)
|
||||
* @param value 数值
|
||||
* @param decimals 小数位数,默认 2
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export const formatNumber = (value: number | null | undefined, decimals: number = 2): string => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
|
||||
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (isNaN(num)) return '-';
|
||||
|
||||
if (Math.abs(num) >= 100000000) {
|
||||
return (num / 100000000).toFixed(decimals) + '亿';
|
||||
} else if (Math.abs(num) >= 10000) {
|
||||
return (num / 10000).toFixed(decimals) + '万';
|
||||
}
|
||||
return num.toFixed(decimals);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param value 数值(已经是百分比形式,如 3.5 表示 3.5%)
|
||||
* @param decimals 小数位数,默认 2
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export const formatPercent = (value: number | null | undefined, decimals: number = 2): string => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
|
||||
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (isNaN(num)) return '-';
|
||||
|
||||
return num.toFixed(decimals) + '%';
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期(取前 10 位)
|
||||
* @param dateStr 日期字符串
|
||||
* @returns 格式化后的日期(YYYY-MM-DD)
|
||||
*/
|
||||
export const formatDate = (dateStr: string | null | undefined): string => {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.substring(0, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化价格
|
||||
* @param value 价格数值
|
||||
* @param decimals 小数位数,默认 2
|
||||
* @returns 格式化后的价格字符串
|
||||
*/
|
||||
export const formatPrice = (value: number | null | undefined, decimals: number = 2): string => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
|
||||
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (isNaN(num)) return '-';
|
||||
|
||||
return num.toFixed(decimals);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化成交量(带单位)
|
||||
* @param value 成交量数值
|
||||
* @returns 格式化后的成交量字符串
|
||||
*/
|
||||
export const formatVolume = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
|
||||
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (isNaN(num)) return '-';
|
||||
|
||||
if (num >= 100000000) {
|
||||
return (num / 100000000).toFixed(2) + '亿股';
|
||||
} else if (num >= 10000) {
|
||||
return (num / 10000).toFixed(2) + '万股';
|
||||
}
|
||||
return num.toFixed(0) + '股';
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化金额(带单位)
|
||||
* @param value 金额数值
|
||||
* @returns 格式化后的金额字符串
|
||||
*/
|
||||
export const formatAmount = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
|
||||
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (isNaN(num)) return '-';
|
||||
|
||||
if (Math.abs(num) >= 100000000) {
|
||||
return (num / 100000000).toFixed(2) + '亿';
|
||||
} else if (Math.abs(num) >= 10000) {
|
||||
return (num / 10000).toFixed(2) + '万';
|
||||
}
|
||||
return num.toFixed(2) + '元';
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化涨跌幅(带符号和颜色提示)
|
||||
* @param value 涨跌幅数值
|
||||
* @returns 带符号的涨跌幅字符串
|
||||
*/
|
||||
export const formatChange = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
|
||||
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (isNaN(num)) return '-';
|
||||
|
||||
const sign = num > 0 ? '+' : '';
|
||||
return sign + num.toFixed(2) + '%';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取涨跌颜色类型
|
||||
* @param value 涨跌幅数值
|
||||
* @returns 'up' | 'down' | 'neutral'
|
||||
*/
|
||||
export const getChangeType = (value: number | null | undefined): 'up' | 'down' | 'neutral' => {
|
||||
if (value === null || value === undefined) return 'neutral';
|
||||
|
||||
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
||||
if (isNaN(num) || num === 0) return 'neutral';
|
||||
|
||||
return num > 0 ? 'up' : 'down';
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化短日期(MM-DD)
|
||||
* @param dateStr 日期字符串
|
||||
* @returns 格式化后的短日期
|
||||
*/
|
||||
export const formatShortDate = (dateStr: string | null | undefined): string => {
|
||||
if (!dateStr) return '-';
|
||||
return dateStr.substring(5, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化时间(HH:mm)
|
||||
* @param timeStr 时间字符串
|
||||
* @returns 格式化后的时间
|
||||
*/
|
||||
export const formatTime = (timeStr: string | null | undefined): string => {
|
||||
if (!timeStr) return '-';
|
||||
// 支持多种格式
|
||||
if (timeStr.includes(':')) {
|
||||
return timeStr.substring(0, 5);
|
||||
}
|
||||
// 如果是 HHmm 格式
|
||||
if (timeStr.length >= 4) {
|
||||
return timeStr.substring(0, 2) + ':' + timeStr.substring(2, 4);
|
||||
}
|
||||
return timeStr;
|
||||
};
|
||||
|
||||
/**
|
||||
* 工具函数集合(兼容旧代码)
|
||||
*/
|
||||
export const formatUtils = {
|
||||
formatNumber,
|
||||
formatPercent,
|
||||
formatDate,
|
||||
formatPrice,
|
||||
formatVolume,
|
||||
formatAmount,
|
||||
formatChange,
|
||||
getChangeType,
|
||||
formatShortDate,
|
||||
formatTime,
|
||||
};
|
||||
|
||||
export default formatUtils;
|
||||
Reference in New Issue
Block a user