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')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算两个值的差异百分比
|
||||
* @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,
|
||||
getValueByPath,
|
||||
isNegativeIndicator,
|
||||
calculateDiff,
|
||||
} from './calculations';
|
||||
|
||||
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 - 股票对比弹窗组件
|
||||
* 展示对比明细、盈利能力对比、成长力对比
|
||||
*
|
||||
* 使用 Ant Design Modal + Table
|
||||
* 主题配置使用 Company/theme 统一配置
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
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 React, { useMemo } from 'react';
|
||||
import { Modal, Table, Spin, Row, Col, Card, Typography, Space, ConfigProvider } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
|
||||
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 type { StockInfo } from '../../FinancialPanorama/types';
|
||||
import {
|
||||
antdDarkTheme,
|
||||
modalStyles,
|
||||
cardStyle,
|
||||
cardStyles,
|
||||
chartCardStyles,
|
||||
FUI_COLORS,
|
||||
} from '../../../theme';
|
||||
import './StockCompareModal.less';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
// ============================================
|
||||
// 类型定义
|
||||
// ============================================
|
||||
interface StockCompareModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -48,6 +41,70 @@ interface StockCompareModalProps {
|
||||
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> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -57,185 +114,203 @@ const StockCompareModal: React.FC<StockCompareModalProps> = ({
|
||||
compareStockInfo,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
// 黑金主题颜色
|
||||
const bgColor = '#1A202C';
|
||||
const borderColor = '#C9A961';
|
||||
const goldColor = '#F4D03F';
|
||||
const positiveColor = '#EF4444'; // 红涨
|
||||
const negativeColor = '#10B981'; // 绿跌
|
||||
// 构建表格数据
|
||||
const tableData = useMemo<CompareTableRow[]>(() => {
|
||||
if (!currentStockInfo || !compareStockInfo) return [];
|
||||
|
||||
// 加载中或无数据时的显示
|
||||
if (isLoading || !currentStockInfo || !compareStockInfo) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="5xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
||||
<ModalHeader color={goldColor}>股票对比</ModalHeader>
|
||||
<ModalCloseButton color={borderColor} />
|
||||
<ModalBody pb={6}>
|
||||
<Center py={20}>
|
||||
{isLoading ? (
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="xl" color={goldColor} />
|
||||
<Text color={borderColor}>加载对比数据中...</Text>
|
||||
</VStack>
|
||||
) : (
|
||||
<Text color={borderColor}>暂无对比数据</Text>
|
||||
)}
|
||||
</Center>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
return COMPARE_METRICS.map((metric) => {
|
||||
const currentValue = getValueByPath<number>(currentStockInfo, metric.path);
|
||||
const compareValue = getValueByPath<number>(compareStockInfo, metric.path);
|
||||
const format = (metric.format || 'number') as 'percent' | 'number';
|
||||
|
||||
return {
|
||||
key: metric.key,
|
||||
metric: metric.label,
|
||||
currentValue,
|
||||
compareValue,
|
||||
diff: calculateDiff(currentValue, compareValue, format),
|
||||
format,
|
||||
};
|
||||
});
|
||||
}, [currentStockInfo, compareStockInfo]);
|
||||
|
||||
// 表格列定义
|
||||
const columns = useMemo<ColumnsType<CompareTableRow>>(() => [
|
||||
{
|
||||
title: '指标',
|
||||
dataIndex: 'metric',
|
||||
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 (
|
||||
<Space direction="vertical" size={20} style={{ width: '100%' }}>
|
||||
{/* 对比明细表格 */}
|
||||
<Card
|
||||
title={<CardTitle>对比明细</CardTitle>}
|
||||
variant="borderless"
|
||||
style={cardStyle}
|
||||
styles={{
|
||||
...cardStyles,
|
||||
body: { padding: '12px 0' },
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
showHeader
|
||||
rowClassName={() => 'compare-table-row'}
|
||||
style={{ background: 'transparent' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 对比图表 */}
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={<CardTitle>盈利能力对比</CardTitle>}
|
||||
variant="borderless"
|
||||
style={cardStyle}
|
||||
styles={chartCardStyles}
|
||||
>
|
||||
<ReactECharts
|
||||
option={profitabilityChartOption}
|
||||
style={{ height: 280 }}
|
||||
notMerge
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={<CardTitle>成长能力对比</CardTitle>}
|
||||
variant="borderless"
|
||||
style={cardStyle}
|
||||
styles={chartCardStyles}
|
||||
>
|
||||
<ReactECharts
|
||||
option={growthChartOption}
|
||||
style={{ height: 280 }}
|
||||
notMerge
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="5xl" scrollBehavior="inside">
|
||||
<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">
|
||||
<CardHeader pb={2}>
|
||||
<Heading size="sm" color={goldColor}>对比明细</Heading>
|
||||
</CardHeader>
|
||||
<CardBody pt={0}>
|
||||
<TableContainer>
|
||||
<Table size="sm" variant="unstyled">
|
||||
<Thead>
|
||||
<Tr borderBottom="1px solid" borderColor={borderColor}>
|
||||
<Th color={borderColor} fontSize="xs">指标</Th>
|
||||
<Th isNumeric color={borderColor} fontSize="xs">{currentStockInfo?.stock_name}</Th>
|
||||
<Th isNumeric color={borderColor} fontSize="xs">{compareStockInfo?.stock_name}</Th>
|
||||
<Th isNumeric color={borderColor} fontSize="xs">差异</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{COMPARE_METRICS.map((metric) => {
|
||||
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>
|
||||
|
||||
{/* 对比图表 */}
|
||||
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
|
||||
<GridItem>
|
||||
<Card bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
||||
<CardHeader pb={2}>
|
||||
<Heading size="sm" color={goldColor}>盈利能力对比</Heading>
|
||||
</CardHeader>
|
||||
<CardBody pt={0}>
|
||||
<ReactECharts
|
||||
option={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,
|
||||
]
|
||||
)}
|
||||
style={{ height: '280px' }}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<Card bg={bgColor} borderColor={borderColor} borderWidth="1px">
|
||||
<CardHeader pb={2}>
|
||||
<Heading size="sm" color={goldColor}>成长能力对比</Heading>
|
||||
</CardHeader>
|
||||
<CardBody pt={0}>
|
||||
<ReactECharts
|
||||
option={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,
|
||||
]
|
||||
)}
|
||||
style={{ height: '280px' }}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<ConfigProvider theme={antdDarkTheme}>
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onCancel={onClose}
|
||||
title={modalTitle}
|
||||
footer={null}
|
||||
width={1000}
|
||||
centered
|
||||
destroyOnHidden
|
||||
styles={modalStyles}
|
||||
>
|
||||
{renderContent()}
|
||||
</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 { FUI_COLORS, FUI_THEME } 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,
|
||||
} from './fui';
|
||||
|
||||
// Ant Design 主题配置
|
||||
export {
|
||||
antdDarkTheme,
|
||||
modalStyles,
|
||||
cardStyle,
|
||||
cardStyles,
|
||||
chartCardStyles,
|
||||
tableStyle,
|
||||
createAntdTheme,
|
||||
} from './antdTheme';
|
||||
|
||||
// 主题组件
|
||||
export * from './components';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user