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

View File

@@ -1,21 +1,14 @@
// src/views/Company/components/MarketDataView/components/panels/FundingPanel.tsx
// 融资融券面板 - 黑金主题
import React, { memo } from 'react';
import {
Box,
Text,
VStack,
HStack,
Grid,
Heading,
} from '@chakra-ui/react';
import React, { memo, useMemo } from 'react';
import { Box, VStack, Grid } from '@chakra-ui/react';
import ECharts from '@components/Charts/ECharts';
import { formatNumber } from '../../utils/formatUtils';
import { getFundingDarkGoldOption } from '../../utils/chartOptions';
import { darkGoldTheme } from '../../constants';
import { DarkGoldCard } from '../shared';
import { DarkGoldCard, DataRow } from '../shared';
import { darkGoldCardFullStyle } from '../shared/styles';
import type { FundingDayData } from '../../types';
@@ -24,6 +17,17 @@ export interface FundingPanelProps {
}
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 (
<VStack spacing={6} align="stretch">
{/* 图表卡片 */}
@@ -31,7 +35,7 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
{fundingData.length > 0 && (
<Box h="400px">
<ECharts
option={getFundingDarkGoldOption(fundingData)}
option={chartOption}
style={{ height: '100%', width: '100%' }}
theme="dark"
/>
@@ -43,78 +47,30 @@ const FundingPanel: React.FC<FundingPanelProps> = ({ fundingData }) => {
{/* 融资数据 */}
<DarkGoldCard title="融资数据" titleColor={darkGoldTheme.gold}>
<VStack spacing={3} align="stretch">
{fundingData
.slice(-5)
.reverse()
.map((item, idx) => (
<Box
key={idx}
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)',
}}
>
<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>
))}
{recentData.map((item, idx) => (
<DataRow
key={idx}
variant="gold"
label={item.date}
value={formatNumber(item.financing.balance)}
subValue={`买入${formatNumber(item.financing.buy)} / 偿还${formatNumber(item.financing.repay)}`}
/>
))}
</VStack>
</DarkGoldCard>
{/* 融券数据 */}
<DarkGoldCard title="融券数据" titleColor={darkGoldTheme.orange}>
<VStack spacing={3} align="stretch">
{fundingData
.slice(-5)
.reverse()
.map((item, idx) => (
<Box
key={idx}
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)',
}}
>
<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>
))}
{recentData.map((item, idx) => (
<DataRow
key={idx}
variant="orange"
label={item.date}
value={formatNumber(item.securities.balance)}
subValue={`卖出${formatNumber(item.securities.sell)} / 偿还${formatNumber(item.securities.repay)}`}
/>
))}
</VStack>
</DarkGoldCard>
</Grid>

View File

