refactor(StockCompareModal): 重构为 Ant Design 并统一主题配置
- 从 Chakra UI 迁移到 Ant Design (Modal, Table, Card) - 新增 antdTheme.ts 统一 Ant Design 深色主题配置 - 提取 calculateDiff 到 FinancialPanorama/utils 复用 - 使用 useMemo 优化性能,提取子组件 - 添加独立的 .less 样式文件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -97,3 +97,28 @@ export const isNegativeIndicator = (key: string): boolean => {
|
|||||||
key.includes('debt_ratio')
|
key.includes('debt_ratio')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算两个值的差异百分比
|
||||||
|
* @param value1 当前股票值
|
||||||
|
* @param value2 对比股票值
|
||||||
|
* @param format 格式类型:percent 直接相减,number 计算变化率
|
||||||
|
* @returns 差异百分比或 null
|
||||||
|
*/
|
||||||
|
export const calculateDiff = (
|
||||||
|
value1: number | null | undefined,
|
||||||
|
value2: number | null | undefined,
|
||||||
|
format: 'percent' | 'number'
|
||||||
|
): number | null => {
|
||||||
|
if (value1 == null || value2 == null) return null;
|
||||||
|
|
||||||
|
if (format === 'percent') {
|
||||||
|
return value1 - value2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value2 !== 0) {
|
||||||
|
return ((value1 - value2) / Math.abs(value2)) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export {
|
|||||||
getCellBackground,
|
getCellBackground,
|
||||||
getValueByPath,
|
getValueByPath,
|
||||||
isNegativeIndicator,
|
isNegativeIndicator,
|
||||||
|
calculateDiff,
|
||||||
} from './calculations';
|
} from './calculations';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* StockCompareModal 样式
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 禁用表格行 hover 效果
|
||||||
|
.compare-table-row {
|
||||||
|
> td {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr.compare-table-row:hover > td,
|
||||||
|
.ant-table-tbody > tr.compare-table-row:hover > td.ant-table-cell,
|
||||||
|
.ant-table-tbody > tr.compare-table-row > td.ant-table-cell-row-hover {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
@@ -1,43 +1,36 @@
|
|||||||
/**
|
/**
|
||||||
* StockCompareModal - 股票对比弹窗组件
|
* StockCompareModal - 股票对比弹窗组件
|
||||||
* 展示对比明细、盈利能力对比、成长力对比
|
* 展示对比明细、盈利能力对比、成长力对比
|
||||||
|
*
|
||||||
|
* 使用 Ant Design Modal + Table
|
||||||
|
* 主题配置使用 Company/theme 统一配置
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import { Modal, Table, Spin, Row, Col, Card, Typography, Space, ConfigProvider } from 'antd';
|
||||||
Modal,
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
ModalOverlay,
|
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Grid,
|
|
||||||
GridItem,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
Heading,
|
|
||||||
Text,
|
|
||||||
Table,
|
|
||||||
Thead,
|
|
||||||
Tbody,
|
|
||||||
Tr,
|
|
||||||
Th,
|
|
||||||
Td,
|
|
||||||
TableContainer,
|
|
||||||
Spinner,
|
|
||||||
Center,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { ArrowUp, ArrowDown } from 'lucide-react';
|
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
|
||||||
import { COMPARE_METRICS } from '../../FinancialPanorama/constants';
|
import { COMPARE_METRICS } from '../../FinancialPanorama/constants';
|
||||||
import { getValueByPath, getCompareBarChartOption } from '../../FinancialPanorama/utils';
|
import { getValueByPath, getCompareBarChartOption, calculateDiff } from '../../FinancialPanorama/utils';
|
||||||
import { formatUtils } from '@services/financialService';
|
import { formatUtils } from '@services/financialService';
|
||||||
import type { StockInfo } from '../../FinancialPanorama/types';
|
import type { StockInfo } from '../../FinancialPanorama/types';
|
||||||
|
import {
|
||||||
|
antdDarkTheme,
|
||||||
|
modalStyles,
|
||||||
|
cardStyle,
|
||||||
|
cardStyles,
|
||||||
|
chartCardStyles,
|
||||||
|
FUI_COLORS,
|
||||||
|
} from '../../../theme';
|
||||||
|
import './StockCompareModal.less';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 类型定义
|
||||||
|
// ============================================
|
||||||
interface StockCompareModalProps {
|
interface StockCompareModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -48,6 +41,70 @@ interface StockCompareModalProps {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CompareTableRow {
|
||||||
|
key: string;
|
||||||
|
metric: string;
|
||||||
|
currentValue: number | null | undefined;
|
||||||
|
compareValue: number | null | undefined;
|
||||||
|
diff: number | null;
|
||||||
|
format: 'percent' | 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 工具函数
|
||||||
|
// ============================================
|
||||||
|
const formatValue = (
|
||||||
|
value: number | null | undefined,
|
||||||
|
format: 'percent' | 'number'
|
||||||
|
): string => {
|
||||||
|
if (value == null) return '-';
|
||||||
|
return format === 'percent'
|
||||||
|
? formatUtils.formatPercent(value)
|
||||||
|
: formatUtils.formatLargeNumber(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 子组件
|
||||||
|
// ============================================
|
||||||
|
const DiffCell: React.FC<{ diff: number | null }> = ({ diff }) => {
|
||||||
|
if (diff === null) return <span style={{ color: FUI_COLORS.text.muted }}>-</span>;
|
||||||
|
|
||||||
|
const isPositive = diff > 0;
|
||||||
|
const color = isPositive ? FUI_COLORS.status.positive : FUI_COLORS.status.negative;
|
||||||
|
const Icon = isPositive ? ArrowUpOutlined : ArrowDownOutlined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space size={4} style={{ color, justifyContent: 'center' }}>
|
||||||
|
<Icon style={{ fontSize: 11 }} />
|
||||||
|
<span style={{ fontWeight: 500 }}>{Math.abs(diff).toFixed(2)}%</span>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CardTitle: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<Title level={5} style={{ margin: 0, color: FUI_COLORS.gold[400], fontSize: 14 }}>
|
||||||
|
{children}
|
||||||
|
</Title>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LoadingState: React.FC = () => (
|
||||||
|
<div style={{ textAlign: 'center', padding: '80px 0' }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<Text style={{ display: 'block', marginTop: 16, color: FUI_COLORS.text.muted }}>
|
||||||
|
加载对比数据中...
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EmptyState: React.FC = () => (
|
||||||
|
<div style={{ textAlign: 'center', padding: '80px 0' }}>
|
||||||
|
<Text style={{ color: FUI_COLORS.text.muted, fontSize: 14 }}>暂无对比数据</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 主组件
|
||||||
|
// ============================================
|
||||||
const StockCompareModal: React.FC<StockCompareModalProps> = ({
|
const StockCompareModal: React.FC<StockCompareModalProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -57,185 +114,203 @@ const StockCompareModal: React.FC<StockCompareModalProps> = ({
|
|||||||
compareStockInfo,
|
compareStockInfo,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}) => {
|
}) => {
|
||||||
// 黑金主题颜色
|
// 构建表格数据
|
||||||
const bgColor = '#1A202C';
|
const tableData = useMemo<CompareTableRow[]>(() => {
|
||||||
const borderColor = '#C9A961';
|
if (!currentStockInfo || !compareStockInfo) return [];
|
||||||
const goldColor = '#F4D03F';
|
|
||||||
const positiveColor = '#EF4444'; // 红涨
|
|
||||||
const negativeColor = '#10B981'; // 绿跌
|
|
||||||
|
|
||||||
// 加载中或无数据时的显示
|
return COMPARE_METRICS.map((metric) => {
|
||||||
if (isLoading || !currentStockInfo || !compareStockInfo) {
|
const currentValue = getValueByPath<number>(currentStockInfo, metric.path);
|
||||||
return (
|
const compareValue = getValueByPath<number>(compareStockInfo, metric.path);
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="5xl" scrollBehavior="inside">
|
const format = (metric.format || 'number') as 'percent' | 'number';
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
return {
|
||||||
<ModalHeader color={goldColor}>股票对比</ModalHeader>
|
key: metric.key,
|
||||||
<ModalCloseButton color={borderColor} />
|
metric: metric.label,
|
||||||
<ModalBody pb={6}>
|
currentValue,
|
||||||
<Center py={20}>
|
compareValue,
|
||||||
{isLoading ? (
|
diff: calculateDiff(currentValue, compareValue, format),
|
||||||
<VStack spacing={4}>
|
format,
|
||||||
<Spinner size="xl" color={goldColor} />
|
};
|
||||||
<Text color={borderColor}>加载对比数据中...</Text>
|
});
|
||||||
</VStack>
|
}, [currentStockInfo, compareStockInfo]);
|
||||||
) : (
|
|
||||||
<Text color={borderColor}>暂无对比数据</Text>
|
// 表格列定义
|
||||||
)}
|
const columns = useMemo<ColumnsType<CompareTableRow>>(() => [
|
||||||
</Center>
|
{
|
||||||
</ModalBody>
|
title: '指标',
|
||||||
</ModalContent>
|
dataIndex: 'metric',
|
||||||
</Modal>
|
key: 'metric',
|
||||||
|
align: 'center',
|
||||||
|
width: '25%',
|
||||||
|
render: (text) => <span style={{ color: FUI_COLORS.text.muted }}>{text}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: currentStockInfo?.stock_name || currentStock,
|
||||||
|
dataIndex: 'currentValue',
|
||||||
|
key: 'currentValue',
|
||||||
|
align: 'center',
|
||||||
|
width: '25%',
|
||||||
|
render: (value, record) => (
|
||||||
|
<span style={{ color: FUI_COLORS.gold[400], fontWeight: 500 }}>
|
||||||
|
{formatValue(value, record.format)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: compareStockInfo?.stock_name || compareStock,
|
||||||
|
dataIndex: 'compareValue',
|
||||||
|
key: 'compareValue',
|
||||||
|
align: 'center',
|
||||||
|
width: '25%',
|
||||||
|
render: (value, record) => (
|
||||||
|
<span style={{ color: FUI_COLORS.gold[400], fontWeight: 500 }}>
|
||||||
|
{formatValue(value, record.format)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '差异',
|
||||||
|
dataIndex: 'diff',
|
||||||
|
key: 'diff',
|
||||||
|
align: 'center',
|
||||||
|
width: '25%',
|
||||||
|
render: (diff: number | null) => <DiffCell diff={diff} />,
|
||||||
|
},
|
||||||
|
], [currentStock, compareStock, currentStockInfo?.stock_name, compareStockInfo?.stock_name]);
|
||||||
|
|
||||||
|
// 盈利能力图表配置
|
||||||
|
const profitabilityChartOption = useMemo(() => {
|
||||||
|
if (!currentStockInfo || !compareStockInfo) return {};
|
||||||
|
|
||||||
|
return getCompareBarChartOption(
|
||||||
|
'盈利能力对比',
|
||||||
|
currentStockInfo.stock_name || '',
|
||||||
|
compareStockInfo.stock_name || '',
|
||||||
|
['ROE', 'ROA', '毛利率', '净利率'],
|
||||||
|
[
|
||||||
|
currentStockInfo.key_metrics?.roe,
|
||||||
|
currentStockInfo.key_metrics?.roa,
|
||||||
|
currentStockInfo.key_metrics?.gross_margin,
|
||||||
|
currentStockInfo.key_metrics?.net_margin,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
compareStockInfo.key_metrics?.roe,
|
||||||
|
compareStockInfo.key_metrics?.roa,
|
||||||
|
compareStockInfo.key_metrics?.gross_margin,
|
||||||
|
compareStockInfo.key_metrics?.net_margin,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}, [currentStockInfo, compareStockInfo]);
|
||||||
|
|
||||||
|
// 成长能力图表配置
|
||||||
|
const growthChartOption = useMemo(() => {
|
||||||
|
if (!currentStockInfo || !compareStockInfo) return {};
|
||||||
|
|
||||||
|
return getCompareBarChartOption(
|
||||||
|
'成长能力对比',
|
||||||
|
currentStockInfo.stock_name || '',
|
||||||
|
compareStockInfo.stock_name || '',
|
||||||
|
['营收增长', '利润增长', '资产增长', '权益增长'],
|
||||||
|
[
|
||||||
|
currentStockInfo.growth_rates?.revenue_growth,
|
||||||
|
currentStockInfo.growth_rates?.profit_growth,
|
||||||
|
currentStockInfo.growth_rates?.asset_growth,
|
||||||
|
currentStockInfo.growth_rates?.equity_growth,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
compareStockInfo.growth_rates?.revenue_growth,
|
||||||
|
compareStockInfo.growth_rates?.profit_growth,
|
||||||
|
compareStockInfo.growth_rates?.asset_growth,
|
||||||
|
compareStockInfo.growth_rates?.equity_growth,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}, [currentStockInfo, compareStockInfo]);
|
||||||
|
|
||||||
|
// Modal 标题
|
||||||
|
const modalTitle = useMemo(() => {
|
||||||
|
if (!currentStockInfo || !compareStockInfo) return '股票对比';
|
||||||
|
return `${currentStockInfo.stock_name} (${currentStock}) vs ${compareStockInfo.stock_name} (${compareStock})`;
|
||||||
|
}, [currentStock, compareStock, currentStockInfo, compareStockInfo]);
|
||||||
|
|
||||||
|
// 渲染内容
|
||||||
|
const renderContent = () => {
|
||||||
|
if (isLoading) return <LoadingState />;
|
||||||
|
if (!currentStockInfo || !compareStockInfo) return <EmptyState />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="5xl" scrollBehavior="inside">
|
<Space direction="vertical" size={20} style={{ width: '100%' }}>
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
|
||||||
<ModalHeader color={goldColor}>
|
|
||||||
{currentStockInfo?.stock_name} ({currentStock}) vs {compareStockInfo?.stock_name} ({compareStock})
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton color={borderColor} />
|
|
||||||
<ModalBody pb={6}>
|
|
||||||
<VStack spacing={6} align="stretch">
|
|
||||||
{/* 对比明细表格 */}
|
{/* 对比明细表格 */}
|
||||||
<Card bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
<Card
|
||||||
<CardHeader pb={2}>
|
title={<CardTitle>对比明细</CardTitle>}
|
||||||
<Heading size="sm" color={goldColor}>对比明细</Heading>
|
variant="borderless"
|
||||||
</CardHeader>
|
style={cardStyle}
|
||||||
<CardBody pt={0}>
|
styles={{
|
||||||
<TableContainer>
|
...cardStyles,
|
||||||
<Table size="sm" variant="unstyled">
|
body: { padding: '12px 0' },
|
||||||
<Thead>
|
}}
|
||||||
<Tr borderBottom="1px solid" borderColor={borderColor}>
|
>
|
||||||
<Th color={borderColor} fontSize="xs">指标</Th>
|
<Table
|
||||||
<Th isNumeric color={borderColor} fontSize="xs">{currentStockInfo?.stock_name}</Th>
|
columns={columns}
|
||||||
<Th isNumeric color={borderColor} fontSize="xs">{compareStockInfo?.stock_name}</Th>
|
dataSource={tableData}
|
||||||
<Th isNumeric color={borderColor} fontSize="xs">差异</Th>
|
pagination={false}
|
||||||
</Tr>
|
size="middle"
|
||||||
</Thead>
|
showHeader
|
||||||
<Tbody>
|
rowClassName={() => 'compare-table-row'}
|
||||||
{COMPARE_METRICS.map((metric) => {
|
style={{ background: 'transparent' }}
|
||||||
const value1 = getValueByPath<number>(currentStockInfo, metric.path);
|
/>
|
||||||
const value2 = getValueByPath<number>(compareStockInfo, metric.path);
|
|
||||||
|
|
||||||
let diff: number | null = null;
|
|
||||||
let diffColor = borderColor;
|
|
||||||
|
|
||||||
if (value1 !== undefined && value2 !== undefined && value1 !== null && value2 !== null) {
|
|
||||||
if (metric.format === 'percent') {
|
|
||||||
diff = value1 - value2;
|
|
||||||
diffColor = diff > 0 ? positiveColor : negativeColor;
|
|
||||||
} else if (value2 !== 0) {
|
|
||||||
diff = ((value1 - value2) / Math.abs(value2)) * 100;
|
|
||||||
diffColor = diff > 0 ? positiveColor : negativeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr key={metric.key} borderBottom="1px solid" borderColor="whiteAlpha.100">
|
|
||||||
<Td color={borderColor} fontSize="sm">{metric.label}</Td>
|
|
||||||
<Td isNumeric color={goldColor} fontSize="sm">
|
|
||||||
{metric.format === 'percent'
|
|
||||||
? formatUtils.formatPercent(value1)
|
|
||||||
: formatUtils.formatLargeNumber(value1)}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric color={goldColor} fontSize="sm">
|
|
||||||
{metric.format === 'percent'
|
|
||||||
? formatUtils.formatPercent(value2)
|
|
||||||
: formatUtils.formatLargeNumber(value2)}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric color={diffColor} fontSize="sm">
|
|
||||||
{diff !== null ? (
|
|
||||||
<HStack spacing={1} justify="flex-end">
|
|
||||||
{diff > 0 && <ArrowUp size={12} />}
|
|
||||||
{diff < 0 && <ArrowDown size={12} />}
|
|
||||||
<Text>
|
|
||||||
{`${Math.abs(diff).toFixed(2)}%`}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 对比图表 */}
|
{/* 对比图表 */}
|
||||||
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
|
<Row gutter={16}>
|
||||||
<GridItem>
|
<Col span={12}>
|
||||||
<Card bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
<Card
|
||||||
<CardHeader pb={2}>
|
title={<CardTitle>盈利能力对比</CardTitle>}
|
||||||
<Heading size="sm" color={goldColor}>盈利能力对比</Heading>
|
variant="borderless"
|
||||||
</CardHeader>
|
style={cardStyle}
|
||||||
<CardBody pt={0}>
|
styles={chartCardStyles}
|
||||||
|
>
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
option={getCompareBarChartOption(
|
option={profitabilityChartOption}
|
||||||
'盈利能力对比',
|
style={{ height: 280 }}
|
||||||
currentStockInfo?.stock_name || '',
|
notMerge
|
||||||
compareStockInfo?.stock_name || '',
|
|
||||||
['ROE', 'ROA', '毛利率', '净利率'],
|
|
||||||
[
|
|
||||||
currentStockInfo?.key_metrics?.roe,
|
|
||||||
currentStockInfo?.key_metrics?.roa,
|
|
||||||
currentStockInfo?.key_metrics?.gross_margin,
|
|
||||||
currentStockInfo?.key_metrics?.net_margin,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
compareStockInfo?.key_metrics?.roe,
|
|
||||||
compareStockInfo?.key_metrics?.roa,
|
|
||||||
compareStockInfo?.key_metrics?.gross_margin,
|
|
||||||
compareStockInfo?.key_metrics?.net_margin,
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
style={{ height: '280px' }}
|
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
|
||||||
</Card>
|
</Card>
|
||||||
</GridItem>
|
</Col>
|
||||||
|
|
||||||
<GridItem>
|
<Col span={12}>
|
||||||
<Card bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
<Card
|
||||||
<CardHeader pb={2}>
|
title={<CardTitle>成长能力对比</CardTitle>}
|
||||||
<Heading size="sm" color={goldColor}>成长能力对比</Heading>
|
variant="borderless"
|
||||||
</CardHeader>
|
style={cardStyle}
|
||||||
<CardBody pt={0}>
|
styles={chartCardStyles}
|
||||||
|
>
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
option={getCompareBarChartOption(
|
option={growthChartOption}
|
||||||
'成长能力对比',
|
style={{ height: 280 }}
|
||||||
currentStockInfo?.stock_name || '',
|
notMerge
|
||||||
compareStockInfo?.stock_name || '',
|
|
||||||
['营收增长', '利润增长', '资产增长', '股东权益增长'],
|
|
||||||
[
|
|
||||||
currentStockInfo?.growth_rates?.revenue_growth,
|
|
||||||
currentStockInfo?.growth_rates?.profit_growth,
|
|
||||||
currentStockInfo?.growth_rates?.asset_growth,
|
|
||||||
currentStockInfo?.growth_rates?.equity_growth,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
compareStockInfo?.growth_rates?.revenue_growth,
|
|
||||||
compareStockInfo?.growth_rates?.profit_growth,
|
|
||||||
compareStockInfo?.growth_rates?.asset_growth,
|
|
||||||
compareStockInfo?.growth_rates?.equity_growth,
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
style={{ height: '280px' }}
|
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
|
||||||
</Card>
|
</Card>
|
||||||
</GridItem>
|
</Col>
|
||||||
</Grid>
|
</Row>
|
||||||
</VStack>
|
</Space>
|
||||||
</ModalBody>
|
);
|
||||||
</ModalContent>
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider theme={antdDarkTheme}>
|
||||||
|
<Modal
|
||||||
|
open={isOpen}
|
||||||
|
onCancel={onClose}
|
||||||
|
title={modalTitle}
|
||||||
|
footer={null}
|
||||||
|
width={1000}
|
||||||
|
centered
|
||||||
|
destroyOnHidden
|
||||||
|
styles={modalStyles}
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
270
src/views/Company/theme/antdTheme.ts
Normal file
270
src/views/Company/theme/antdTheme.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* Company 页面 Ant Design 主题配置
|
||||||
|
*
|
||||||
|
* 与 FUI 主题系统保持一致的 Ant Design ConfigProvider 主题配置
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* import { antdDarkTheme, modalStyles, cardStyle } from '@views/Company/theme/antdTheme';
|
||||||
|
*
|
||||||
|
* <ConfigProvider theme={antdDarkTheme}>
|
||||||
|
* <Modal styles={modalStyles}>...</Modal>
|
||||||
|
* </ConfigProvider>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { theme } from 'antd';
|
||||||
|
import { FUI_COLORS, FUI_GLOW, FUI_GLASS } from './fui';
|
||||||
|
import { alpha, fui } from './utils';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Ant Design 深空主题 Token
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const antdDarkTheme = {
|
||||||
|
algorithm: theme.darkAlgorithm,
|
||||||
|
token: {
|
||||||
|
// 主色调
|
||||||
|
colorPrimary: FUI_COLORS.gold[400],
|
||||||
|
colorPrimaryHover: FUI_COLORS.gold[300],
|
||||||
|
colorPrimaryActive: FUI_COLORS.gold[500],
|
||||||
|
|
||||||
|
// 背景色
|
||||||
|
colorBgBase: FUI_COLORS.bg.deep,
|
||||||
|
colorBgContainer: FUI_COLORS.bg.elevated,
|
||||||
|
colorBgElevated: FUI_COLORS.bg.surface,
|
||||||
|
colorBgLayout: FUI_COLORS.bg.primary,
|
||||||
|
colorBgMask: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
|
||||||
|
// 边框
|
||||||
|
colorBorder: fui.border('default'),
|
||||||
|
colorBorderSecondary: fui.border('subtle'),
|
||||||
|
|
||||||
|
// 文字
|
||||||
|
colorText: FUI_COLORS.text.primary,
|
||||||
|
colorTextSecondary: FUI_COLORS.text.secondary,
|
||||||
|
colorTextTertiary: FUI_COLORS.text.muted,
|
||||||
|
colorTextQuaternary: FUI_COLORS.text.dim,
|
||||||
|
colorTextHeading: FUI_COLORS.gold[400],
|
||||||
|
|
||||||
|
// 链接
|
||||||
|
colorLink: FUI_COLORS.gold[400],
|
||||||
|
colorLinkHover: FUI_COLORS.gold[300],
|
||||||
|
colorLinkActive: FUI_COLORS.gold[500],
|
||||||
|
|
||||||
|
// 成功/错误状态(涨跌色)
|
||||||
|
colorSuccess: FUI_COLORS.status.negative, // 绿色
|
||||||
|
colorError: FUI_COLORS.status.positive, // 红色
|
||||||
|
colorWarning: FUI_COLORS.status.warning,
|
||||||
|
colorInfo: FUI_COLORS.status.info,
|
||||||
|
|
||||||
|
// 圆角
|
||||||
|
borderRadius: 8,
|
||||||
|
borderRadiusLG: 12,
|
||||||
|
borderRadiusSM: 6,
|
||||||
|
borderRadiusXS: 4,
|
||||||
|
|
||||||
|
// 字体
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: 14,
|
||||||
|
|
||||||
|
// 间距
|
||||||
|
padding: 16,
|
||||||
|
paddingLG: 24,
|
||||||
|
paddingSM: 12,
|
||||||
|
paddingXS: 8,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
// Modal 组件
|
||||||
|
Modal: {
|
||||||
|
headerBg: FUI_COLORS.bg.deep,
|
||||||
|
contentBg: FUI_COLORS.bg.deep,
|
||||||
|
footerBg: FUI_COLORS.bg.deep,
|
||||||
|
titleColor: FUI_COLORS.gold[400],
|
||||||
|
titleFontSize: 18,
|
||||||
|
colorIcon: FUI_COLORS.text.muted,
|
||||||
|
colorIconHover: FUI_COLORS.gold[400],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Table 组件
|
||||||
|
Table: {
|
||||||
|
headerBg: alpha('gold', 0.05),
|
||||||
|
headerColor: FUI_COLORS.text.muted,
|
||||||
|
headerSplitColor: fui.border('subtle'),
|
||||||
|
rowHoverBg: alpha('gold', 0.1),
|
||||||
|
rowSelectedBg: alpha('gold', 0.15),
|
||||||
|
rowSelectedHoverBg: alpha('gold', 0.18),
|
||||||
|
borderColor: fui.border('subtle'),
|
||||||
|
cellFontSize: 13,
|
||||||
|
cellPaddingBlock: 14,
|
||||||
|
cellPaddingInline: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Card 组件
|
||||||
|
Card: {
|
||||||
|
headerBg: 'transparent',
|
||||||
|
colorBgContainer: FUI_COLORS.bg.elevated,
|
||||||
|
colorBorderSecondary: fui.border('default'),
|
||||||
|
paddingLG: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Button 组件
|
||||||
|
Button: {
|
||||||
|
primaryColor: FUI_COLORS.bg.deep,
|
||||||
|
colorPrimaryHover: FUI_COLORS.gold[300],
|
||||||
|
colorPrimaryActive: FUI_COLORS.gold[500],
|
||||||
|
defaultBg: 'transparent',
|
||||||
|
defaultBorderColor: fui.border('default'),
|
||||||
|
defaultColor: FUI_COLORS.text.secondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Input 组件
|
||||||
|
Input: {
|
||||||
|
colorBgContainer: FUI_COLORS.bg.primary,
|
||||||
|
colorBorder: fui.border('default'),
|
||||||
|
hoverBorderColor: fui.border('hover'),
|
||||||
|
activeBorderColor: FUI_COLORS.gold[400],
|
||||||
|
activeShadow: FUI_GLOW.gold.sm,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Select 组件
|
||||||
|
Select: {
|
||||||
|
colorBgContainer: FUI_COLORS.bg.primary,
|
||||||
|
colorBorder: fui.border('default'),
|
||||||
|
optionSelectedBg: alpha('gold', 0.15),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Spin 组件
|
||||||
|
Spin: {
|
||||||
|
colorPrimary: FUI_COLORS.gold[400],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tabs 组件
|
||||||
|
Tabs: {
|
||||||
|
inkBarColor: FUI_COLORS.gold[400],
|
||||||
|
itemActiveColor: FUI_COLORS.gold[400],
|
||||||
|
itemHoverColor: FUI_COLORS.gold[300],
|
||||||
|
itemSelectedColor: FUI_COLORS.gold[400],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tag 组件
|
||||||
|
Tag: {
|
||||||
|
defaultBg: alpha('gold', 0.1),
|
||||||
|
defaultColor: FUI_COLORS.gold[400],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tooltip 组件
|
||||||
|
Tooltip: {
|
||||||
|
colorBgSpotlight: FUI_COLORS.bg.surface,
|
||||||
|
colorTextLightSolid: FUI_COLORS.text.primary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Divider 组件
|
||||||
|
Divider: {
|
||||||
|
colorSplit: fui.border('subtle'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 组件样式预设
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal 样式配置
|
||||||
|
* 用于 Modal 组件的 styles 属性
|
||||||
|
*/
|
||||||
|
export const modalStyles = {
|
||||||
|
mask: {
|
||||||
|
backdropFilter: FUI_GLASS.blur.md,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
background: FUI_COLORS.bg.deep,
|
||||||
|
border: fui.glassBorder('default'),
|
||||||
|
borderRadius: 16,
|
||||||
|
boxShadow: FUI_GLOW.gold.md,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
background: 'transparent',
|
||||||
|
borderBottom: fui.glassBorder('subtle'),
|
||||||
|
padding: '16px 24px',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: 24,
|
||||||
|
maxHeight: 'calc(100vh - 200px)',
|
||||||
|
overflowY: 'auto' as const,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
background: 'transparent',
|
||||||
|
borderTop: fui.glassBorder('subtle'),
|
||||||
|
padding: '12px 24px',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玻璃卡片样式
|
||||||
|
* 用于 Card 组件的 style 属性
|
||||||
|
*/
|
||||||
|
export const cardStyle = {
|
||||||
|
background: FUI_COLORS.bg.elevated,
|
||||||
|
border: fui.glassBorder('default'),
|
||||||
|
borderRadius: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card 内部样式配置
|
||||||
|
* 用于 Card 组件的 styles 属性
|
||||||
|
*/
|
||||||
|
export const cardStyles = {
|
||||||
|
header: {
|
||||||
|
borderBottom: fui.glassBorder('subtle'),
|
||||||
|
padding: '12px 16px',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图表卡片样式配置
|
||||||
|
*/
|
||||||
|
export const chartCardStyles = {
|
||||||
|
header: {
|
||||||
|
borderBottom: fui.glassBorder('subtle'),
|
||||||
|
padding: '12px 16px',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格样式
|
||||||
|
* 用于 Table 组件的 style 属性
|
||||||
|
*/
|
||||||
|
export const tableStyle = {
|
||||||
|
background: 'transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 工具函数
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建自定义 Ant Design 主题
|
||||||
|
* 可在基础主题上覆盖特定配置
|
||||||
|
*/
|
||||||
|
export function createAntdTheme(overrides?: Partial<typeof antdDarkTheme>) {
|
||||||
|
return {
|
||||||
|
...antdDarkTheme,
|
||||||
|
...overrides,
|
||||||
|
token: {
|
||||||
|
...antdDarkTheme.token,
|
||||||
|
...overrides?.token,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
...antdDarkTheme.components,
|
||||||
|
...overrides?.components,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default antdDarkTheme;
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
* import { COLORS, GLOW, GLASS } from '@views/Company/theme';
|
* import { COLORS, GLOW, GLASS } from '@views/Company/theme';
|
||||||
* import { FUI_COLORS, FUI_THEME } from '@views/Company/theme';
|
* import { FUI_COLORS, FUI_THEME } from '@views/Company/theme';
|
||||||
* import { alpha, fui, chartTheme } from '@views/Company/theme';
|
* import { alpha, fui, chartTheme } from '@views/Company/theme';
|
||||||
|
* import { antdDarkTheme, modalStyles, cardStyle } from '@views/Company/theme';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 完整主题对象
|
// 完整主题对象
|
||||||
@@ -18,6 +19,17 @@ export {
|
|||||||
FUI_STYLES,
|
FUI_STYLES,
|
||||||
} from './fui';
|
} from './fui';
|
||||||
|
|
||||||
|
// Ant Design 主题配置
|
||||||
|
export {
|
||||||
|
antdDarkTheme,
|
||||||
|
modalStyles,
|
||||||
|
cardStyle,
|
||||||
|
cardStyles,
|
||||||
|
chartCardStyles,
|
||||||
|
tableStyle,
|
||||||
|
createAntdTheme,
|
||||||
|
} from './antdTheme';
|
||||||
|
|
||||||
// 主题组件
|
// 主题组件
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user