From c1394dbf1997e60ff8e6348c31a5c5c19ed984e4 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 26 Dec 2025 13:59:44 +0800 Subject: [PATCH] =?UTF-8?q?refactor(StockCompareModal):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=B8=BA=20Ant=20Design=20=E5=B9=B6=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从 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 --- .../FinancialPanorama/utils/calculations.ts | 25 + .../FinancialPanorama/utils/index.ts | 1 + .../components/StockCompareModal.less | 16 + .../components/StockCompareModal.tsx | 483 ++++++++++-------- src/views/Company/theme/antdTheme.ts | 270 ++++++++++ src/views/Company/theme/index.ts | 12 + 6 files changed, 603 insertions(+), 204 deletions(-) create mode 100644 src/views/Company/components/StockQuoteCard/components/StockCompareModal.less create mode 100644 src/views/Company/theme/antdTheme.ts 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}) - - - - - {/* 对比明细表格 */} - - - 对比明细 - - - -
- - - - - - - - - - {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 ( - - - - - - - ); - })} - -
指标{currentStockInfo?.stock_name}{compareStockInfo?.stock_name}差异
{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';