diff --git a/.eslintrc.js b/.eslintrc.js index f92631b5..7a1b8166 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -79,6 +79,16 @@ module.exports = { 'no-unused-vars': 'off', // 使用 TS 版本的规则 }, }, + // Company 视图主题硬编码检测 + { + files: ['src/views/Company/**/*.{ts,tsx,js,jsx}'], + excludedFiles: ['**/theme/**', '**/*.test.*', '**/*.spec.*'], + plugins: ['local-rules'], + rules: { + // warning 级别:提醒开发者但不阻塞构建 + 'local-rules/no-hardcoded-fui-colors': 'warn', + }, + }, ], /* 忽略文件(与 .eslintignore 等效)*/ diff --git a/eslint-rules/index.js b/eslint-rules/index.js new file mode 100644 index 00000000..06e9a1b8 --- /dev/null +++ b/eslint-rules/index.js @@ -0,0 +1,10 @@ +/** + * 本地 ESLint 规则插件 + * + * 在 .eslintrc.js 中通过 eslint-plugin-local-rules 使用 + */ +module.exports = { + rules: { + 'no-hardcoded-fui-colors': require('./no-hardcoded-fui-colors'), + }, +}; diff --git a/eslint-rules/no-hardcoded-fui-colors.js b/eslint-rules/no-hardcoded-fui-colors.js new file mode 100644 index 00000000..d6fd4877 --- /dev/null +++ b/eslint-rules/no-hardcoded-fui-colors.js @@ -0,0 +1,86 @@ +/** + * ESLint 规则:禁止在 Company 目录使用硬编码颜色 + * + * 检测模式: + * - #D4AF37 (金色十六进制) + * - rgba(212, 175, 55, x) (金色 RGBA) + * + * 使用方式: + * 在 .eslintrc.js 中添加规则配置 + */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: '禁止在 FUI 主题组件中使用硬编码颜色值', + category: 'Best Practices', + recommended: true, + }, + messages: { + noHardcodedGoldRgba: + '避免硬编码金色 RGBA 值 "{{value}}"。请使用 alpha("gold", {{opacity}}) 或 fui.border() 等语义化 API。', + noHardcodedGoldHex: + '避免硬编码金色十六进制值 "{{value}}"。请使用 fui.gold 或 FUI_COLORS.gold[400]。', + }, + schema: [], + }, + + create(context) { + const filename = context.getFilename(); + + // 仅在 src/views/Company 目录下生效 + if (!filename.includes('src/views/Company')) { + return {}; + } + + // 排除主题定义文件 + if ( + filename.includes('/theme/') || + filename.includes('/utils/colorUtils') + ) { + return {}; + } + + // 金色 RGBA 正则 - 匹配 rgba(212, 175, 55, x) + const goldRgbaPattern = + /rgba\(\s*212\s*,\s*175\s*,\s*55\s*,\s*([\d.]+)\s*\)/i; + + // 金色十六进制 - 匹配 #D4AF37(不区分大小写) + const goldHexPattern = /#D4AF37/i; + + function checkValue(node, value) { + if (typeof value !== 'string') return; + + // 检测金色十六进制 + if (goldHexPattern.test(value)) { + context.report({ + node, + messageId: 'noHardcodedGoldHex', + data: { value }, + }); + return; + } + + // 检测金色 RGBA + const rgbaMatch = value.match(goldRgbaPattern); + if (rgbaMatch) { + const opacity = rgbaMatch[1]; + context.report({ + node, + messageId: 'noHardcodedGoldRgba', + data: { value, opacity }, + }); + } + } + + return { + Literal(node) { + checkValue(node, node.value); + }, + + TemplateElement(node) { + checkValue(node, node.value.raw); + }, + }; + }, +}; diff --git a/package.json b/package.json index 91627049..a8298159 100755 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "cos-nodejs-sdk-v5": "^2.15.4", "env-cmd": "^11.0.0", "eslint-config-prettier": "8.3.0", + "eslint-plugin-local-rules": "^3.0.2", "eslint-plugin-prettier": "3.4.0", "gulp": "4.0.2", "gulp-append-prepend": "1.0.9", diff --git a/src/views/Company/components/FinancialPanorama/utils/chartOptions.ts b/src/views/Company/components/FinancialPanorama/utils/chartOptions.ts index cbdbb5cb..c30a3be4 100644 --- a/src/views/Company/components/FinancialPanorama/utils/chartOptions.ts +++ b/src/views/Company/components/FinancialPanorama/utils/chartOptions.ts @@ -3,6 +3,7 @@ */ import { formatUtils } from '@services/financialService'; +import { alpha, fui, chartTheme } from '@views/Company/theme'; interface ChartDataItem { period: string; @@ -106,22 +107,22 @@ export const getComparisonChartOption = ( text: '营收与利润趋势', left: 'center', textStyle: { - color: '#D4AF37', + color: fui.gold, fontSize: 16, fontWeight: 'bold', }, }, tooltip: { trigger: 'axis', - backgroundColor: 'rgba(26, 32, 44, 0.95)', - borderColor: 'rgba(212, 175, 55, 0.3)', + backgroundColor: chartTheme.tooltip.bg, + borderColor: chartTheme.tooltip.border, textStyle: { color: '#E2E8F0', }, axisPointer: { type: 'cross', crossStyle: { - color: 'rgba(212, 175, 55, 0.5)', + color: alpha('gold', 0.5), }, }, }, @@ -144,7 +145,7 @@ export const getComparisonChartOption = ( data: revenueData.map((d) => d.period), axisLine: { lineStyle: { - color: 'rgba(212, 175, 55, 0.3)', + color: chartTheme.axisLine, }, }, axisLabel: { @@ -161,7 +162,7 @@ export const getComparisonChartOption = ( }, axisLine: { lineStyle: { - color: 'rgba(212, 175, 55, 0.3)', + color: chartTheme.axisLine, }, }, axisLabel: { @@ -169,7 +170,7 @@ export const getComparisonChartOption = ( }, splitLine: { lineStyle: { - color: 'rgba(212, 175, 55, 0.1)', + color: chartTheme.splitLine, }, }, }, @@ -182,7 +183,7 @@ export const getComparisonChartOption = ( }, axisLine: { lineStyle: { - color: 'rgba(212, 175, 55, 0.3)', + color: chartTheme.axisLine, }, }, axisLabel: { @@ -201,7 +202,7 @@ export const getComparisonChartOption = ( itemStyle: { color: (params: { dataIndex: number; value: number }) => { const idx = params.dataIndex; - if (idx === 0) return '#D4AF37'; // 金色作为基准 + if (idx === 0) return fui.gold; // 金色作为基准 const prevValue = revenueData[idx - 1].value; const currValue = params.value; // 红涨绿跌 @@ -215,37 +216,18 @@ export const getComparisonChartOption = ( yAxisIndex: 1, data: profitData.map((d) => d.value?.toFixed(2)), smooth: true, - itemStyle: { color: '#D4AF37' }, - lineStyle: { width: 2, color: '#D4AF37' }, + itemStyle: { color: fui.gold }, + lineStyle: { width: 2, color: fui.gold }, areaStyle: { - color: { - type: 'linear', - x: 0, - y: 0, - x2: 0, - y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(212, 175, 55, 0.3)' }, - { offset: 1, color: 'rgba(212, 175, 55, 0.05)' }, - ], - }, + color: chartTheme.goldGradient(0.3, 0.05), }, }, ], }; }; -// 黑金主题饼图配色 -const BLACK_GOLD_PIE_COLORS = [ - '#D4AF37', // 金色 - '#B8860B', // 深金色 - '#FFD700', // 亮金色 - '#DAA520', // 金菊色 - '#CD853F', // 秘鲁色 - '#F4A460', // 沙褐色 - '#DEB887', // 实木色 - '#D2691E', // 巧克力色 -]; +// 黑金主题饼图配色(使用 chartTheme 统一定义) +const BLACK_GOLD_PIE_COLORS = chartTheme.goldSeries; /** * 生成主营业务饼图配置 - 黑金主题 @@ -265,7 +247,7 @@ export const getMainBusinessPieOption = ( subtext: subtitle, left: 'center', textStyle: { - color: '#D4AF37', + color: fui.gold, fontSize: 14, }, subtextStyle: { @@ -275,8 +257,8 @@ export const getMainBusinessPieOption = ( }, tooltip: { trigger: 'item', - backgroundColor: 'rgba(26, 32, 44, 0.95)', - borderColor: 'rgba(212, 175, 55, 0.3)', + backgroundColor: chartTheme.tooltip.bg, + borderColor: chartTheme.tooltip.border, textStyle: { color: '#E2E8F0', }, @@ -310,14 +292,14 @@ export const getMainBusinessPieOption = ( }, labelLine: { lineStyle: { - color: 'rgba(212, 175, 55, 0.5)', + color: alpha('gold', 0.5), }, }, emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, - shadowColor: 'rgba(212, 175, 55, 0.5)', + shadowColor: alpha('gold', 0.5), }, }, }, diff --git a/src/views/Company/components/MarketDataView/components/shared/styles.ts b/src/views/Company/components/MarketDataView/components/shared/styles.ts index e091ebb1..849a5b29 100644 --- a/src/views/Company/components/MarketDataView/components/shared/styles.ts +++ b/src/views/Company/components/MarketDataView/components/shared/styles.ts @@ -2,6 +2,7 @@ // 共享样式常量 import { darkGoldTheme } from '../../constants'; +import { alpha, fui } from '@views/Company/theme'; import type { SystemStyleObject } from '@chakra-ui/react'; /** @@ -21,7 +22,7 @@ export const darkGoldCardStyle: SystemStyleObject = { */ export const darkGoldCardHoverStyle: SystemStyleObject = { borderColor: darkGoldTheme.borderHover, - boxShadow: '0 8px 30px rgba(212, 175, 55, 0.15)', + boxShadow: `0 8px 30px ${alpha('gold', 0.15)}`, transform: 'translateY(-2px)', }; @@ -47,7 +48,7 @@ export const dataRowStyle: SystemStyleObject = { * 表格行悬停样式 */ export const tableRowHoverStyle: SystemStyleObject = { - bg: 'rgba(212, 175, 55, 0.08)', + bg: alpha('gold', 0.08), }; /** @@ -55,7 +56,7 @@ export const tableRowHoverStyle: SystemStyleObject = { */ export const tableBorderStyle: SystemStyleObject = { borderBottom: '1px solid', - borderColor: 'rgba(212, 175, 55, 0.1)', + borderColor: fui.border('subtle'), }; /** @@ -63,14 +64,14 @@ export const tableBorderStyle: SystemStyleObject = { */ export const financingRowStyle: SystemStyleObject = { p: 3, - bg: 'rgba(212, 175, 55, 0.08)', + bg: alpha('gold', 0.08), borderRadius: 'md', border: '1px solid', - borderColor: 'rgba(212, 175, 55, 0.15)', + borderColor: alpha('gold', 0.15), transition: 'all 0.2s', _hover: { - bg: 'rgba(212, 175, 55, 0.12)', - borderColor: 'rgba(212, 175, 55, 0.3)', + bg: alpha('gold', 0.12), + borderColor: alpha('gold', 0.3), }, }; @@ -127,8 +128,8 @@ export const sellRowStyle: SystemStyleObject = { */ export const dayCardStyle: SystemStyleObject = { p: 4, - bg: 'rgba(212, 175, 55, 0.05)', + bg: alpha('gold', 0.05), borderRadius: 'lg', border: '1px solid', - borderColor: 'rgba(212, 175, 55, 0.15)', + borderColor: alpha('gold', 0.15), }; diff --git a/src/views/Company/theme/fui.ts b/src/views/Company/theme/fui.ts index f4544456..e4938004 100644 --- a/src/views/Company/theme/fui.ts +++ b/src/views/Company/theme/fui.ts @@ -50,6 +50,20 @@ export const FUI_COLORS = { default: 'rgba(212, 175, 55, 0.2)', emphasis: 'rgba(212, 175, 55, 0.4)', glow: 'rgba(212, 175, 55, 0.6)', + // 完整透明度等级(用于替换硬编码) + opacity03: 'rgba(212, 175, 55, 0.03)', + opacity05: 'rgba(212, 175, 55, 0.05)', + opacity08: 'rgba(212, 175, 55, 0.08)', + opacity10: 'rgba(212, 175, 55, 0.1)', + opacity12: 'rgba(212, 175, 55, 0.12)', + opacity15: 'rgba(212, 175, 55, 0.15)', + opacity20: 'rgba(212, 175, 55, 0.2)', + opacity30: 'rgba(212, 175, 55, 0.3)', + opacity40: 'rgba(212, 175, 55, 0.4)', + opacity50: 'rgba(212, 175, 55, 0.5)', + opacity60: 'rgba(212, 175, 55, 0.6)', + opacity70: 'rgba(212, 175, 55, 0.7)', + opacity80: 'rgba(212, 175, 55, 0.8)', }, // 文字 diff --git a/src/views/Company/theme/index.ts b/src/views/Company/theme/index.ts index 10f75a8f..3d517d12 100644 --- a/src/views/Company/theme/index.ts +++ b/src/views/Company/theme/index.ts @@ -4,6 +4,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'; */ // 完整主题对象 @@ -20,6 +21,22 @@ export { // 主题组件 export * from './components'; +// ============================================ +// 工具函数导出(推荐使用) +// ============================================ + +export { + // 核心工具 + alpha, + hex, + fui, + BASE_COLORS, + OPACITY, + chartTheme, + // 类型 + type ColorName, +} from './utils'; + // ============================================ // 便捷常量导出(推荐使用) // ============================================ diff --git a/src/views/Company/theme/utils/colorUtils.ts b/src/views/Company/theme/utils/colorUtils.ts new file mode 100644 index 00000000..dd26d4d7 --- /dev/null +++ b/src/views/Company/theme/utils/colorUtils.ts @@ -0,0 +1,251 @@ +/** + * FUI 颜色工具函数 + * + * 提供便捷的颜色+透明度生成能力,用于替换硬编码的 rgba 值 + * + * @example + * // 替换 'rgba(212, 175, 55, 0.2)' 为: + * alpha('gold', 0.2) + * + * // 或使用语义化 API: + * fui.border() // => 'rgba(212, 175, 55, 0.2)' + * fui.border('hover') // => 'rgba(212, 175, 55, 0.4)' + */ + +// ============================================ +// 基础色值(单一数据源) +// ============================================ + +export const BASE_COLORS = { + gold: { r: 212, g: 175, b: 55 }, // #D4AF37 + white: { r: 255, g: 255, b: 255 }, + black: { r: 0, g: 0, b: 0 }, + bgPrimary: { r: 15, g: 15, b: 26 }, // #0F0F1A + bgElevated: { r: 26, g: 26, b: 46 }, // #1A1A2E + positive: { r: 239, g: 68, b: 68 }, // #EF4444 涨 + negative: { r: 34, g: 197, b: 94 }, // #22C55E 跌 + warning: { r: 245, g: 158, b: 11 }, // #F59E0B + info: { r: 59, g: 130, b: 246 }, // #3B82F6 +} as const; + +export type ColorName = keyof typeof BASE_COLORS; + +// ============================================ +// 核心工具函数 +// ============================================ + +/** + * 生成带透明度的颜色值 + * @param color - 颜色名称 + * @param opacity - 透明度 (0-1) + * @returns rgba 字符串 + * + * @example + * alpha('gold', 0.2) => 'rgba(212, 175, 55, 0.2)' + * alpha('white', 0.5) => 'rgba(255, 255, 255, 0.5)' + */ +export function alpha(color: ColorName, opacity: number): string { + const { r, g, b } = BASE_COLORS[color]; + return `rgba(${r}, ${g}, ${b}, ${opacity})`; +} + +/** + * 获取十六进制颜色值 + * @param color - 颜色名称 + * @returns hex 字符串 + * + * @example + * hex('gold') => '#D4AF37' + */ +export function hex(color: ColorName): string { + const { r, g, b } = BASE_COLORS[color]; + const toHex = (n: number) => n.toString(16).padStart(2, '0').toUpperCase(); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +} + +// ============================================ +// 预设透明度级别 +// ============================================ + +export const OPACITY = { + /** 0.05 - 极淡背景 */ + ghost: 0.05, + /** 0.08 - 微弱背景 */ + faint: 0.08, + /** 0.1 - 淡背景/边框 */ + subtle: 0.1, + /** 0.15 - 轻背景 */ + light: 0.15, + /** 0.2 - 默认边框 */ + muted: 0.2, + /** 0.3 - 标签边框 */ + soft: 0.3, + /** 0.4 - 悬停边框 */ + medium: 0.4, + /** 0.5 - 半透明 */ + half: 0.5, + /** 0.6 - 发光边框 */ + emphasis: 0.6, + /** 0.7 - 次要文字 */ + secondary: 0.7, + /** 0.8 - 高可见 */ + high: 0.8, + /** 0.95 - 主文字 */ + primary: 0.95, +} as const; + +// ============================================ +// 语义化颜色生成器 +// ============================================ + +type BorderVariant = 'subtle' | 'default' | 'hover' | 'emphasis'; +type BgVariant = 'ghost' | 'subtle' | 'light' | 'medium'; +type TextVariant = 'primary' | 'secondary' | 'muted' | 'dim'; +type GlowSize = 'sm' | 'md' | 'lg' | 'pulse'; + +/** + * FUI 语义化颜色 API + * 提供直观的颜色获取方式 + */ +export const fui = { + /** + * 边框颜色 + * @example + * fui.border() => 'rgba(212, 175, 55, 0.2)' + * fui.border('hover') => 'rgba(212, 175, 55, 0.4)' + */ + border: (variant: BorderVariant = 'default'): string => { + const opacityMap: Record = { + subtle: 0.1, + default: 0.2, + hover: 0.4, + emphasis: 0.6, + }; + return alpha('gold', opacityMap[variant]); + }, + + /** + * 背景颜色(金色系) + * @example + * fui.bg() => 'rgba(212, 175, 55, 0.1)' + * fui.bg('ghost') => 'rgba(212, 175, 55, 0.05)' + */ + bg: (variant: BgVariant = 'subtle'): string => { + const opacityMap: Record = { + ghost: 0.05, + subtle: 0.1, + light: 0.15, + medium: 0.2, + }; + return alpha('gold', opacityMap[variant]); + }, + + /** + * 文字颜色(白色系) + * @example + * fui.text() => 'rgba(255, 255, 255, 0.95)' + * fui.text('secondary') => 'rgba(255, 255, 255, 0.7)' + */ + text: (variant: TextVariant = 'primary'): string => { + const opacityMap: Record = { + primary: 0.95, + secondary: 0.7, + muted: 0.5, + dim: 0.3, + }; + return alpha('white', opacityMap[variant]); + }, + + /** + * 发光效果(box-shadow 值) + * @example + * fui.glow() => '0 0 16px rgba(212, 175, 55, 0.4)' + * fui.glow('lg') => '0 0 32px rgba(212, 175, 55, 0.5)' + */ + glow: (size: GlowSize = 'md'): string => { + const sizeMap: Record = { + sm: `0 0 8px ${alpha('gold', 0.3)}`, + md: `0 0 16px ${alpha('gold', 0.4)}`, + lg: `0 0 32px ${alpha('gold', 0.5)}`, + pulse: `0 0 20px ${alpha('gold', 0.6)}, 0 0 40px ${alpha('gold', 0.3)}`, + }; + return sizeMap[size]; + }, + + /** + * 玻璃态边框(完整 border 属性) + * @example + * fui.glassBorder() => '1px solid rgba(212, 175, 55, 0.2)' + */ + glassBorder: (variant: BorderVariant = 'default'): string => { + return `1px solid ${fui.border(variant)}`; + }, + + /** + * 状态颜色 + */ + status: { + positive: hex('positive'), // #EF4444 + negative: hex('negative'), // #22C55E + warning: hex('warning'), // #F59E0B + info: hex('info'), // #3B82F6 + positiveBg: alpha('positive', 0.15), + negativeBg: alpha('negative', 0.15), + }, + + /** + * 金色(主色调) + */ + gold: hex('gold'), // #D4AF37 +} as const; + +// ============================================ +// 图表主题配置 +// ============================================ + +/** + * ECharts 图表主题配置 + */ +export const chartTheme = { + // 轴线和网格线 + axisLine: alpha('gold', 0.3), + splitLine: alpha('gold', 0.1), + + // Tooltip + tooltip: { + bg: alpha('bgElevated', 0.95), + border: alpha('gold', 0.3), + text: alpha('white', 0.9), + }, + + // 标题 + title: { + color: hex('gold'), + subtextColor: alpha('white', 0.6), + }, + + // 金色系列配色 + goldSeries: [ + '#D4AF37', + '#B8860B', + '#FFD700', + '#DAA520', + '#CD853F', + '#F4A460', + '#DEB887', + '#D2691E', + ], + + // 渐变生成器 + goldGradient: (startOpacity = 0.3, endOpacity = 0.05) => ({ + type: 'linear' as const, + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: alpha('gold', startOpacity) }, + { offset: 1, color: alpha('gold', endOpacity) }, + ], + }), +} as const; diff --git a/src/views/Company/theme/utils/index.ts b/src/views/Company/theme/utils/index.ts new file mode 100644 index 00000000..2cccc9d4 --- /dev/null +++ b/src/views/Company/theme/utils/index.ts @@ -0,0 +1,15 @@ +/** + * FUI 主题工具函数导出 + */ + +export { + // 核心工具 + alpha, + hex, + fui, + BASE_COLORS, + OPACITY, + chartTheme, + // 类型 + type ColorName, +} from './colorUtils';