cherry-studio/packages/ui/ICON_IMPLEMENTATION_GUIDE.md
MyPrototypeWhat a3062d6e38 Add ICON_IMPLEMENTATION_GUIDE and new SVG icons
- Created ICON_IMPLEMENTATION_GUIDE.md to document icon usage and common issues.
- Added multiple new SVG icons to the `icons/` directory, enhancing the icon library.
- Updated tailwind.css for improved styling consistency across components.
2025-11-14 13:31:17 +08:00

1322 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Cherry Studio UI - 彩色 Logo 图标系统实施方案
> 借鉴 Lucide IconNode 架构,为 Cherry Studio UI 库构建专门支持彩色品牌 Logo 的轻量级图标系统
## 📋 目录
- [项目概述](#项目概述)
- [架构设计](#架构设计)
- [核心概念](#核心概念)
- [实施步骤](#实施步骤)
- [使用示例](#使用示例)
- [工作流程](#工作流程)
- [常见问题](#常见问题)
---
## 项目概述
### 背景
Cherry Studio 是一个 Electron + React 的 monorepo 项目,主包通过 Vite alias 直接引用 UI 包的源码。我们需要为 UI 库添加 40+ 个 **彩色品牌 Logo 图标**(如 Anthropic、OpenAI、DeepSeek、Cohere 等)。
### 图标特点
这些图标与传统线性图标(如 Lucide有本质区别
| 特征 | 传统线性图标 | 我们的彩色 Logo |
|------|------------|----------------|
| **颜色方式** | `stroke="currentColor"` | `fill="#3F3FAA"` 等固定颜色 |
| **结构** | 简单 path | 复杂嵌套 (`<g>`, `<clipPath>`, `<defs>`) |
| **样式控制** | 可动态改变颜色/描边 | 必须保留原始颜色 |
| **使用场景** | UI 通用图标 | 品牌标识 |
### 方案特点
-**保留原色**:完整保留 SVG 中的所有 fill 颜色
-**支持嵌套**:处理复杂的 `<g>`、`<clipPath>`、`<defs>` 结构
-**借鉴 Lucide**:采用 IconNode 数据结构,工厂模式创建组件
-**轻量级实现**:简化构建流程,无需复杂工具链
-**源码直连**:主包通过 alias 直接使用源码,支持热更新
-**TypeScript 支持**:完整的类型推导和类型安全
-**Tailwind 友好**:完美支持 Tailwind CSS 样式
-**自动化生成**:一键从 SVG 生成 React 组件
### 技术栈
- React 19
- TypeScript 5.8
- SVGO 3.0 (SVG 优化)
- Tailwind CSS 4.1
---
## 架构设计
### 目录结构
```
cherry-studio/
├── packages/ui/
│ ├── icons/ # ① 源 SVG 文件目录
│ │ ├── arrow-right.svg
│ │ ├── check.svg
│ │ ├── close.svg
│ │ └── ... (40+ SVG 文件)
│ │
│ ├── scripts/
│ │ └── generate-icons.ts # ② 生成脚本
│ │
│ ├── src/
│ │ └── components/
│ │ └── icons/
│ │ ├── Icon.tsx # ③ 基础组件
│ │ ├── generated/ # ④ 自动生成的图标组件
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Check.tsx
│ │ │ ├── Close.tsx
│ │ │ └── index.ts
│ │ └── index.ts # ⑤ 统一导出
│ │
│ └── package.json
└── src/renderer/src/ # ⑦ 主包使用
└── components/
└── YourComponent.tsx
```
### 架构分层
```
┌─────────────────────────────────────────────────────────────┐
│ 用户层 │
│ <ArrowRight size={24} className="text-blue-500" /> │
└────────────────────────┬────────────────────────────────────┘
┌────────────────────────▼────────────────────────────────────┐
│ 具体图标组件层 │
│ ArrowRight = createIcon('ArrowRight', iconNode) │
└────────────────────────┬────────────────────────────────────┘
┌────────────────────────▼────────────────────────────────────┐
│ 工厂函数层 │
│ createIcon(name, iconNode) → IconComponent │
└────────────────────────┬────────────────────────────────────┘
┌────────────────────────▼────────────────────────────────────┐
│ 基础组件层 │
│ Icon: 渲染 SVG处理 props映射 IconNode │
└────────────────────────┬────────────────────────────────────┘
┌────────────────────────▼────────────────────────────────────┐
│ 数据层 │
│ IconNode: [['path', { d: '...' }], ['circle', {...}]] │
└─────────────────────────────────────────────────────────────┘
```
---
## 核心概念
### 1. IconNode 数据结构(支持嵌套)
借鉴 Lucide 的核心设计,扩展为支持嵌套结构的数组格式:
```typescript
type IconNode = [
tag: string, // SVG 元素标签名
attrs: Record<string, string | number>, // 元素属性
children?: IconNode // 子元素(支持嵌套)
][];
// 简单示例(平面结构)
const simpleIcon: IconNode = [
['path', { d: 'M5 12h14', fill: '#000' }],
['circle', { cx: '12', cy: '12', r: '10', fill: '#fff' }]
];
// 复杂示例(嵌套结构,品牌 Logo 常见)
const complexIcon: IconNode = [
['g', { clipPath: 'url(#clip0)' }, [
['path', { d: 'M18 0H6...', fill: '#CA9F7B' }],
['path', { d: 'M15.38 6.43...', fill: '#191918' }]
]],
['defs', {}, [
['clipPath', { id: 'clip0' }, [
['rect', { width: '24', height: '24', fill: 'white' }]
]]
]]
];
```
**优势:**
- 📦 体积极小(只存储数据,不存储模板)
- 🔄 框架无关(可复用于 Vue/Svelte 等)
- ⚡ 渲染快速(直接 createElement无需解析
- 🎨 **保留原色**(完整保留 fill 等样式属性)
- 🌲 **支持嵌套**(处理复杂的品牌 Logo 结构)
### 2. 工厂模式
使用 `createIcon` 函数统一创建图标组件:
```typescript
export function createIcon(
componentName: string,
iconNode: IconNode
) {
const IconComponent = forwardRef<SVGSVGElement, IconProps>(
(props, ref) => {
return <Icon ref={ref} iconNode={iconNode} {...props} />;
}
);
IconComponent.displayName = componentName;
return IconComponent;
}
```
**优势:**
- ✅ 代码复用40+ 图标共享同一套逻辑)
- ✅ 统一行为(所有图标的 props 处理完全一致)
- ✅ 易于维护(修改一处,全部更新)
### 3. 自动化生成
从 SVG 到 React 组件的自动化流程:
```
SVG 文件 → SVGO 优化 → 正则解析 → IconNode → 组件代码 → 写入文件
```
---
## 实施步骤
### 步骤 1创建基础 Icon 组件
创建文件:`packages/ui/src/components/icons/Icon.tsx`
```tsx
import React, { forwardRef, memo } from 'react';
import { cn } from '@/utils';
/**
* IconNode 数据结构(借鉴 lucide扩展支持嵌套
* 格式: [标签名, 属性对象, 子元素(可选)]
*/
export type IconNode = [
tag: string,
attrs: Record<string, string | number>,
children?: IconNode
][];
/**
* Icon 组件的 Props专为彩色 Logo 优化)
*/
export interface IconProps extends React.SVGProps<SVGSVGElement> {
/** 图标大小支持数字px或字符串如 "1rem" */
size?: number | string;
/** 自定义类名 */
className?: string;
/** 子元素 */
children?: React.ReactNode;
}
/**
* Icon 组件内部 Props包含 iconNode
*/
interface IconComponentProps extends IconProps {
iconNode: IconNode;
/** 图标名称(用于 className */
iconName?: string;
}
/**
* 递归渲染 IconNode支持嵌套结构
*/
function renderNodes(nodes: IconNode, keyPrefix = ''): React.ReactNode[] {
return nodes.map((node, index) => {
const [tag, attrs, children] = node;
const key = `${keyPrefix}${index}`;
// 如果有子元素,递归渲染
const childElements = children ? renderNodes(children, `${key}-`) : undefined;
return React.createElement(
tag,
{ key, ...attrs },
childElements
);
});
}
/**
* 基础 Icon 组件(专为彩色品牌 Logo 设计)
* - 保留 SVG 原始颜色(不强制 fill/stroke
* - 支持嵌套结构g, clipPath, defs 等)
* - 只控制 size 和 className
*/
export const Icon = memo(
forwardRef<SVGSVGElement, IconComponentProps>(
(
{
iconNode,
iconName,
size = 24,
className,
children,
...props
},
ref
) => {
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
className={cn(
'inline-block flex-shrink-0',
iconName && `icon-${iconName}`,
className
)}
{...props}
>
{/* 递归渲染 IconNode支持嵌套 */}
{renderNodes(iconNode)}
{children}
</svg>
);
}
)
);
Icon.displayName = 'Icon';
/**
* 工厂函数:创建具体的图标组件
* @param componentName - 组件名称PascalCase
* @param iconNode - 图标数据
* @returns 图标组件
*/
export function createIcon(
componentName: string,
iconNode: IconNode
) {
const IconComponent = forwardRef<SVGSVGElement, IconProps>(
(props, ref) => {
return (
<Icon
ref={ref}
iconNode={iconNode}
iconName={componentName}
{...props}
/>
);
}
);
IconComponent.displayName = componentName;
return IconComponent;
}
```
---
### 步骤 2创建生成脚本
创建文件:`packages/ui/scripts/generate-icons.ts`
```typescript
import fs from 'fs/promises';
import path from 'path';
import { optimize } from 'svgo';
const ICONS_DIR = path.join(__dirname, '../icons');
const OUTPUT_DIR = path.join(__dirname, '../src/components/icons/generated');
// SVGO 优化配置(专为彩色 Logo 优化)
const svgoConfig = {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
// 保留 viewBox必须
removeViewBox: false,
// 不转换为 path保持原始形状
convertShapeToPath: false,
// 不移除隐藏元素(可能包含 defs
removeHiddenElems: false,
},
},
},
{
// 只移除 width 和 height保留所有颜色和样式
name: 'removeAttrs',
params: {
attrs: '(width|height)',
},
},
],
};
/**
* 转换命名kebab-case → PascalCase
* 例: arrow-right → ArrowRight, 302ai → Ai302
*/
function toPascalCase(str: string): string {
// 处理数字开头的情况(如 302ai
if (/^\d/.test(str)) {
// 提取开头的数字和后续部分
const match = str.match(/^(\d+)(.*)$/);
if (match) {
const [, numbers, rest] = match;
// 将数字放在后面302ai → Ai302
str = rest + numbers;
}
}
return str
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
/**
* 解析 SVG 为 IconNode 格式(支持嵌套结构)
* 使用 svgson 库来可靠地处理复杂的嵌套结构
*/
async function parseSvg(svgContent: string): Promise<string> {
const { parse } = await import('svgson');
// 1. SVGO 优化
const optimized = optimize(svgContent, svgoConfig);
const svgString = optimized.data;
// 2. 使用 svgson 解析为 AST
const ast = await parse(svgString);
// 3. 转换为 IconNode 格式
const iconNode = convertToIconNode(ast.children);
// 4. 格式化为 TypeScript 代码
return formatIconNode(iconNode);
}
/**
* 将 svgson AST 转换为 IconNode 格式
*/
function convertToIconNode(nodes: any[]): any[] {
return nodes.map(node => {
const { name, attributes, children } = node;
if (children && children.length > 0) {
const childNodes = convertToIconNode(children);
return [name, attributes, childNodes];
} else {
return [name, attributes];
}
});
}
/**
* 格式化 IconNode 为 TypeScript 代码
*/
function formatIconNode(nodes: any[], indent = 0): string {
const indentStr = ' '.repeat(indent + 1);
const items = nodes.map(node => {
const [tag, attrs, children] = node;
if (children) {
const childrenStr = formatIconNode(children, indent + 1);
return `${indentStr}['${tag}', ${JSON.stringify(attrs)}, ${childrenStr}]`;
} else {
return `${indentStr}['${tag}', ${JSON.stringify(attrs)}]`;
}
});
if (indent === 0) {
return `[\n${items.join(',\n')}\n]`;
} else {
return `[\n${items.join(',\n')}\n${' '.repeat(indent)}]`;
}
}
/**
* 生成单个图标组件文件
*/
async function generateIconComponent(
iconName: string,
iconNode: string
): Promise<string> {
const componentName = toPascalCase(iconName);
return `import { forwardRef } from 'react';
import { createIcon, type IconProps } from '../Icon';
import type { IconNode } from '../Icon';
const iconNode: IconNode = ${iconNode};
/**
* ${componentName} icon component
*
* @example
* <${componentName} size={24} color="red" />
* <${componentName} className="text-blue-500" />
*/
export const ${componentName} = createIcon('${componentName}', iconNode);
export default ${componentName};
`;
}
/**
* 主函数:生成所有图标
*/
async function generateIcons() {
console.log('🚀 开始生成图标组件...\n');
try {
// 检查 icons 目录是否存在
try {
await fs.access(ICONS_DIR);
} catch {
console.error(`❌ 错误: 找不到 icons 目录: ${ICONS_DIR}`);
console.log(`💡 提示: 请创建 ${ICONS_DIR} 目录并放入 SVG 文件`);
process.exit(1);
}
// 确保输出目录存在
await fs.mkdir(OUTPUT_DIR, { recursive: true });
// 读取所有 SVG 文件
const files = await fs.readdir(ICONS_DIR);
const svgFiles = files.filter(f => f.endsWith('.svg'));
if (svgFiles.length === 0) {
console.warn(`⚠️ 警告: ${ICONS_DIR} 目录中没有找到 SVG 文件`);
process.exit(0);
}
console.log(`📁 找到 ${svgFiles.length} 个 SVG 文件\n`);
const exports: string[] = [];
let successCount = 0;
let errorCount = 0;
// 处理每个 SVG 文件
for (const file of svgFiles) {
const iconName = file.replace('.svg', '');
const componentName = toPascalCase(iconName);
try {
console.log(`⚙️ 处理: ${iconName}`);
// 读取 SVG 内容
const svgPath = path.join(ICONS_DIR, file);
const svgContent = await fs.readFile(svgPath, 'utf-8');
// 解析为 IconNode
const iconNode = await parseSvg(svgContent);
// 生成组件代码
const componentCode = await generateIconComponent(iconName, iconNode);
// 写入文件
const outputPath = path.join(OUTPUT_DIR, `${componentName}.tsx`);
await fs.writeFile(outputPath, componentCode, 'utf-8');
// 收集导出语句
exports.push(`export { ${componentName} } from './${componentName}';`);
successCount++;
} catch (error) {
console.error(`❌ 处理 ${iconName} 失败:`, error);
errorCount++;
}
}
// 生成 index.ts统一导出
const indexContent = `/**
* 自动生成的图标导出文件
* 请勿手动编辑
*
* 生成时间: ${new Date().toISOString()}
* 图标数量: ${successCount}
*/
${exports.sort().join('\n')}
`;
await fs.writeFile(
path.join(OUTPUT_DIR, 'index.ts'),
indexContent,
'utf-8'
);
// 输出结果
console.log(`\n✅ 成功生成 ${successCount} 个图标组件!`);
if (errorCount > 0) {
console.log(`⚠️ 失败 ${errorCount} 个图标`);
}
console.log(`📦 输出目录: ${OUTPUT_DIR}`);
} catch (error) {
console.error('\n❌ 生成过程发生错误:', error);
process.exit(1);
}
}
// 执行生成
generateIcons();
```
---
### 步骤 3更新 package.json
`packages/ui/package.json` 中添加:
```json
{
"scripts": {
"generate:icons": "tsx scripts/generate-icons.ts",
"build": "pnpm generate:icons && tsdown",
"dev": "tsc -w"
},
"devDependencies": {
"svgo": "^3.0.0",
"svgson": "^5.3.1",
"tsx": "^4.20.5"
}
}
```
安装依赖:
```bash
cd packages/ui
pnpm add -D svgo svgson tsx
```
---
### 步骤 4创建统一导出
创建文件:`packages/ui/src/components/icons/index.ts`
```typescript
/**
* Icons 模块统一导出
*/
// 导出基础组件和类型
export { Icon, createIcon, type IconProps, type IconNode } from './Icon';
// 导出所有生成的图标
export * from './generated';
```
---
### 步骤 5更新主导出文件
`packages/ui/src/components/index.ts` 中添加:
```typescript
// Icons
export * from './icons';
```
---
### 步骤 6配置子路径导出推荐
为了支持 `@cherrystudio/ui/icons` 导入路径,需要在 `package.json` 中配置 `exports` 字段:
```json
{
"name": "@cherrystudio/ui",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./src/components/index.ts",
"types": "./src/components/index.ts"
},
"./icons": {
"import": "./src/components/icons/index.ts",
"types": "./src/components/icons/index.ts"
}
}
}
```
**同时更新主包的 Vite 配置**`electron.vite.config.ts`
```typescript
export default defineConfig({
resolve: {
alias: {
'@cherrystudio/ui': resolve('packages/ui/src/components'),
'@cherrystudio/ui/icons': resolve('packages/ui/src/components/icons'),
}
}
});
```
这样你就可以使用两种导入方式:
```tsx
// 推荐:从 icons 子路径导入84个图标
import { Anthropic, Deepseek } from '@cherrystudio/ui/icons';
// 兼容:从主包导入
import { Anthropic, Deepseek } from '@cherrystudio/ui';
```
---
### 步骤 7创建 icons 目录
```bash
cd packages/ui
mkdir icons
```
将你的 84 个 SVG 文件放入 `icons/` 目录。
---
## 使用示例
### 基础用法(彩色品牌 Logo
```tsx
// 推荐:从 icons 子路径导入84个图标语义更清晰
import { Anthropic, Deepseek, Cohere, Ai302 } from '@cherrystudio/ui/icons';
// 也可以:从主包导入(兼容方式)
// import { Anthropic, Deepseek } from '@cherrystudio/ui';
export function BasicExample() {
return (
<div className="flex gap-4 items-center">
{/* 默认大小24px */}
<Anthropic />
{/* 自定义大小 */}
<Deepseek size={32} />
<Cohere size={48} />
{/* 使用字符串大小 */}
<Ai302 size="2rem" />
</div>
);
}
```
### Tailwind CSS 集成
```tsx
import { Anthropic, Deepseek } from '@cherrystudio/ui/icons';
export function TailwindExample() {
return (
<div>
{/* 使用 Tailwind 控制大小 */}
<Anthropic className="w-8 h-8" />
{/* 悬停效果(注意:颜色不可更改,但可添加缩放、阴影等效果) */}
<Deepseek className="w-10 h-10 hover:scale-110 cursor-pointer transition-transform" />
{/* 响应式设计 */}
<Anthropic className="w-6 h-6 sm:w-8 sm:h-8 lg:w-10 lg:h-10" />
{/* 添加阴影和圆角效果 */}
<Deepseek className="w-12 h-12 rounded-lg shadow-lg hover:shadow-xl transition-shadow" />
</div>
);
}
```
**重要提示:**
-**不要尝试修改图标颜色**(这些是品牌 Logo颜色是固定的
- ✅ 可以修改 `size`、`className`
- ✅ 可以使用 Tailwind 的 `scale`、`opacity`、`transform`、`shadow` 等效果
### 事件处理
```tsx
import { Anthropic, Deepseek } from '@cherrystudio/ui/icons';
export function EventExample() {
const handleClick = () => {
console.log('Logo clicked!');
};
return (
<div className="flex gap-4">
{/* onClick 事件 */}
<Anthropic onClick={handleClick} className="cursor-pointer hover:opacity-80" />
{/* 其他事件 */}
<Deepseek
onMouseEnter={() => console.log('Mouse enter')}
onMouseLeave={() => console.log('Mouse leave')}
className="cursor-pointer transition-opacity hover:opacity-75"
/>
</div>
);
}
```
### 使用 ref
```tsx
import { Anthropic } from '@cherrystudio/ui/icons';
import { useRef, useEffect } from 'react';
export function RefExample() {
const iconRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (iconRef.current) {
console.log('Logo SVG element:', iconRef.current);
// 可以执行 DOM 操作,如添加动画
}
}, []);
return <Anthropic ref={iconRef} size={32} />;
}
```
### 自定义彩色图标
如果你有自己的彩色 SVG 图标,可以手动创建:
```tsx
import { createIcon, type IconNode } from '@cherrystudio/ui/icons';
// 定义自定义彩色图标数据(带嵌套和颜色)
const customLogoNode: IconNode = [
['g', { clipPath: 'url(#clip0)' }, [
['circle', { cx: '12', cy: '12', r: '10', fill: '#FF6B6B' }],
['path', { d: 'M12 6v6l4 2', stroke: '#fff', strokeWidth: '2' }]
]],
['defs', {}, [
['clipPath', { id: 'clip0' }, [
['rect', { width: '24', height: '24', fill: 'white' }]
]]
]]
];
// 创建自定义图标组件
const MyBrandLogo = createIcon('MyBrandLogo', customLogoNode);
export function CustomIconExample() {
return <MyBrandLogo size={48} className="hover:scale-110 transition-transform" />;
}
```
### 组合使用
```tsx
import { Anthropic, Deepseek, Cohere } from '@cherrystudio/ui/icons';
export function CompositeExample() {
return (
<div className="space-y-6">
{/* AI 模型选择器 */}
<div className="flex gap-4">
{[
{ Logo: Anthropic, name: 'Claude' },
{ Logo: Deepseek, name: 'DeepSeek' },
{ Logo: Cohere, name: 'Cohere' }
].map(({ Logo, name }) => (
<button
key={name}
className="flex flex-col items-center gap-2 p-4 border rounded-lg hover:bg-gray-50 transition-colors"
>
<Logo size={48} />
<span className="text-sm font-medium">{name}</span>
</button>
))}
</div>
{/* Logo 网格展示 */}
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center justify-center p-4 bg-white rounded-lg shadow">
<Anthropic size={32} />
</div>
<div className="flex items-center justify-center p-4 bg-white rounded-lg shadow">
<Deepseek size={32} />
</div>
<div className="flex items-center justify-center p-4 bg-white rounded-lg shadow">
<Cohere size={32} />
</div>
</div>
</div>
);
}
```
---
## 工作流程
### 开发流程
```bash
# 1. 准备 SVG 文件
# 将 SVG 文件放到 packages/ui/icons/ 目录
# 2. 生成图标组件
cd packages/ui
pnpm generate:icons
# 3. 在主包中使用
# 主包会通过 Vite alias 自动识别,直接导入使用
```
### 生成的文件
```
packages/ui/src/components/icons/
├── Icon.tsx # ✅ 手写(基础组件)
├── generated/ # ⚠️ 自动生成(不要手动编辑)
│ ├── ArrowRight.tsx
│ ├── Check.tsx
│ ├── Close.tsx
│ ├── ... (40+ 个文件)
│ └── index.ts
└── index.ts # ✅ 手写(统一导出)
```
### 构建流程
**开发模式:**
- 主包通过 Vite alias 直接引用 UI 包源码
- 支持热更新HMR
- 无需构建 UI 包
**生产构建:**
```bash
# UI 包单独构建(如需发布)
cd packages/ui
pnpm build # 会先 generate:icons然后 tsdown 打包
# 主包构建
cd cherry-studio
pnpm build # Vite 会处理 UI 包的源码
```
---
## 常见问题
### Q1: 为什么主包可以直接使用源码?
**A:** 因为主包的 Vite 配置了 alias
```typescript
// electron.vite.config.ts
'@cherrystudio/ui': resolve('packages/ui/src')
```
这样导入的是 `.tsx` 源文件Vite 会像处理主包代码一样处理这些文件,支持热更新和 TypeScript 类型推导。
---
### Q2: 彩色 Logo SVG 文件有什么要求?
**A:** 针对彩色品牌 Logo 的要求:
- ✅ 使用标准的 24x24 viewBox推荐
- ✅ 保留所有 `fill` 颜色(会自动保留)
- ✅ 支持复杂嵌套结构(`<g>`, `<clipPath>`, `<defs>` 等)
- ✅ 文件名使用 kebab-case`anthropic.svg`、`deep-seek.svg`
- ✅ 数字开头的文件名会自动转换(如 `302ai.svg``Ai302` 组件)
示例彩色 Logo SVG
```xml
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M18 0H6C2.68629 0..." fill="#CA9F7B"/>
<path d="M15.3843 6.43481H12..." fill="#191918"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>
```
**注意事项:**
- ⚠️ 确保 SVG 格式正确(使用 Figma/Illustrator 导出时选择"优化"
- ⚠️ ID 属性可能需要全局唯一(如 `clip0` 改为 `clip-anthropic`
- ⚠️ 过大的 SVG 文件(超过 100KB建议先手动优化
---
### Q3: 如何调试生成错误?
**A:** 如果某个图标生成失败:
1. **检查 SVG 文件语法**:使用浏览器直接打开 SVG 文件,看是否正常显示
2. **测试 SVGO 优化**
```bash
npx svgo icons/your-icon.svg -o test.svg
```
3. **查看生成脚本的错误日志**:运行 `pnpm generate:icons` 时会显示详细错误
4. **验证 SVG 结构**:本方案支持所有标准 SVG 元素(通过 svgson 解析)
5. **检查文件编码**:确保 SVG 文件是 UTF-8 编码
---
### Q4: 如何添加新图标?
**A:** 非常简单:
```bash
# 1. 将新的 SVG 文件放入 icons/ 目录
cp new-icon.svg packages/ui/icons/
# 2. 重新生成
cd packages/ui
pnpm generate:icons
# 3. 立即可用(无需重启开发服务器)
import { NewIcon } from '@cherrystudio/ui/icons';
```
---
### Q5: 如何自定义生成的代码?
**A:** 修改 `scripts/generate-icons.ts` 中的 `generateIconComponent` 函数:
```typescript
async function generateIconComponent(
iconName: string,
iconNode: string
): Promise<string> {
const componentName = toPascalCase(iconName);
// 在这里自定义生成的代码模板
return `...`;
}
```
---
### Q6: TypeScript 类型如何工作?
**A:** 完全自动,无需手动声明:
```typescript
// IconProps 继承自 React.SVGProps<SVGSVGElement>
// 所以支持所有 SVG 属性
<Anthropic
size={24} // IconProps 自定义数字或字符串
className="..." // SVGProps
onClick={() => {}} // SVGProps
onMouseEnter={() => {}} // SVGProps
style={{ opacity: 0.8 }} // SVGProps
aria-label="Anthropic" // SVGProps
// ... 所有 SVG 属性
/>
```
**注意:** 彩色 Logo 版本移除了 `color``strokeWidth` 属性(因为颜色是固定的)
---
### Q7: 生成的文件需要提交到 Git 吗?
**A:** 推荐做法:
-**提交** `generated/` 目录(方便团队协作)
-**在 CI/CD 中重新生成**(确保一致性)
`.gitignore` 配置:
```gitignore
# 可选:不提交生成文件(需要在 CI 中生成)
# packages/ui/src/components/icons/generated/
# 必须提交 icons 源文件
!packages/ui/icons/*.svg
```
---
### Q8: 能否修改 Logo 的颜色?
**A:****不建议修改品牌 Logo 的颜色**
这些是品牌官方 Logo颜色是品牌标识的一部分**不应该修改**。
**替代方案:**
```tsx
import { Anthropic } from '@cherrystudio/ui/icons';
// ✅ 可以调整透明度
<Anthropic className="opacity-50" />
<Anthropic style={{ opacity: 0.8 }} />
// ✅ 可以添加滤镜效果(慎用)
<Anthropic className="grayscale" /> // 灰度滤镜
<Anthropic style={{ filter: 'brightness(1.2)' }} />
// ❌ 不要尝试修改颜色
// <Anthropic style={{ fill: 'red' }} /> // 无效
// <Anthropic className="text-red-500" /> // 无效
```
**为什么无法修改颜色?**
- 颜色信息存储在 IconNode 数据中(如 `fill="#CA9F7B"`
- 这是设计决策:保护品牌标识的完整性
- 如果需要可变颜色的图标,应该使用 Lucide 等线性图标库
---
### Q9: 如何与 Lucide React 共存?
**A:** 完全可以同时使用:
```tsx
// 使用彩色品牌 Logo84个
import { Anthropic, Deepseek } from '@cherrystudio/ui/icons';
// 使用 Lucide 线性图标
import { Heart, Settings, User } from 'lucide-react';
export function MixedExample() {
return (
<div>
{/* 品牌 Logo固定颜色 */}
<Anthropic size={24} />
<Deepseek size={24} />
{/* Lucide 图标:可变颜色 */}
<Heart size={24} color="red" />
<Settings size={24} className="text-blue-500" />
</div>
);
}
```
两者的 API 基本一致(都继承自 Lucide 的设计),但用途不同:
- **Cherry Studio Icons**:彩色品牌 Logo
- **Lucide Icons**:单色通用图标
---
### Q10: 性能如何?
**A:** 性能优秀:
- 📦 **体积小**:每个图标约 0.5-1.5KBIconNode 数据)
-**渲染快**:直接 `createElement`,无需解析
- 🌲 **Tree-shaking**:只打包使用的图标
- 💾 **无运行时**:零运行时依赖
- 🎨 **保留细节**:完整保留彩色 Logo 的所有颜色和细节
对比:
```
传统方式(内联 SVG JSX~2-3KB/Logo
IconNode 方式(彩色):~0.5-1.5KB/Logo
节省:~50-75% 体积
```
**注意:** 彩色 Logo 比简单线性图标稍大,因为包含更多颜色和路径信息,但仍然非常高效。
---
### Q11: 如何批量更新图标?
**A:** 直接替换 SVG 文件,然后重新生成:
```bash
# 1. 更新 SVG 文件(替换现有的或添加新的)
cp new-logos/*.svg packages/ui/icons/
# 2. 重新生成组件
cd packages/ui
pnpm generate:icons
# 3. 所有使用该图标的地方自动更新(无需修改代码)
```
**批量处理技巧:**
```bash
# 批量优化 SVG使用 SVGO
npx svggo -f icons/ -o icons-optimized/
# 批量重命名(确保 kebab-case
# 使用 rename 工具或脚本处理
```
---
## 附录
### A. 命名规范
**SVG 文件名(品牌 Logo**
- 使用 kebab-case`anthropic.svg`、`deep-seek.svg`
- 只包含小写字母、数字、连字符
- 使用品牌官方名称
- 数字开头会自动处理:`302ai.svg` → `Ai302`
**组件名(自动生成):**
- 自动转换为 PascalCase`Anthropic`、`DeepSeek`、`Ai302`
- 无需手动指定
### B. SVGO 配置说明(彩色 Logo 专用)
```javascript
{
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false, // 保留 viewBox必须
convertShapeToPath: false, // 不转换为 path保持原始形状
removeHiddenElems: false, // 不移除隐藏元素(保留 defs
},
},
},
{
name: 'removeAttrs',
params: {
attrs: '(width|height)', // 只移除 width/height保留所有颜色
},
},
],
}
```
**关键差异:**
-**保留 fill**:不移除颜色属性(传统方案会移除)
-**保留 defs**:保留 clipPath、linearGradient 等定义
-**保留嵌套**:完整保留 `<g>` 嵌套结构
### C. 目录结构完整示例(彩色品牌 Logo
```
packages/ui/
├── icons/ # 彩色 Logo SVG 源文件
│ ├── 302ai.svg
│ ├── aiOnly.svg
│ ├── aihubmix.svg
│ ├── anthropic.svg
│ ├── aws-bedrock.svg
│ ├── baichuan.svg
│ ├── baidu-cloud.svg
│ ├── bailian.svg
│ ├── bytedance.svg
│ ├── cephalon.svg
│ ├── cherryin.svg
│ ├── cohere.svg
│ ├── dashscope.svg
│ ├── deepseek.svg
│ └── ... (40+ 品牌 Logo)
├── scripts/
│ └── generate-icons.ts # 生成脚本(支持嵌套和彩色)
├── src/
│ └── components/
│ └── icons/
│ ├── Icon.tsx # 基础组件(支持嵌套渲染)
│ ├── index.ts # 统一导出
│ └── generated/ # 自动生成的 Logo 组件
│ ├── Ai302.tsx
│ ├── AiOnly.tsx
│ ├── Aihubmix.tsx
│ ├── Anthropic.tsx
│ ├── AwsBedrock.tsx
│ ├── Baichuan.tsx
│ ├── BaiduCloud.tsx
│ ├── Bailian.tsx
│ ├── Bytedance.tsx
│ ├── Cephalon.tsx
│ ├── Cherryin.tsx
│ ├── Cohere.tsx
│ ├── Dashscope.tsx
│ ├── Deepseek.tsx
│ ├── ... (40+ 组件)
│ └── index.ts
└── package.json
```
---
## 总结
本方案借鉴 Lucide 的核心 IconNode 架构,专门为 Cherry Studio UI 库打造了一个轻量级、高性能的**彩色品牌 Logo 图标系统**
### 核心优势
**保留原色**:完整保留品牌 Logo 的所有颜色和细节
**支持嵌套**:处理复杂的 SVG 结构g, clipPath, defs 等)
**自动生成**:一键从 SVG 生成 React 组件,无需手动编写
**高效轻量**IconNode 数据结构,体积小、渲染快
**类型安全**:完整的 TypeScript 支持
**Tailwind 友好**:完美集成 Tailwind CSS
**易于维护**:工厂模式,统一管理,易于扩展
### 与传统方案的区别
| 特性 | 传统线性图标Lucide | 本方案(彩色 Logo |
|------|---------------------|------------------|
| **颜色** | 单色,可动态改变 | 多色,保留原色 |
| **复杂度** | 简单路径 | 支持嵌套结构 |
| **用途** | 通用 UI 图标 | 品牌 Logo 展示 |
| **体积** | ~0.3KB | ~0.5-1.5KB |
### 快速开始
```bash
cd packages/ui
# 1. 安装依赖
pnpm add -D svgo svgson tsx
# 2. 准备 SVG 文件(已有 84 个品牌 Logo
# icons/ 目录已包含: anthropic.svg, deepseek.svg, cohere.svg...
# 3. 生成组件
pnpm generate:icons
# 4. 在代码中使用(推荐使用 /icons 子路径)
# import { Anthropic, Deepseek } from '@cherrystudio/ui/icons';
```
现在就可以在主包中使用彩色品牌 Logo 了!🎉
### 下一步
1. 阅读[使用示例](#使用示例)了解更多用法
2. 查看[常见问题](#常见问题)解决疑惑
3. 将更多品牌 Logo SVG 添加到 `icons/` 目录
4. 运行 `pnpm generate:icons` 生成新组件