feat: implement theme selection and system integration

This commit is contained in:
sagitchu
2026-03-20 22:47:49 +08:00
parent 9f0670f4d0
commit bd4e1f66cb
17 changed files with 1834 additions and 105 deletions
+382
View File
@@ -0,0 +1,382 @@
# FLVX 主题开发指南
## 概述
FLVX 主题系统允许你完全自定义前端的外观和行为。一个主题可以覆盖:
| 覆盖层级 | 说明 | 难度 |
|----------|------|------|
| **CSS 变量** | 修改颜色、字体、圆角等设计 token | ⭐ 简单 |
| **原始 CSS** | 注入自定义 CSS(动画、字体、阴影等) | ⭐⭐ 中等 |
| **组件替换** | 替换任意 UI 组件(按钮、卡片、输入框等) | ⭐⭐⭐ 高级 |
| **布局替换** | 替换整个页面布局结构 | ⭐⭐⭐ 高级 |
| **页面替换** | 替换整个页面实现 | ⭐⭐⭐⭐ 专家 |
## 快速开始
### 1. 创建主题文件夹
```
src/themes/my-theme/
├── index.ts ← 必须:主题入口,导出 ThemePackage
├── components/ ← 可选:组件覆盖
│ ├── index.ts
│ └── button.tsx
├── layouts/ ← 可选:布局覆盖
│ └── admin.tsx
├── pages/ ← 可选:页面覆盖
│ └── login.tsx
├── assets/ ← 可选:图片、字体等资源
└── styles.css ← 可选:额外样式文件
```
### 2. 编写主题入口 `index.ts`
```typescript
import type { ThemePackage } from "../types";
const myTheme: ThemePackage = {
id: "my-theme", // 唯一标识(kebab-case)
name: "我的主题", // 显示名称
author: "Your Name", // 作者
version: "1.0.0", // 版本号
description: "一个自定义主题",
// CSS 变量覆盖
tokens: {
light: {
"--primary": "#ff6600",
"--primary-foreground": "#ffffff",
"--background": "#fafafa",
},
dark: {
"--primary": "#ff8833",
"--background": "#1a1a2e",
},
},
};
export default myTheme;
```
### 3. 注册主题
打开 `src/themes/loader.ts`,添加两行:
```typescript
import myTheme from "./my-theme";
registerTheme(myTheme);
```
完成!主题已可用。
---
## 详细指南
### CSS 变量覆盖
所有可用的 CSS 变量定义在 `src/themes/types.ts` 的 `ThemeTokens` 接口中。常用的:
```typescript
tokens: {
light: {
// 基础色
"--background": "#ffffff", // 页面背景
"--foreground": "#000000", // 文字颜色
"--border": "#e5e7eb", // 边框颜色
"--content1": "#ffffff", // 卡片背景
// 品牌色
"--primary": "#2563eb", // 主色
"--primary-foreground": "#fff", // 主色上的文字
"--secondary": "#6366f1", // 辅色
// 状态色
"--danger": "#dc2626",
"--success": "#16a34a",
"--warning": "#d97706",
// 每种品牌色都有 50-900 共 10 级色阶
"--primary-50": "#eff6ff", // 最浅
"--primary-500": "#3b82f6", // 中间
"--primary-900": "#1e3a8a", // 最深
// 字体
"--font-sans": '"Inter", sans-serif',
"--font-mono": '"Fira Code", monospace',
// 圆角
"--radius": "0.5rem",
},
dark: {
// 暗色模式下的覆盖...
},
}
```
> **提示**: 你不需要定义所有变量,只定义你想修改的,其余沿用默认值。
### 原始 CSS 注入
`css` 字段可以注入任意 CSS。主题激活时会插入一个 `<style>` 标签,停用时自动移除。
```typescript
const theme: ThemePackage = {
// ...
css: `
/* 自定义字体 */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap');
body {
font-family: 'Noto Sans SC', sans-serif;
}
/* 自定义动画 */
@keyframes my-fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 给所有卡片加阴影 */
.rounded-xl, .rounded-lg {
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
/* 自定义滚动条 */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 4px;
}
`,
};
```
### 组件替换
可以替换任意 UI 组件。替换组件**必须接受与原组件相同的 props**。
#### 可替换的组件列表
| 组件键名 | 原始位置 | 说明 |
|----------|----------|------|
| `Button` | `shadcn-bridge/heroui/button` | 按钮 |
| `Card`, `CardHeader`, `CardBody`, `CardFooter` | `shadcn-bridge/heroui/card` | 卡片 |
| `Input` | `shadcn-bridge/heroui/input` | 输入框 |
| `Select`, `SelectItem` | `shadcn-bridge/heroui/select` | 下拉选择 |
| `Switch` | `shadcn-bridge/heroui/switch` | 开关 |
| `Checkbox` | `shadcn-bridge/heroui/checkbox` | 复选框 |
| `Chip` | `shadcn-bridge/heroui/chip` | 标签/芯片 |
| `Modal`, `ModalContent`, `ModalHeader`, `ModalBody`, `ModalFooter` | `shadcn-bridge/heroui/modal` | 模态框 |
| `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableCell`, `TableColumn` | `shadcn-bridge/heroui/table` | 表格 |
| `Tabs`, `Tab` | `shadcn-bridge/heroui/tabs` | 标签页 |
| `Progress` | `shadcn-bridge/heroui/progress` | 进度条 |
| `Spinner` | `shadcn-bridge/heroui/spinner` | 加载指示器 |
| `Divider` | `shadcn-bridge/heroui/divider` | 分割线 |
| `Link` | `shadcn-bridge/heroui/link` | 链接 |
| `Dropdown`, `DropdownTrigger`, `DropdownMenu`, `DropdownItem` | `shadcn-bridge/heroui/dropdown` | 下拉菜单 |
| `Navbar`, `NavbarContent`, `NavbarItem` | `shadcn-bridge/heroui/navbar` | 导航栏 |
| `Radio`, `RadioGroup` | `shadcn-bridge/heroui/radio` | 单选框 |
| `Accordion`, `AccordionItem` | `shadcn-bridge/heroui/accordion` | 手风琴 |
| `DatePicker` | `shadcn-bridge/heroui/date-picker` | 日期选择器 |
| `Alert` | `shadcn-bridge/heroui/alert` | 警告提示 |
| `SearchBar` | `components/search-bar` | 搜索栏 |
| `BrandLogo` | `components/brand-logo` | 品牌 Logo |
| `VersionFooter` | `components/version-footer` | 版本页脚 |
#### 组件替换示例
**方式一:包装原组件**(推荐,保证兼容性)
```typescript
// src/themes/my-theme/components/button.tsx
import React from "react";
import type { ButtonProps } from "@/shadcn-bridge/heroui/button";
import { Button as OriginalButton } from "@/shadcn-bridge/heroui/button";
export const MyButton: React.FC<ButtonProps> = (props) => {
return (
<OriginalButton
{...props}
className={`${props.className || ""} my-custom-class`}
style={{
...props.style,
borderRadius: "9999px", // 全圆角
}}
/>
);
};
```
**方式二:完全重写组件**
```typescript
// src/themes/my-theme/components/button.tsx
import React from "react";
import type { ButtonProps } from "@/shadcn-bridge/heroui/button";
export const MyButton: React.FC<ButtonProps> = ({
children,
color = "default",
variant = "solid",
size = "md",
isLoading,
isDisabled,
onPress,
className,
...rest
}) => {
return (
<button
className={`my-totally-custom-button ${className || ""}`}
disabled={isDisabled || isLoading}
onClick={() => onPress?.()}
{...rest}
>
{isLoading && <span className="spinner" />}
{children}
</button>
);
};
```
然后在主题入口中注册:
```typescript
// src/themes/my-theme/index.ts
import { MyButton } from "./components/button";
const theme: ThemePackage = {
// ...
components: {
Button: MyButton,
},
};
```
### 布局替换
可以替换 4 种布局:
| 布局键名 | 说明 |
|----------|------|
| `AdminLayout` | 管理后台主布局(侧边栏 + 顶栏) |
| `H5Layout` | 移动端布局(底部导航) |
| `H5SimpleLayout` | 移动端简洁布局(无底部导航) |
| `DefaultLayout` | 默认布局(登录页等) |
```typescript
// src/themes/my-theme/layouts/admin.tsx
import React from "react";
const MyAdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div className="my-admin-layout">
<header className="my-header">
{/* 自定义顶栏 */}
</header>
<aside className="my-sidebar">
{/* 自定义侧边栏 */}
</aside>
<main className="my-content">
{children}
</main>
</div>
);
};
export default MyAdminLayout;
```
### 页面替换
可以替换任意页面路由的实现:
| 页面键名 | 路由 |
|----------|------|
| `LoginPage` | `/` |
| `DashboardPage` | `/dashboard` |
| `MonitorPage` | `/monitor` |
| `ForwardPage` | `/forward` |
| `TunnelPage` | `/tunnel` |
| `NodePage` | `/node` |
| `UserPage` | `/user` |
| `GroupPage` | `/group` |
| `ProfilePage` | `/profile` |
| `LimitPage` | `/limit` |
| `ConfigPage` | `/config` |
| `PanelSharingPage` | `/panel-sharing` |
| `SettingsPage` | `/settings` |
### 生命周期钩子
```typescript
const theme: ThemePackage = {
// ...
onActivate: () => {
// 主题被激活时执行
// 例如:加载外部字体、注入全局属性
console.log("Theme activated!");
},
onDeactivate: () => {
// 主题被停用时执行
// 例如:清理全局属性
console.log("Theme deactivated!");
},
};
```
---
## 主题提交流程
1. Fork 本仓库
2. 在 `src/themes/` 下创建你的主题文件夹
3. 在 `src/themes/loader.ts` 中注册
4. 提交 Pull Request
### 命名规范
- 文件夹名:`kebab-case`(如 `my-awesome-theme`)
- 主题 `id`:与文件夹名一致
- 主题 `name`:简短中文名
### 代码规范
- TypeScript 严格模式
- 组件替换必须保证 props 兼容性
- 不得修改 `src/themes/types.ts`(影响其他主题)
- 不得修改 `src/themes/registry.ts`(影响核心逻辑)
- 仅修改你自己的主题文件夹 + `loader.ts` 中的注册
---
## 文件结构参考
```
src/themes/
├── types.ts # 主题接口定义 (勿改)
├── registry.ts # 主题注册表 (勿改)
├── context.tsx # React Context (勿改)
├── index.ts # 公共 API (勿改)
├── loader.ts # 主题加载器 (仅在此添加注册)
│
├── default/ # 默认主题 (参考实现)
│ └── index.ts
│
├── example-cyberpunk/ # 示例主题 (可复制修改)
│ ├── index.ts
│ └── components/
│ └── button.tsx
│
└── your-theme/ # 你的主题
├── index.ts
├── components/
│ ├── button.tsx
│ └── card.tsx
├── layouts/
│ └── admin.tsx
└── assets/
└── logo.svg
```