refactor(MarketDataView): 提取 DataRow 原子组件,样式统一

- 新增 shared/DataRow.tsx:通用数据行组件(支持 gold/orange/red/green 变体)
- 新增样式常量:financingRowStyle, securitiesRowStyle, buyRowStyle, sellRowStyle, dayCardStyle
- FundingPanel: 使用 useMemo 缓存图表配置和数据,使用 DataRow 替代重复结构
- BigDealPanel: 使用 dayCardStyle 替代内联样式

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 14:20:46 +08:00
parent 852d5fd188
commit 94854fee3e
6 changed files with 306 additions and 229 deletions

View File

@@ -20,6 +20,7 @@ import {
import { formatNumber } from '../../utils/formatUtils'; import { formatNumber } from '../../utils/formatUtils';
import { darkGoldTheme } from '../../constants'; import { darkGoldTheme } from '../../constants';
import { DarkGoldCard, DarkGoldBadge, EmptyState } from '../shared'; import { DarkGoldCard, DarkGoldBadge, EmptyState } from '../shared';
import { dayCardStyle } from '../shared/styles';
import type { BigDealData } from '../../types'; import type { BigDealData } from '../../types';
export interface BigDealPanelProps { export interface BigDealPanelProps {
@@ -32,14 +33,7 @@ const BigDealPanel: React.FC<BigDealPanelProps> = ({ bigDealData }) => {
{bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? ( {bigDealData?.daily_stats && bigDealData.daily_stats.length > 0 ? (
<VStack spacing={4} align="stretch"> <VStack spacing={4} align="stretch">
{bigDealData.daily_stats.map((dayStats, idx) => ( {bigDealData.daily_stats.map((dayStats, idx) => (
<Box <Box key={idx} sx={dayCardStyle}>
key={idx}
p={4}
bg="rgba(212, 175, 55, 0.05)"
borderRadius="lg"
border="1px solid"
borderColor="rgba(212, 175, 55, 0.15)"
>
<HStack justify="space-between" mb={4} flexWrap="wrap" gap={2}> <HStack justify="space-between" mb={4} flexWrap="wrap" gap={2}>
<Text fontSize="md" fontWeight="bold" color={darkGoldTheme.gold}> <Text fontSize="md" fontWeight="bold" color={darkGoldTheme.gold}>
{dayStats.date} {dayStats.date}

View File

@@ -1,21 +1,14 @@
// src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx // src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx
// 融资融券面板 - 黑金主题 // 融资融券面板 - 黑金主题
import React, { memo } from 'react'; import React, { memo, useMemo } from 'react';
import { import { Box, VStack, Grid } from '@chakra-ui/react';
Box,
Text,
VStack,
HStack,
Grid,
Heading,
} from '@chakra-ui/react';
import ECharts from '@components/Charts/ECharts'; import ECharts from '@components/Charts/ECharts';
import { formatNumber } from '../../utils/formatUtils'; import { formatNumber } from '../../utils/formatUtils';
import { getFundingDarkGoldOption } from '../../utils/chartOptions'; import { getFundingDarkGoldOption } from '../../utils/chartOptions';
import { darkGoldTheme } from '../../constants'; import { darkGoldTheme } from '../../constants';
import { DarkGoldCard } from '../shared'; import { DarkGoldCard, DataRow } from '../shared';
import { darkGoldCardFullStyle } from '../shared/styles'; import { darkGoldCardFullStyle } from '../shared/styles';
import type { FundingDayData } from '../../types'; import type { FundingDayData } from '../../types';
@@ -24,6 +17,17 @@ export interface FundingPanelProps {
} }
const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => { const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
// 缓存图表配置
const chartOption = useMemo(() => {
if (fundingData.length === 0) return {};
return getFundingDarkGoldOption(fundingData);
}, [fundingData]);
// 缓存最近5条数据倒序
const recentData = useMemo(() => {
return fundingData.slice(-5).reverse();
}, [fundingData]);
return ( return (
<VStack spacing={6} align="stretch"> <VStack spacing={6} align="stretch">
{/* 图表卡片 */} {/* 图表卡片 */}
@@ -31,7 +35,7 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
{fundingData.length > 0 && ( {fundingData.length > 0 && (
<Box h="400px"> <Box h="400px">
<ECharts <ECharts
option={getFundingDarkGoldOption(fundingData)} option={chartOption}
style={{ height: '100%', width: '100%' }} style={{ height: '100%', width: '100%' }}
theme="dark" theme="dark"
/> />
@@ -43,38 +47,14 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
{/* 融资数据 */} {/* 融资数据 */}
<DarkGoldCard title="融资数据" titleColor={darkGoldTheme.gold}> <DarkGoldCard title="融资数据" titleColor={darkGoldTheme.gold}>
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
{fundingData {recentData.map((item, idx) => (
.slice(-5) <DataRow
.reverse()
.map((item, idx) => (
<Box
key={idx} key={idx}
p={3} variant="gold"
bg="rgba(212, 175, 55, 0.08)" label={item.date}
borderRadius="md" value={formatNumber(item.financing.balance)}
border="1px solid" subValue={`买入${formatNumber(item.financing.buy)} / 偿还${formatNumber(item.financing.repay)}`}
borderColor="rgba(212, 175, 55, 0.15)" />
transition="all 0.2s"
_hover={{
bg: 'rgba(212, 175, 55, 0.12)',
borderColor: 'rgba(212, 175, 55, 0.3)',
}}
>
<HStack justify="space-between">
<Text color={darkGoldTheme.textMuted} fontSize="sm">
{item.date}
</Text>
<VStack align="end" spacing={0}>
<Text color={darkGoldTheme.gold} fontWeight="bold">
{formatNumber(item.financing.balance)}
</Text>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{formatNumber(item.financing.buy)} /
{formatNumber(item.financing.repay)}
</Text>
</VStack>
</HStack>
</Box>
))} ))}
</VStack> </VStack>
</DarkGoldCard> </DarkGoldCard>
@@ -82,38 +62,14 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
{/* 融券数据 */} {/* 融券数据 */}
<DarkGoldCard title="融券数据" titleColor={darkGoldTheme.orange}> <DarkGoldCard title="融券数据" titleColor={darkGoldTheme.orange}>
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
{fundingData {recentData.map((item, idx) => (
.slice(-5) <DataRow
.reverse()
.map((item, idx) => (
<Box
key={idx} key={idx}
p={3} variant="orange"
bg="rgba(255, 149, 0, 0.08)" label={item.date}
borderRadius="md" value={formatNumber(item.securities.balance)}
border="1px solid" subValue={`卖出${formatNumber(item.securities.sell)} / 偿还${formatNumber(item.securities.repay)}`}
borderColor="rgba(255, 149, 0, 0.15)" />
transition="all 0.2s"
_hover={{
bg: 'rgba(255, 149, 0, 0.12)',
borderColor: 'rgba(255, 149, 0, 0.3)',
}}
>
<HStack justify="space-between">
<Text color={darkGoldTheme.textMuted} fontSize="sm">
{item.date}
</Text>
<VStack align="end" spacing={0}>
<Text color={darkGoldTheme.orange} fontWeight="bold">
{formatNumber(item.securities.balance)}
</Text>
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{formatNumber(item.securities.sell)} /
{formatNumber(item.securities.repay)}
</Text>
</VStack>
</HStack>
</Box>
))} ))}
</VStack> </VStack>
</DarkGoldCard> </DarkGoldCard>

View File

@@ -2,17 +2,12 @@
// 龙虎榜面板 - 黑金主题 // 龙虎榜面板 - 黑金主题
import React, { memo } from 'react'; import React, { memo } from 'react';
import { import { Box, Text, VStack, HStack, Grid } from '@chakra-ui/react';
Box,
Text,
VStack,
HStack,
Grid,
} from '@chakra-ui/react';
import { formatNumber } from '../../utils/formatUtils'; import { formatNumber } from '../../utils/formatUtils';
import { darkGoldTheme } from '../../constants'; import { darkGoldTheme } from '../../constants';
import { DarkGoldCard, DarkGoldBadge, EmptyState } from '../shared'; import { DarkGoldCard, DarkGoldBadge, EmptyState, DataRow } from '../shared';
import { dayCardStyle } from '../shared/styles';
import type { UnusualData } from '../../types'; import type { UnusualData } from '../../types';
export interface UnusualPanelProps { export interface UnusualPanelProps {
@@ -25,14 +20,7 @@ const UnusualPanel: React.FC<UnusualPanelProps> = ({ unusualData }) => {
{unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? ( {unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? (
<VStack spacing={4} align="stretch"> <VStack spacing={4} align="stretch">
{unusualData.grouped_data.map((dayData, idx) => ( {unusualData.grouped_data.map((dayData, idx) => (
<Box <Box key={idx} sx={dayCardStyle}>
key={idx}
p={4}
bg="rgba(212, 175, 55, 0.05)"
borderRadius="lg"
border="1px solid"
borderColor="rgba(212, 175, 55, 0.15)"
>
<HStack justify="space-between" mb={4} flexWrap="wrap" gap={2}> <HStack justify="space-between" mb={4} flexWrap="wrap" gap={2}>
<Text fontSize="md" fontWeight="bold" color={darkGoldTheme.gold}> <Text fontSize="md" fontWeight="bold" color={darkGoldTheme.gold}>
{dayData.date} {dayData.date}
@@ -58,32 +46,14 @@ const UnusualPanel: React.FC<UnusualPanelProps> = ({ unusualData }) => {
<VStack spacing={1} align="stretch"> <VStack spacing={1} align="stretch">
{dayData.buyers && dayData.buyers.length > 0 ? ( {dayData.buyers && dayData.buyers.length > 0 ? (
dayData.buyers.slice(0, 5).map((buyer, i) => ( dayData.buyers.slice(0, 5).map((buyer, i) => (
<HStack <DataRow
key={i} key={i}
justify="space-between" variant="red"
p={2} label={buyer.dept_name}
bg="rgba(255, 68, 68, 0.08)" value={formatNumber(buyer.buy_amount)}
borderRadius="md"
border="1px solid"
borderColor="rgba(255, 68, 68, 0.15)"
transition="all 0.2s"
_hover={{
bg: 'rgba(255, 68, 68, 0.12)',
borderColor: 'rgba(255, 68, 68, 0.3)',
}}
>
<Text
fontSize="xs"
color={darkGoldTheme.textSecondary}
isTruncated isTruncated
maxW="70%" maxLabelWidth="70%"
> />
{buyer.dept_name}
</Text>
<Text fontSize="xs" color={darkGoldTheme.red} fontWeight="bold">
{formatNumber(buyer.buy_amount)}
</Text>
</HStack>
)) ))
) : ( ) : (
<Text fontSize="xs" color={darkGoldTheme.textMuted}> <Text fontSize="xs" color={darkGoldTheme.textMuted}>
@@ -100,32 +70,14 @@ const UnusualPanel: React.FC<UnusualPanelProps> = ({ unusualData }) => {
<VStack spacing={1} align="stretch"> <VStack spacing={1} align="stretch">
{dayData.sellers && dayData.sellers.length > 0 ? ( {dayData.sellers && dayData.sellers.length > 0 ? (
dayData.sellers.slice(0, 5).map((seller, i) => ( dayData.sellers.slice(0, 5).map((seller, i) => (
<HStack <DataRow
key={i} key={i}
justify="space-between" variant="green"
p={2} label={seller.dept_name}
bg="rgba(0, 200, 81, 0.08)" value={formatNumber(seller.sell_amount)}
borderRadius="md"
border="1px solid"
borderColor="rgba(0, 200, 81, 0.15)"
transition="all 0.2s"
_hover={{
bg: 'rgba(0, 200, 81, 0.12)',
borderColor: 'rgba(0, 200, 81, 0.3)',
}}
>
<Text
fontSize="xs"
color={darkGoldTheme.textSecondary}
isTruncated isTruncated
maxW="70%" maxLabelWidth="70%"
> />
{seller.dept_name}
</Text>
<Text fontSize="xs" color={darkGoldTheme.green} fontWeight="bold">
{formatNumber(seller.sell_amount)}
</Text>
</HStack>
)) ))
) : ( ) : (
<Text fontSize="xs" color={darkGoldTheme.textMuted}> <Text fontSize="xs" color={darkGoldTheme.textMuted}>

View File

@@ -0,0 +1,84 @@
// src/views/Company/components/MarketDataView/components/shared/DataRow.tsx
// 通用数据行原子组件
import React, { memo } from 'react';
import { Box, Text, VStack, HStack } from '@chakra-ui/react';
import { darkGoldTheme } from '../../constants';
import {
financingRowStyle,
securitiesRowStyle,
buyRowStyle,
sellRowStyle,
} from './styles';
export type DataRowVariant = 'gold' | 'orange' | 'red' | 'green';
export interface DataRowProps {
/** 样式变体 */
variant: DataRowVariant;
/** 左侧标签 */
label: React.ReactNode;
/** 主要值 */
value: React.ReactNode;
/** 次要值(可选) */
subValue?: React.ReactNode;
/** 标签是否可截断 */
isTruncated?: boolean;
/** 标签最大宽度 */
maxLabelWidth?: string;
}
// 样式映射
const styleMap = {
gold: financingRowStyle,
orange: securitiesRowStyle,
red: buyRowStyle,
green: sellRowStyle,
};
// 颜色映射
const colorMap = {
gold: darkGoldTheme.gold,
orange: darkGoldTheme.orange,
red: darkGoldTheme.red,
green: darkGoldTheme.green,
};
/**
* 通用数据行组件
* 用于融资融券、龙虎榜等列表展示
*/
const DataRow: React.FC<DataRowProps> = ({
variant,
label,
value,
subValue,
isTruncated = false,
maxLabelWidth,
}) => {
return (
<HStack justify="space-between" sx={styleMap[variant]}>
<Text
color={darkGoldTheme.textMuted}
fontSize="sm"
isTruncated={isTruncated}
maxW={maxLabelWidth}
>
{label}
</Text>
<VStack align="end" spacing={0}>
<Text color={colorMap[variant]} fontWeight="bold">
{value}
</Text>
{subValue && (
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
{subValue}
</Text>
)}
</VStack>
</HStack>
);
};
export default memo(DataRow);

View File

@@ -4,5 +4,21 @@
export { default as DarkGoldCard } from './DarkGoldCard'; export { default as DarkGoldCard } from './DarkGoldCard';
export { default as DarkGoldBadge } from './DarkGoldBadge'; export { default as DarkGoldBadge } from './DarkGoldBadge';
export { default as EmptyState } from './EmptyState'; export { default as EmptyState } from './EmptyState';
export { darkGoldCardStyle, darkGoldCardHoverStyle } from './styles'; export { default as DataRow } from './DataRow';
export {
darkGoldCardStyle,
darkGoldCardHoverStyle,
darkGoldCardFullStyle,
dataRowStyle,
tableRowHoverStyle,
tableBorderStyle,
financingRowStyle,
securitiesRowStyle,
buyRowStyle,
sellRowStyle,
dayCardStyle,
} from './styles';
export type { DarkGoldBadgeVariant } from './DarkGoldBadge'; export type { DarkGoldBadgeVariant } from './DarkGoldBadge';
export type { DataRowVariant, DataRowProps } from './DataRow';

View File

@@ -57,3 +57,78 @@ export const tableBorderStyle: SystemStyleObject = {
borderBottom: '1px solid', borderBottom: '1px solid',
borderColor: 'rgba(212, 175, 55, 0.1)', borderColor: 'rgba(212, 175, 55, 0.1)',
}; };
/**
* 融资行样式 (金色主题)
*/
export const financingRowStyle: SystemStyleObject = {
p: 3,
bg: 'rgba(212, 175, 55, 0.08)',
borderRadius: 'md',
border: '1px solid',
borderColor: 'rgba(212, 175, 55, 0.15)',
transition: 'all 0.2s',
_hover: {
bg: 'rgba(212, 175, 55, 0.12)',
borderColor: 'rgba(212, 175, 55, 0.3)',
},
};
/**
* 融券行样式 (橙色主题)
*/
export const securitiesRowStyle: SystemStyleObject = {
p: 3,
bg: 'rgba(255, 149, 0, 0.08)',
borderRadius: 'md',
border: '1px solid',
borderColor: 'rgba(255, 149, 0, 0.15)',
transition: 'all 0.2s',
_hover: {
bg: 'rgba(255, 149, 0, 0.12)',
borderColor: 'rgba(255, 149, 0, 0.3)',
},
};
/**
* 买入行样式 (红色主题)
*/
export const buyRowStyle: SystemStyleObject = {
p: 2,
bg: 'rgba(255, 68, 68, 0.08)',
borderRadius: 'md',
border: '1px solid',
borderColor: 'rgba(255, 68, 68, 0.15)',
transition: 'all 0.2s',
_hover: {
bg: 'rgba(255, 68, 68, 0.12)',
borderColor: 'rgba(255, 68, 68, 0.3)',
},
};
/**
* 卖出行样式 (绿色主题)
*/
export const sellRowStyle: SystemStyleObject = {
p: 2,
bg: 'rgba(0, 200, 81, 0.08)',
borderRadius: 'md',
border: '1px solid',
borderColor: 'rgba(0, 200, 81, 0.15)',
transition: 'all 0.2s',
_hover: {
bg: 'rgba(0, 200, 81, 0.12)',
borderColor: 'rgba(0, 200, 81, 0.3)',
},
};
/**
* 日期数据卡片样式
*/
export const dayCardStyle: SystemStyleObject = {
p: 4,
bg: 'rgba(212, 175, 55, 0.05)',
borderRadius: 'lg',
border: '1px solid',
borderColor: 'rgba(212, 175, 55, 0.15)',
};