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