diff --git a/src/views/Company/components/FinancialPanorama/utils/calculations.ts b/src/views/Company/components/FinancialPanorama/utils/calculations.ts
index b517f466..72e71aec 100644
--- a/src/views/Company/components/FinancialPanorama/utils/calculations.ts
+++ b/src/views/Company/components/FinancialPanorama/utils/calculations.ts
@@ -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;
+};
diff --git a/src/views/Company/components/FinancialPanorama/utils/index.ts b/src/views/Company/components/FinancialPanorama/utils/index.ts
index 1c8fdc35..45aabc03 100644
--- a/src/views/Company/components/FinancialPanorama/utils/index.ts
+++ b/src/views/Company/components/FinancialPanorama/utils/index.ts
@@ -7,6 +7,7 @@ export {
getCellBackground,
getValueByPath,
isNegativeIndicator,
+ calculateDiff,
} from './calculations';
export {
diff --git a/src/views/Company/components/StockQuoteCard/components/StockCompareModal.less b/src/views/Company/components/StockQuoteCard/components/StockCompareModal.less
new file mode 100644
index 00000000..c43a0315
--- /dev/null
+++ b/src/views/Company/components/StockQuoteCard/components/StockCompareModal.less
@@ -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;
+}
diff --git a/src/views/Company/components/StockQuoteCard/components/StockCompareModal.tsx b/src/views/Company/components/StockQuoteCard/components/StockCompareModal.tsx
index 035ec56d..f9dde576 100644
--- a/src/views/Company/components/StockQuoteCard/components/StockCompareModal.tsx
+++ b/src/views/Company/components/StockQuoteCard/components/StockCompareModal.tsx
@@ -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 -;
+
+ const isPositive = diff > 0;
+ const color = isPositive ? FUI_COLORS.status.positive : FUI_COLORS.status.negative;
+ const Icon = isPositive ? ArrowUpOutlined : ArrowDownOutlined;
+
+ return (
+
+
+ {Math.abs(diff).toFixed(2)}%
+
+ );
+};
+
+const CardTitle: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+ {children}
+
+);
+
+const LoadingState: React.FC = () => (
+
+
+
+ 加载对比数据中...
+
+
+);
+
+const EmptyState: React.FC = () => (
+
+ 暂无对比数据
+
+);
+
+// ============================================
+// 主组件
+// ============================================
const StockCompareModal: React.FC = ({
isOpen,
onClose,
@@ -57,185 +114,203 @@ const StockCompareModal: React.FC = ({
compareStockInfo,
isLoading = false,
}) => {
- // 黑金主题颜色
- const bgColor = '#1A202C';
- const borderColor = '#C9A961';
- const goldColor = '#F4D03F';
- const positiveColor = '#EF4444'; // 红涨
- const negativeColor = '#10B981'; // 绿跌
+ // 构建表格数据
+ const tableData = useMemo(() => {
+ if (!currentStockInfo || !compareStockInfo) return [];
- // 加载中或无数据时的显示
- if (isLoading || !currentStockInfo || !compareStockInfo) {
- return (
-
-
-
- 股票对比
-
-
-
- {isLoading ? (
-
-
- 加载对比数据中...
-
- ) : (
- 暂无对比数据
- )}
-
-
-
-
+ return COMPARE_METRICS.map((metric) => {
+ const currentValue = getValueByPath(currentStockInfo, metric.path);
+ const compareValue = getValueByPath(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>(() => [
+ {
+ title: '指标',
+ dataIndex: 'metric',
+ key: 'metric',
+ align: 'center',
+ width: '25%',
+ render: (text) => {text},
+ },
+ {
+ title: currentStockInfo?.stock_name || currentStock,
+ dataIndex: 'currentValue',
+ key: 'currentValue',
+ align: 'center',
+ width: '25%',
+ render: (value, record) => (
+
+ {formatValue(value, record.format)}
+
+ ),
+ },
+ {
+ title: compareStockInfo?.stock_name || compareStock,
+ dataIndex: 'compareValue',
+ key: 'compareValue',
+ align: 'center',
+ width: '25%',
+ render: (value, record) => (
+
+ {formatValue(value, record.format)}
+
+ ),
+ },
+ {
+ title: '差异',
+ dataIndex: 'diff',
+ key: 'diff',
+ align: 'center',
+ width: '25%',
+ render: (diff: number | null) => ,
+ },
+ ], [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 ;
+ if (!currentStockInfo || !compareStockInfo) return ;
+
+ return (
+
+ {/* 对比明细表格 */}
+ 对比明细}
+ variant="borderless"
+ style={cardStyle}
+ styles={{
+ ...cardStyles,
+ body: { padding: '12px 0' },
+ }}
+ >
+ 'compare-table-row'}
+ style={{ background: 'transparent' }}
+ />
+
+
+ {/* 对比图表 */}
+
+
+ 盈利能力对比}
+ variant="borderless"
+ style={cardStyle}
+ styles={chartCardStyles}
+ >
+
+
+
+
+
+ 成长能力对比}
+ variant="borderless"
+ style={cardStyle}
+ styles={chartCardStyles}
+ >
+
+
+
+
+
+ );
+ };
return (
-
-
-
-
- {currentStockInfo?.stock_name} ({currentStock}) vs {compareStockInfo?.stock_name} ({compareStock})
-
-
-
-
- {/* 对比明细表格 */}
-
-
- 对比明细
-
-
-
-
-
-
- | 指标 |
- {currentStockInfo?.stock_name} |
- {compareStockInfo?.stock_name} |
- 差异 |
-
-
-
- {COMPARE_METRICS.map((metric) => {
- const value1 = getValueByPath(currentStockInfo, metric.path);
- const value2 = getValueByPath(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 (
-
- | {metric.label} |
-
- {metric.format === 'percent'
- ? formatUtils.formatPercent(value1)
- : formatUtils.formatLargeNumber(value1)}
- |
-
- {metric.format === 'percent'
- ? formatUtils.formatPercent(value2)
- : formatUtils.formatLargeNumber(value2)}
- |
-
- {diff !== null ? (
-
- {diff > 0 && }
- {diff < 0 && }
-
- {`${Math.abs(diff).toFixed(2)}%`}
-
-
- ) : (
- '-'
- )}
- |
-
- );
- })}
-
-
-
-
-
-
- {/* 对比图表 */}
-
-
-
-
- 盈利能力对比
-
-
-
-
-
-
-
-
-
-
- 成长能力对比
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {renderContent()}
+
+
);
};
diff --git a/src/views/Company/theme/antdTheme.ts b/src/views/Company/theme/antdTheme.ts
new file mode 100644
index 00000000..2e9aa2f0
--- /dev/null
+++ b/src/views/Company/theme/antdTheme.ts
@@ -0,0 +1,270 @@
+/**
+ * Company 页面 Ant Design 主题配置
+ *
+ * 与 FUI 主题系统保持一致的 Ant Design ConfigProvider 主题配置
+ *
+ * @example
+ * import { antdDarkTheme, modalStyles, cardStyle } from '@views/Company/theme/antdTheme';
+ *
+ *
+ * ...
+ *
+ */
+
+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) {
+ return {
+ ...antdDarkTheme,
+ ...overrides,
+ token: {
+ ...antdDarkTheme.token,
+ ...overrides?.token,
+ },
+ components: {
+ ...antdDarkTheme.components,
+ ...overrides?.components,
+ },
+ };
+}
+
+export default antdDarkTheme;
diff --git a/src/views/Company/theme/index.ts b/src/views/Company/theme/index.ts
index 64f80a58..7648cce0 100644
--- a/src/views/Company/theme/index.ts
+++ b/src/views/Company/theme/index.ts
@@ -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';