一个 AI 创作平台的前端从旧版原生 HTML 迁移到了 Vue3 + Vite + TypeScript + Element Plus + Pinia + Tailwind CSS。项目当前约 121 个 Vue SFC、95 个 TS 文件、8 个 CSS 模块,合计约 5.2 万行。这里记录一下主题体系和组件化重构的思路。
一、设计令牌(Design Tokens)
所有颜色、阴影、模糊、边框统一收敛到 CSS 自定义属性,通过主题变量控制全局外观。index.css 定义了完整的令牌体系:
1 | :root { |
Element Plus 的主题色通过赋值给 --el-color-primary 来统一,避免第三方组件和自定义组件颜色不一致。
二、Liquid Glass 面板组件
核心面板组件 LiquidGlassPanel,承载透明玻璃质感的容器:
1 | <script setup lang="ts"> |
用 ::before 伪元素做顶部高光、backdrop-filter 做背景模糊、color-mix 做半透明混合,三件套实现玻璃效果。interactive 模式下悬停会浮起并向品牌色靠近。
三、Liquid Glass Select 组件
Element Plus 原生的 ElSelect 在玻璃背景下有对比度问题、弹层被裁切、长模型名显示不佳。自己写了一个 LiquidGlassSelect:
1 | <script setup lang="ts"> |
关键设计决策:
- Teleport 到 body:弹层不再受父容器
overflow: hidden/backdrop-filter/fixed影响 - 自适应位置:优先向下弹出,空间不足则向上;水平方向约束在 viewport 内
- 键盘导航:支持
ArrowDown/Up/Enter/Space/Escape - ARIA 属性:
role="listbox"/aria-expanded/aria-selected
四、模块化样式治理
项目样式按模块拆分到独立的 CSS 文件中,由 index.css 统一引入:
1 | /* src/styles/index.css */ |
每个模块 CSS 使用 Tailwind 的 @layer components 包装,示例来自 center.css:
1 | @layer components { |
而 Liquid Glass 的原生效果(color-mix、backdrop-filter、多层渐变、变量阴影)保持在原生 CSS 中,避免 Tailwind 无法表达的玻璃质感在 Safari/iOS 下降级。
五、页面级组织:以 AI 创作为例
AiCreatePage.vue 目前约 1489 行,拆成了多个层级:
页面壳层:
1 | <script setup lang="ts"> |
组件层级(全部从页面壳拆出):
1 | AiCreatePage.vue |
Composable 层级(每个 composable 负责一个独立关注点):
1 | useCosUpload.ts —— COS 上传 |
工具层(纯函数,无副作用):
1 | aiTaskCards.ts —— 本地任务、远程作品、长视频项目合并去重 |
六、Pinia 持久化策略
使用 pinia-plugin-persistedstate 做持久化,但不是所有状态都需要持久化。只持久化跨刷新必要的状态:
creationTasks store:
1 | const TASKS_KEY = 'ai_create_tasks_v1' |
- 持久化:任务卡、参考素材、输入草稿、模型偏好
- 不持久化:用户余额、模型配置、账单——这些后端可恢复且易过期
auth store:
- 持久化
token/refreshToken,底层 key 兼容旧代码 - 登录态刷新/过期不再用
window自定义事件,改用类型化信号和useAuthSessionBridge
七、路由结构
1 | const routes = [ |
所有页面使用懒加载 () => import(...),MainLayout 和 FullscreenLayout 两套布局壳。
八、核心约定
- 样式先拆到模块 CSS:
src/styles/<module>.css,使用@layer components包装 - Liquid Glass 原生效果保持原生 CSS:
color-mix/backdrop-filter/ 玻璃阴影不用 Tailwind - 一个 composable 一个关注点:不把图片参数和视频轮询混在一个 composable 里
- 持久化最小化:只存跨刷新必要的状态,其余后端可恢复
- 组件 Teleport:弹层/下拉框 Teleport 到 body,避免被父容器裁切
- 无
as any、无生产console.log:全量扫描确认过