@@ -2,17 +2,12 @@
// 龙虎榜面板 - 黑金主题
import React, { memo } from 'react';
import {
Box,
Text,
VStack,
HStack,
Grid,
} from '@chakra-ui/react';
import { Box, Text, VStack, HStack, Grid } from '@chakra-ui/react';
import { formatNumber } from '../../utils/formatUtils';
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';
export interface UnusualPanelProps {
@@ -23,144 +18,101 @@ const UnusualPanel: React.FC<UnusualPanelProps> = ({ unusualData }) => {
return (
<DarkGoldCard title="龙虎榜数据">
{unusualData?.grouped_data && unusualData.grouped_data.length > 0 ? (
<VStack spacing={4} align="stretch">
{unusualData.grouped_data.map((dayData, idx) => (
<Box
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}>
<Text fontSize="md" fontWeight="bold" color={darkGoldTheme.gold}>
{dayData.date}
</Text>
<HStack spacing={2} flexWrap="wrap">
<DarkGoldBadge variant="red">
: {formatNumber(dayData.total_buy)}
</DarkGoldBadge>
<DarkGoldBadge variant="green">
: {formatNumber(dayData.total_sell)}
</DarkGoldBadge>
<DarkGoldBadge variant={dayData.net_amount > 0 ? 'red' : 'green'}>
: {formatNumber(dayData.net_amount)}
</DarkGoldBadge>
</HStack>
<VStack spacing={4} align="stretch">
{unusualData.grouped_data.map((dayData, idx) => (
<Box key={idx} sx={dayCardStyle}>
<HStack justify="space-between" mb={4} flexWrap="wrap" gap={2}>
<Text fontSize="md" fontWeight="bold" color={darkGoldTheme.gold}>
{dayData.date}
</Text>
<HStack spacing={2} flexWrap="wrap">
<DarkGoldBadge variant="red">
: {formatNumber(dayData.total_buy)}
</DarkGoldBadge>
<DarkGoldBadge variant="green">
: {formatNumber(dayData.total_sell)}
</DarkGoldBadge>
<DarkGoldBadge variant={dayData.net_amount > 0 ? 'red' : 'green'}>
: {formatNumber(dayData.net_amount)}
</DarkGoldBadge>
</HStack>
</HStack>
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
<Box>
<Text fontWeight="bold" color={darkGoldTheme.red} mb={2} fontSize="sm">
</Text>
<VStack spacing={1} align="stretch">
{dayData.buyers && dayData.buyers.length > 0 ? (
dayData.buyers.slice(0, 5).map((buyer, i) => (
<HStack
key={i}
justify="space-between"
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)',
}}
>
<Text
fontSize="xs"
color={darkGoldTheme.textSecondary}
isTruncated
maxW="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>
)}
</VStack>
</Box>
<Box>
<Text fontWeight="bold" color={darkGoldTheme.green} mb={2} fontSize="sm">
</Text>
<VStack spacing={1} align="stretch">
{dayData.sellers && dayData.sellers.length > 0 ? (
dayData.sellers.slice(0, 5).map((seller, i) => (
<HStack
key={i}
justify="space-between"
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)',
}}
>
<Text
fontSize="xs"
color={darkGoldTheme.textSecondary}
isTruncated
maxW="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>
)}
</VStack>
</Box>
</Grid>
{/* 信息类型标签 */}
<HStack mt={3} spacing={2} flexWrap="wrap">
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
:
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
<Box>
<Text fontWeight="bold" color={darkGoldTheme.red} mb={2} fontSize="sm">
</Text>
{dayData.info_types?.map((type, i) => (
<Box
key={i}
px={2}
py={0.5}
bg="rgba(212, 175, 55, 0.1)"
color={darkGoldTheme.gold}
borderRadius="sm"
fontSize="xs"
>
{type}
</Box>
))}
</HStack>
</Box>
))}
</VStack>
) : (
<EmptyState message="暂无龙虎榜数据" />
)}
<VStack spacing={1} align="stretch">
{dayData.buyers && dayData.buyers.length > 0 ? (
dayData.buyers.slice(0, 5).map((buyer, i) => (
<DataRow
key={i}
variant="red"
label={buyer.dept_name}
value={formatNumber(buyer.buy_amount)}
isTruncated
maxLabelWidth="70%"
/>
))
) : (
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
</Text>
)}
</VStack>
</Box>
<Box>
<Text fontWeight="bold" color={darkGoldTheme.green} mb={2} fontSize="sm">
</Text>
<VStack spacing={1} align="stretch">
{dayData.sellers && dayData.sellers.length > 0 ? (
dayData.sellers.slice(0, 5).map((seller, i) => (
<DataRow
key={i}
variant="green"
label={seller.dept_name}
value={formatNumber(seller.sell_amount)}
isTruncated
maxLabelWidth="70%"
/>
))
) : (
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
</Text>
)}
</VStack>
</Box>
</Grid>
{/* 信息类型标签 */}
<HStack mt={3} spacing={2} flexWrap="wrap">
<Text fontSize="xs" color={darkGoldTheme.textMuted}>
:
</Text>
{dayData.info_types?.map((type, i) => (
<Box
key={i}
px={2}
py={0.5}
bg="rgba(212, 175, 55, 0.1)"
color={darkGoldTheme.gold}
borderRadius="sm"
fontSize="xs"
>
{type}
</Box>
))}
</HStack>
</Box>
))}
</VStack>
) : (
<EmptyState message="暂无龙虎榜数据" />
)}
</DarkGoldCard>
);
};

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 DarkGoldBadge } from './DarkGoldBadge';
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 { DataRowVariant, DataRowProps } from './DataRow';

View File

@@ -57,3 +57,78 @@ export const tableBorderStyle: SystemStyleObject = {
borderBottom: '1px solid',
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)',
};