refactor(KLineModule): 黑金主题 + 精简组件结构

- KLineModule 应用黑金主题(渐变背景、金色按钮、金色图标)
- 删除 TradeTable、MinuteStats、TradeAnalysis 组件
- 删除 atoms 目录,EmptyState 内联到 KLineModule
- 更新 types.ts 移除 TradeTableProps
- 更新导出文件

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-16 15:03:50 +08:00
parent 406b951e53
commit 1022fa4077
9 changed files with 111 additions and 388 deletions

View File

@@ -1,5 +1,5 @@
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/KLineModule.tsx
// K线模块 - 日K线/分钟K线切换展示
// K线模块 - 日K线/分钟K线切换展示(黑金主题)
import React, { useState } from 'react';
import {
@@ -12,19 +12,29 @@ import {
Badge,
Center,
Spinner,
CardBody,
CardHeader,
Heading,
Icon,
} from '@chakra-ui/react';
import { RepeatIcon } from '@chakra-ui/icons';
import { BarChart2, Clock } from 'lucide-react';
import { RepeatIcon, InfoIcon } from '@chakra-ui/icons';
import { BarChart2, Clock, TrendingUp } from 'lucide-react';
import ReactECharts from 'echarts-for-react';
import ThemedCard from '../../ThemedCard';
import { darkGoldTheme } from '../../../constants';
import { getKLineOption, getMinuteKLineOption } from '../../../utils/chartOptions';
import { MinuteStats, TradeAnalysis, EmptyState } from './atoms';
import type { KLineModuleProps } from '../../../types';
// 空状态组件(内联)
const EmptyState: React.FC<{ title: string; description: string }> = ({ title, description }) => (
<Center h="300px">
<VStack spacing={4}>
<Icon as={InfoIcon} color={darkGoldTheme.textMuted} boxSize={12} />
<VStack spacing={2}>
<Text color={darkGoldTheme.textMuted} fontSize="lg">{title}</Text>
<Text color={darkGoldTheme.textMuted} fontSize="sm" textAlign="center">{description}</Text>
</VStack>
</VStack>
</Center>
);
// 重新导出类型供外部使用
export type { KLineModuleProps } from '../../../types';
@@ -50,16 +60,68 @@ const KLineModule: React.FC<KLineModuleProps> = ({
}
};
// 黑金主题按钮样式
const activeButtonStyle = {
bg: `linear-gradient(135deg, ${darkGoldTheme.gold} 0%, ${darkGoldTheme.orange} 100%)`,
color: '#1a1a2e',
borderColor: darkGoldTheme.gold,
_hover: {
bg: `linear-gradient(135deg, ${darkGoldTheme.goldLight} 0%, ${darkGoldTheme.gold} 100%)`,
},
};
const inactiveButtonStyle = {
bg: 'transparent',
color: darkGoldTheme.textMuted,
borderColor: darkGoldTheme.border,
_hover: {
bg: 'rgba(212, 175, 55, 0.1)',
borderColor: darkGoldTheme.gold,
color: darkGoldTheme.gold,
},
};
return (
<ThemedCard theme={theme}>
<CardHeader>
<Box
bg={darkGoldTheme.bgCard}
borderRadius="xl"
border="1px solid"
borderColor={darkGoldTheme.border}
overflow="hidden"
transition="all 0.3s ease"
_hover={{
borderColor: darkGoldTheme.borderHover,
boxShadow: `0 4px 20px rgba(212, 175, 55, 0.15)`,
}}
>
{/* 卡片头部 */}
<Box px={6} py={4} borderBottom="1px solid" borderColor={darkGoldTheme.border}>
<HStack justify="space-between" align="center">
<HStack spacing={3}>
<Heading size="md" color={theme.textSecondary}>
<Box
p={2}
borderRadius="lg"
bg={darkGoldTheme.tagBg}
>
<TrendingUp size={20} color={darkGoldTheme.gold} />
</Box>
<Text
fontSize="lg"
fontWeight="bold"
bgGradient={`linear(to-r, ${darkGoldTheme.gold}, ${darkGoldTheme.orange})`}
bgClip="text"
>
{mode === 'daily' ? '日K线图' : '分钟K线图'}
</Heading>
</Text>
{mode === 'minute' && minuteData?.trade_date && (
<Badge colorScheme="blue" fontSize="xs">
<Badge
bg={darkGoldTheme.tagBg}
color={darkGoldTheme.gold}
fontSize="xs"
px={2}
py={1}
borderRadius="md"
>
{minuteData.trade_date}
</Badge>
)}
@@ -72,39 +134,38 @@ const KLineModule: React.FC<KLineModuleProps> = ({
leftIcon={<RepeatIcon />}
size="sm"
variant="outline"
colorScheme="blue"
onClick={onLoadMinuteData}
isLoading={minuteLoading}
loadingText="获取中"
{...inactiveButtonStyle}
>
</Button>
)}
{/* 模式切换按钮组 */}
<ButtonGroup size="sm" isAttached variant="outline">
<ButtonGroup size="sm" isAttached>
<Button
leftIcon={<BarChart2 size={16} />}
colorScheme={mode === 'daily' ? 'blue' : 'gray'}
variant={mode === 'daily' ? 'solid' : 'outline'}
leftIcon={<BarChart2 size={14} />}
onClick={() => handleModeChange('daily')}
{...(mode === 'daily' ? activeButtonStyle : inactiveButtonStyle)}
>
K
</Button>
<Button
leftIcon={<Clock size={16} />}
colorScheme={mode === 'minute' ? 'blue' : 'gray'}
variant={mode === 'minute' ? 'solid' : 'outline'}
leftIcon={<Clock size={14} />}
onClick={() => handleModeChange('minute')}
{...(mode === 'minute' ? activeButtonStyle : inactiveButtonStyle)}
>
</Button>
</ButtonGroup>
</HStack>
</HStack>
</CardHeader>
</Box>
<CardBody>
{/* 卡片内容 */}
<Box p={6}>
{mode === 'daily' ? (
// 日K线图
tradeData.length > 0 ? (
@@ -112,16 +173,12 @@ const KLineModule: React.FC<KLineModuleProps> = ({
<ReactECharts
option={getKLineOption(theme, tradeData, analysisMap)}
style={{ height: '100%', width: '100%' }}
theme="light"
theme="dark"
onEvents={{ click: onChartClick }}
/>
</Box>
) : (
<EmptyState
theme={theme}
title="暂无日K线数据"
description="该股票暂无交易数据"
/>
<EmptyState title="暂无日K线数据" description="该股票暂无交易数据" />
)
) : (
// 分钟K线图
@@ -131,37 +188,29 @@ const KLineModule: React.FC<KLineModuleProps> = ({
<Spinner
thickness="4px"
speed="0.65s"
emptyColor={theme.bgDark}
color={theme.primary}
emptyColor="rgba(212, 175, 55, 0.2)"
color={darkGoldTheme.gold}
size="lg"
/>
<Text color={theme.textMuted} fontSize="sm">
<Text color={darkGoldTheme.textMuted} fontSize="sm">
...
</Text>
</VStack>
</Center>
) : hasMinuteData ? (
<VStack spacing={6} align="stretch">
<Box h="500px">
<ReactECharts
option={getMinuteKLineOption(theme, minuteData)}
style={{ height: '100%', width: '100%' }}
theme="light"
/>
</Box>
<MinuteStats theme={theme} data={minuteData.data} />
<TradeAnalysis theme={theme} data={minuteData.data} />
</VStack>
<Box h="500px">
<ReactECharts
option={getMinuteKLineOption(theme, minuteData)}
style={{ height: '100%', width: '100%' }}
theme="dark"
/>
</Box>
) : (
<EmptyState
theme={theme}
title="暂无分钟数据"
description="点击右上角刷新按钮获取当日分钟频数据"
/>
<EmptyState title="暂无分钟数据" description="点击刷新按钮获取当日分钟频数据" />
)
)}
</CardBody>
</ThemedCard>
</Box>
</Box>
);
};

View File

@@ -1,87 +0,0 @@
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/TradeTable.tsx
// 交易明细表格组件
import React from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
CardBody,
CardHeader,
Heading,
} from '@chakra-ui/react';
import ThemedCard from '../../ThemedCard';
import { formatNumber, formatPercent } from '../../../utils/formatUtils';
import type { Theme, TradeDayData } from '../../../types';
export interface TradeTableProps {
theme: Theme;
tradeData: TradeDayData[];
}
const TradeTable: React.FC<TradeTableProps> = ({ theme, tradeData }) => {
return (
<ThemedCard theme={theme}>
<CardHeader>
<Heading size="md" color={theme.textSecondary}>
</Heading>
</CardHeader>
<CardBody>
<TableContainer>
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th color={theme.textSecondary}></Th>
<Th isNumeric color={theme.textSecondary}></Th>
<Th isNumeric color={theme.textSecondary}></Th>
<Th isNumeric color={theme.textSecondary}></Th>
<Th isNumeric color={theme.textSecondary}></Th>
<Th isNumeric color={theme.textSecondary}></Th>
<Th isNumeric color={theme.textSecondary}></Th>
<Th isNumeric color={theme.textSecondary}></Th>
</Tr>
</Thead>
<Tbody>
{tradeData
.slice(-10)
.reverse()
.map((item, idx) => (
<Tr key={idx} _hover={{ bg: theme.bgDark }}>
<Td color={theme.textPrimary}>{item.date}</Td>
<Td isNumeric color={theme.textPrimary}>{item.open}</Td>
<Td isNumeric color={theme.textPrimary}>{item.high}</Td>
<Td isNumeric color={theme.textPrimary}>{item.low}</Td>
<Td isNumeric color={theme.textPrimary} fontWeight="bold">
{item.close}
</Td>
<Td
isNumeric
color={item.change_percent >= 0 ? theme.success : theme.danger}
fontWeight="bold"
>
{item.change_percent >= 0 ? '+' : ''}
{formatPercent(item.change_percent)}
</Td>
<Td isNumeric color={theme.textPrimary}>
{formatNumber(item.volume, 0)}
</Td>
<Td isNumeric color={theme.textPrimary}>
{formatNumber(item.amount)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</CardBody>
</ThemedCard>
);
};
export default TradeTable;

View File

@@ -1,40 +0,0 @@
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/atoms/EmptyState.tsx
// 空状态组件
import React from 'react';
import { Center, VStack, Text, Icon } from '@chakra-ui/react';
import { InfoIcon } from '@chakra-ui/icons';
import type { Theme } from '../../../../types';
export interface EmptyStateProps {
theme: Theme;
title?: string;
description?: string;
height?: string;
}
const EmptyState: React.FC<EmptyStateProps> = ({
theme,
title = '暂无分钟频数据',
description = '点击"获取分钟数据"按钮加载最新的交易日分钟频数据',
height = '300px',
}) => {
return (
<Center h={height}>
<VStack spacing={4}>
<Icon as={InfoIcon} color={theme.textMuted} boxSize={12} />
<VStack spacing={2}>
<Text color={theme.textMuted} fontSize="lg">
{title}
</Text>
<Text color={theme.textMuted} fontSize="sm" textAlign="center">
{description}
</Text>
</VStack>
</VStack>
</Center>
);
};
export default EmptyState;

View File

@@ -1,97 +0,0 @@
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/atoms/MinuteStats.tsx
// 分钟数据统计组件
import React from 'react';
import {
SimpleGrid,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
HStack,
Text,
Icon,
} from '@chakra-ui/react';
import {
ChevronDownIcon,
ChevronUpIcon,
ArrowUpIcon,
ArrowDownIcon,
} from '@chakra-ui/icons';
import type { Theme, MinuteDataPoint } from '../../../../types';
export interface MinuteStatsProps {
theme: Theme;
data: MinuteDataPoint[];
}
const MinuteStats: React.FC<MinuteStatsProps> = ({ theme, data }) => {
if (data.length === 0) return null;
const firstOpen = data[0]?.open || 0;
const lastClose = data[data.length - 1]?.close || 0;
const highPrice = Math.max(...data.map((item) => item.high).filter(Boolean));
const lowPrice = Math.min(...data.map((item) => item.low).filter(Boolean));
const isUp = lastClose >= firstOpen;
const changePercent = firstOpen ? Math.abs(((lastClose - firstOpen) / firstOpen) * 100) : 0;
return (
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
<Stat>
<StatLabel color={theme.textMuted} fontSize="sm">
<HStack spacing={1}>
<Icon as={ArrowUpIcon} boxSize={3} />
<Text></Text>
</HStack>
</StatLabel>
<StatNumber color={theme.textPrimary} fontSize="lg">
{firstOpen?.toFixed(2) || '-'}
</StatNumber>
</Stat>
<Stat>
<StatLabel color={theme.textMuted} fontSize="sm">
<HStack spacing={1}>
<Icon as={ArrowDownIcon} boxSize={3} />
<Text></Text>
</HStack>
</StatLabel>
<StatNumber color={isUp ? theme.success : theme.danger} fontSize="lg">
{lastClose?.toFixed(2) || '-'}
</StatNumber>
<StatHelpText fontSize="xs">
<StatArrow type={isUp ? 'increase' : 'decrease'} />
{changePercent.toFixed(2)}%
</StatHelpText>
</Stat>
<Stat>
<StatLabel color={theme.textMuted} fontSize="sm">
<HStack spacing={1}>
<Icon as={ChevronUpIcon} boxSize={3} />
<Text></Text>
</HStack>
</StatLabel>
<StatNumber color={theme.success} fontSize="lg">
{highPrice.toFixed(2)}
</StatNumber>
</Stat>
<Stat>
<StatLabel color={theme.textMuted} fontSize="sm">
<HStack spacing={1}>
<Icon as={ChevronDownIcon} boxSize={3} />
<Text></Text>
</HStack>
</StatLabel>
<StatNumber color={theme.danger} fontSize="lg">
{lowPrice.toFixed(2)}
</StatNumber>
</Stat>
</SimpleGrid>
);
};
export default MinuteStats;

View File

@@ -1,76 +0,0 @@
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/atoms/TradeAnalysis.tsx
// 成交数据分析组件
import React from 'react';
import { Box, Text, HStack, Badge, Grid } from '@chakra-ui/react';
import { formatNumber } from '../../../../utils/formatUtils';
import type { Theme, MinuteDataPoint } from '../../../../types';
export interface TradeAnalysisProps {
theme: Theme;
data: MinuteDataPoint[];
}
const TradeAnalysis: React.FC<TradeAnalysisProps> = ({ theme, data }) => {
if (data.length === 0) return null;
const totalVolume = data.reduce((sum, item) => sum + item.volume, 0);
const totalAmount = data.reduce((sum, item) => sum + item.amount, 0);
const avgPrice = data.reduce((sum, item) => sum + item.close, 0) / data.length;
const maxVolume = Math.max(...data.map((item) => item.volume));
const activeTime = data.find((item) => item.volume === maxVolume);
return (
<Box
p={4}
bg={theme.bgDark}
borderRadius="lg"
border="1px solid"
borderColor={theme.border}
>
<HStack justify="space-between" mb={4}>
<Text fontWeight="bold" color={theme.textSecondary}>
</Text>
<HStack spacing={2}>
<Badge colorScheme="purple" fontSize="xs">
: {formatNumber(totalVolume, 0)}
</Badge>
<Badge colorScheme="orange" fontSize="xs">
: {formatNumber(totalAmount)}
</Badge>
</HStack>
</HStack>
<Grid templateColumns="repeat(3, 1fr)" gap={4}>
<Box>
<Text fontSize="sm" color={theme.textMuted} mb={2}>
</Text>
<Text fontSize="sm" color={theme.textPrimary}>
{activeTime ? `${activeTime.time} (${formatNumber(maxVolume, 0)})` : '-'}
</Text>
</Box>
<Box>
<Text fontSize="sm" color={theme.textMuted} mb={2}>
</Text>
<Text fontSize="sm" color={theme.textPrimary}>
{avgPrice.toFixed(2)}
</Text>
</Box>
<Box>
<Text fontSize="sm" color={theme.textMuted} mb={2}>
</Text>
<Text fontSize="sm" color={theme.textPrimary}>
{data.length}
</Text>
</Box>
</Grid>
</Box>
);
};
export default TradeAnalysis;

View File

@@ -1,10 +0,0 @@
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/atoms/index.ts
// 原子组件统一导出
export { default as MinuteStats } from './MinuteStats';
export { default as TradeAnalysis } from './TradeAnalysis';
export { default as EmptyState } from './EmptyState';
export type { MinuteStatsProps } from './MinuteStats';
export type { TradeAnalysisProps } from './TradeAnalysis';
export type { EmptyStateProps } from './EmptyState';

View File

@@ -1,11 +1,9 @@
// src/views/Company/components/MarketDataView/components/panels/TradeDataPanel/index.tsx
// 交易数据面板 - K线模块日K/分钟切换)、交易明细表格
// 交易数据面板 - K线模块日K/分钟切换)
import React from 'react';
import { VStack } from '@chakra-ui/react';
import KLineModule from './KLineModule';
import TradeTable from './TradeTable';
import type { Theme, TradeDayData, MinuteData, RiseAnalysis } from '../../../types';
export interface TradeDataPanelProps {
@@ -28,19 +26,15 @@ const TradeDataPanel: React.FC<TradeDataPanelProps> = ({
onChartClick,
}) => {
return (
<VStack spacing={6} align="stretch">
<KLineModule
theme={theme}
tradeData={tradeData}
minuteData={minuteData}
minuteLoading={minuteLoading}
analysisMap={analysisMap}
onLoadMinuteData={onLoadMinuteData}
onChartClick={onChartClick}
/>
<TradeTable theme={theme} tradeData={tradeData} />
</VStack>
<KLineModule
theme={theme}
tradeData={tradeData}
minuteData={minuteData}
minuteLoading={minuteLoading}
analysisMap={analysisMap}
onLoadMinuteData={onLoadMinuteData}
onChartClick={onChartClick}
/>
);
};
@@ -48,6 +42,4 @@ export default TradeDataPanel;
// 导出子组件供外部按需使用
export { default as KLineModule } from './KLineModule';
export { default as TradeTable } from './TradeTable';
export type { KLineModuleProps } from './KLineModule';
export type { TradeTableProps } from './TradeTable';

View File

@@ -15,5 +15,5 @@ export type { UnusualPanelProps } from './UnusualPanel';
export type { PledgePanelProps } from './PledgePanel';
// 导出 TradeDataPanel 子组件
export { KLineModule, TradeTable } from './TradeDataPanel';
export type { KLineModuleProps, TradeTableProps } from './TradeDataPanel';
export { KLineModule } from './TradeDataPanel';
export type { KLineModuleProps } from './TradeDataPanel';

View File

@@ -299,14 +299,6 @@ export interface KLineModuleProps {
onChartClick: (params: { seriesName?: string; data?: [number, number] }) => void;
}
/**
* TradeTable 组件 Props
*/
export interface TradeTableProps {
theme: Theme;
tradeData: TradeDayData[];
}
/**
* FundingTab 组件 Props
*/