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:
zdl
2025-12-10 15:14:23 +08:00
parent 722d038b56
commit bfb6ef63d0
14 changed files with 3605 additions and 2060 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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天' },
];

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

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