feat(Company): 优化主题系统,添加颜色工具函数和 ESLint 规则

- 新增 colorUtils.ts:提供 alpha()、hex()、fui.* 语义化颜色 API
- 新增 chartTheme:统一图表主题配置(坐标轴、tooltip、渐变等)
- 扩展 FUI_COLORS.line:完整的透明度级别(0.03-0.8)
- 新增 ESLint 规则:检测硬编码金色值(rgba/hex),warning 级别
- 迁移 chartOptions.ts:~15 处硬编码值改用工具函数
- 迁移 shared/styles.ts:~8 处硬编码值改用工具函数

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-24 19:29:53 +08:00
parent fc13666ff0
commit 7f4f9e4032
10 changed files with 434 additions and 47 deletions

View File

@@ -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 等效)*/

10
eslint-rules/index.js Normal file
View File

@@ -0,0 +1,10 @@
/**
* 本地 ESLint 规则插件
*
* 在 .eslintrc.js 中通过 eslint-plugin-local-rules 使用
*/
module.exports = {
rules: {
'no-hardcoded-fui-colors': require('./no-hardcoded-fui-colors'),
},
};

View File

@@ -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);
},
};
},
};

View File

@@ -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",

View File

@@ -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),
},
},
},

View File

@@ -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),
};

View File

@@ -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)',
},
// 文字

View File

@@ -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';
// ============================================
// 便捷常量导出(推荐使用)
// ============================================

View File

@@ -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<BorderVariant, number> = {
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<BgVariant, number> = {
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<TextVariant, number> = {
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<GlowSize, string> = {
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;

View File

@@ -0,0 +1,15 @@
/**
* FUI 主题工具函数导出
*/
export {
// 核心工具
alpha,
hex,
fui,
BASE_COLORS,
OPACITY,
chartTheme,
// 类型
type ColorName,
} from './colorUtils';