【问题】
桌面端(Electron)切换主题时颜色变化速度不一致、明显卡顿。
根因分析(4 轮迭代):
1. 过渡时长不统一(body 0.3s / 组件 0.15s)→ 统一到 0.2s
2. antd CSS-in-JS 标签替换导致瞬间变色 → 尝试 cssVar(无效,仍走标签替换)
3. applyHtmlTheme 在 updater 回调中执行,与 antd CSS 变更不同帧 → 移入 useLayoutEffect
4. html * 全局过渡在 Electron 中为数千 DOM 节点同步插值 → 合成器掉帧
终极方案:View Transitions API(GPU 合成器 1 次 cross-fade 替代 N 个 CSS 过渡)
+ 精准 CSS 过渡覆盖 ~80 个 antd 容器类(回退方案)
【修改】
ThemeProvider.tsx — 核心重构
- applyHtmlTheme 从 setIsDark updater → useLayoutEffect([isDark])
确保自定义 CSS 变量与 antd CSS-in-JS 在同一次 React 提交中生效
- View Transitions API:document.startViewTransition + flushSync
GPU 合成器截取旧/新两帧做单次 cross-fade,消除多元素插值卡顿
- 浏览器不支持时自动回退到 CSS transition 方案
globals.css — 过渡策略
- body: 0.3s ease → 0.15s linear
- View Transitions 动画:::view-transition-old/new(root) 0.15s linear
- ~80 个 antd 容器类精准过渡(Layout/Card/Modal/Table/Input/
Select/Picker/Menu/Tabs/Tag/Alert/Empty/Result/Skeleton 等)
按功能分组:布局 → 表格 → 表单 → 导航 → 反馈
不覆盖交互组件自有 transition(Button/Switch/Slider 等)
linear 替代 ease — 匀速插值,桌面应用更利落
Layout.tsx / HomeContent.tsx / LeftPanel.tsx / ModelSelector.tsx
- 过渡参数对齐:全部 0.2s → 0.15s linear
SettingsPage.tsx — 用户体验
- Modal 取消 keyboard={false},Esc 键可关闭
- macOS:关闭按钮移至标题左侧(遵循 macOS HIG)
- afterClose 中 blur 当前焦点元素,防止聚焦跳到导航栏
【文档】
CHANGELOG.md — 新增 0.0.15 版本记录
CLAUDE.md — 新增主题系统架构章节(双轨颜色体系 / 时序 / 过渡策略)
375 lines
10 KiB
CSS
375 lines
10 KiB
CSS
/* ============================================================
|
||
* 全局样式 —— Tailwind CSS v4 指令 + 蓝紫渐变主题扩展 + 重置样式
|
||
* Tailwind v4 使用 CSS @theme 指令替代 tailwind.config.ts,
|
||
* 设计令牌需与 tokens.ts / antd-theme.ts 保持同步。
|
||
* ============================================================ */
|
||
|
||
@import 'tailwindcss';
|
||
|
||
/* ============================================================
|
||
* Tailwind CSS v4 主题扩展(用 @theme 指令)
|
||
* 此处定义的自定义令牌会自动生成对应的 Tailwind 工具类。
|
||
* 例如:colors.primary → bg-primary, text-primary, border-primary 等。
|
||
* ============================================================ */
|
||
|
||
/* 品牌色 — 蓝紫渐变 */
|
||
@theme {
|
||
/* 主色系 */
|
||
--color-primary: #6366f1; /* Indigo-500 */
|
||
--color-primary-light: #818cf8; /* Indigo-400 */
|
||
--color-primary-dark: #4f46e5; /* Indigo-600 */
|
||
--color-primary-50: #eef2ff; /* Indigo-50 */
|
||
--color-primary-100: #e0e7ff; /* Indigo-100 */
|
||
--color-primary-200: #c7d2fe; /* Indigo-200 */
|
||
--color-primary-300: #a5b4fc; /* Indigo-300 */
|
||
--color-primary-400: #818cf8; /* Indigo-400 */
|
||
--color-primary-500: #6366f1; /* Indigo-500 */
|
||
--color-primary-600: #4f46e5; /* Indigo-600 */
|
||
--color-primary-700: #4338ca; /* Indigo-700 */
|
||
--color-primary-800: #3730a3; /* Indigo-800 */
|
||
--color-primary-900: #312e81; /* Indigo-900 */
|
||
|
||
/* 辅助色系 — Violet */
|
||
--color-secondary: #8b5cf6; /* Violet-500 */
|
||
--color-secondary-light: #a78bfa; /* Violet-400 */
|
||
--color-secondary-dark: #7c3aed; /* Violet-600 */
|
||
|
||
/* 功能色 */
|
||
--color-success: #10b981;
|
||
--color-success-light: #d1fae5;
|
||
--color-warning: #f59e0b;
|
||
--color-warning-light: #fef3c7;
|
||
--color-error: #ef4444;
|
||
--color-error-light: #fee2e2;
|
||
--color-info: #3b82f6;
|
||
--color-info-light: #dbeafe;
|
||
|
||
/* 圆角 */
|
||
--radius-xs: 4px;
|
||
--radius-sm: 6px;
|
||
--radius-base: 8px;
|
||
--radius-lg: 12px;
|
||
--radius-xl: 16px;
|
||
--radius-2xl: 24px;
|
||
|
||
/* 字体 */
|
||
--font-family-base:
|
||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
|
||
'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
--font-family-mono:
|
||
'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, 'Courier New', monospace;
|
||
|
||
/* 动画 */
|
||
--transition-duration: 0.2s;
|
||
--transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
/* ============================================================
|
||
* 🌞 亮色模式 CSS 变量(默认)
|
||
* 这些变量与 tokens.ts 中的亮色令牌同步
|
||
* ============================================================ */
|
||
|
||
:root,
|
||
[data-theme='light'] {
|
||
/* 中性色 — 亮色 */
|
||
--color-text-base: #1e1b4b;
|
||
--color-text-secondary: #6b7280;
|
||
--color-text-tertiary: #9ca3af;
|
||
--color-bg-base: #ffffff;
|
||
--color-bg-container: #f9fafb;
|
||
--color-bg-layout: #f5f3ff;
|
||
--color-bg-elevated: #ffffff;
|
||
--color-border-base: #e5e7eb;
|
||
--color-border-secondary: #f3f4f6;
|
||
|
||
/* 阴影 */
|
||
--shadow-base: 0 1px 3px 0 rgba(99, 102, 241, 0.08), 0 1px 2px -1px rgba(99, 102, 241, 0.08);
|
||
--shadow-lg: 0 10px 15px -3px rgba(99, 102, 241, 0.1), 0 4px 6px -4px rgba(99, 102, 241, 0.1);
|
||
}
|
||
|
||
/* ============================================================
|
||
* 🌙 暗色模式 CSS 变量
|
||
* 当 <html data-theme="dark"> 时自动切换
|
||
* 这些变量与 tokens.ts 中的暗色令牌同步
|
||
* ============================================================ */
|
||
|
||
[data-theme='dark'] {
|
||
/* 中性色 — 暗色(科技蓝紫) */
|
||
--color-text-base: #e8e8fa;
|
||
--color-text-secondary: #a0a8d8;
|
||
--color-text-tertiary: #6e78b0;
|
||
--color-bg-base: #080c24;
|
||
--color-bg-container: #0f1340;
|
||
--color-bg-layout: #050818;
|
||
--color-bg-elevated: #171b52;
|
||
--color-border-base: #2a3278;
|
||
--color-border-secondary: #1c2250;
|
||
|
||
/* 暗色模式下品牌色提亮以保持对比度 */
|
||
--color-primary: #818cf8;
|
||
--color-primary-light: #a5b4fc;
|
||
--color-primary-dark: #6366f1;
|
||
|
||
/* 阴影 — 暗色下用深投影 */
|
||
--shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px -1px rgba(0, 0, 0, 0.3);
|
||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
/* ============================================================
|
||
* 蓝紫渐变工具类
|
||
* Tailwind v4 使用 @utility 定义自定义工具类
|
||
* ============================================================ */
|
||
|
||
@utility gradient-primary {
|
||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||
}
|
||
|
||
@utility gradient-primary-hover {
|
||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||
}
|
||
|
||
@utility gradient-primary-text {
|
||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
@utility gradient-border {
|
||
border-image: linear-gradient(135deg, #4f46e5, #7c3aed) 1;
|
||
}
|
||
|
||
/* ============================================================
|
||
* 全局重置样式
|
||
* 仅放必要的重置,不堆积组件样式
|
||
* ============================================================ */
|
||
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html {
|
||
font-family: var(--font-family-base);
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
text-rendering: optimizeLegibility;
|
||
/* 亮/暗切换过渡 */
|
||
color-scheme: light dark;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
color: var(--color-text-base);
|
||
background-color: var(--color-bg-base);
|
||
min-height: 100vh;
|
||
transition:
|
||
background-color 0.15s linear,
|
||
color 0.15s linear;
|
||
}
|
||
|
||
/* ============================================================
|
||
* View Transitions API — GPU 合成器驱动的主题切换动画
|
||
* 替代 80+ 个独立 CSS 过渡,单次 cross-fade 消除 Electron 卡顿。
|
||
* 仅在 Chromium 111+(Electron 28+)生效,其他浏览器回退 CSS 过渡。
|
||
* ============================================================ */
|
||
|
||
::view-transition-old(root),
|
||
::view-transition-new(root) {
|
||
animation-duration: 0.15s;
|
||
animation-timing-function: linear;
|
||
}
|
||
|
||
/* ============================================================
|
||
* 主题切换过渡 — 精准覆盖大面积视觉容器,避免 * 全局选择器
|
||
* 在 Electron 中为数千个 DOM 节点同时启动过渡造成卡顿。
|
||
*
|
||
* 策略:
|
||
* 1. 容器/布局组件(Card / Modal / Layout / Drawer 等)
|
||
* 2. 表格 & 列表(Table / List / Timeline)
|
||
* 3. 表单控件(Input / Select / Picker)
|
||
* 4. 导航 & 标签(Menu / Tabs / Tag / Breadcrumb)
|
||
* 5. 反馈 & 展示(Alert / Empty / Result / Statistic / Skeleton)
|
||
*
|
||
* 不覆盖交互组件自有 transition(Button / Switch / Slider 等
|
||
* 均使用 transition: all,特异性更高不受影响)。
|
||
* ============================================================ */
|
||
|
||
/* —— 布局 & 容器 —— */
|
||
body,
|
||
.ant-layout,
|
||
.ant-layout-header,
|
||
.ant-layout-sider,
|
||
.ant-layout-content,
|
||
.ant-card,
|
||
.ant-card-head,
|
||
.ant-card-body,
|
||
.ant-card-actions,
|
||
.ant-modal-content,
|
||
.ant-modal-header,
|
||
.ant-modal-body,
|
||
.ant-modal-footer,
|
||
.ant-drawer-content,
|
||
.ant-drawer-header,
|
||
.ant-drawer-body,
|
||
.ant-drawer-footer,
|
||
|
||
/* —— 表格 & 列表 —— */
|
||
.ant-table,
|
||
.ant-table-thead > tr > th,
|
||
.ant-table-tbody > tr > td,
|
||
.ant-table-tbody > tr:hover > td,
|
||
.ant-list,
|
||
.ant-list-item,
|
||
.ant-timeline-item,
|
||
.ant-transfer-list,
|
||
.ant-tree-node-content-wrapper,
|
||
|
||
/* —— 表单控件 —— */
|
||
.ant-input,
|
||
.ant-input-affix-wrapper,
|
||
.ant-select-selector,
|
||
.ant-select-dropdown,
|
||
.ant-picker,
|
||
.ant-picker-input,
|
||
.ant-picker-dropdown,
|
||
.ant-picker-panel-container,
|
||
.ant-input-number,
|
||
.ant-input-number-input,
|
||
.ant-radio-button-wrapper,
|
||
.ant-radio-group,
|
||
.ant-checkbox-wrapper,
|
||
.ant-segmented,
|
||
.ant-segmented-item,
|
||
.ant-upload-drag,
|
||
.ant-upload-list-item,
|
||
|
||
/* —— 导航 & 标签 —— */
|
||
.ant-menu,
|
||
.ant-menu-item,
|
||
.ant-menu-submenu-title,
|
||
.ant-tabs-nav,
|
||
.ant-tabs-nav-list,
|
||
.ant-tabs-tab,
|
||
.ant-tabs-content,
|
||
.ant-tag,
|
||
.ant-breadcrumb,
|
||
.ant-pagination-item,
|
||
.ant-dropdown-menu,
|
||
|
||
/* —— 反馈 & 展示 —— */
|
||
.ant-alert,
|
||
.ant-empty,
|
||
.ant-result,
|
||
.ant-statistic,
|
||
.ant-statistic-content,
|
||
.ant-badge,
|
||
.ant-avatar,
|
||
.ant-divider,
|
||
.ant-skeleton,
|
||
.ant-skeleton-input,
|
||
.ant-skeleton-button,
|
||
.ant-notification-notice,
|
||
.ant-message-notice-content,
|
||
.ant-popover-inner,
|
||
.ant-tooltip-inner,
|
||
.ant-collapse,
|
||
.ant-collapse-item,
|
||
.ant-collapse-header,
|
||
.ant-collapse-content-box,
|
||
.ant-descriptions-item-container {
|
||
transition:
|
||
color 0.15s linear,
|
||
background-color 0.15s linear,
|
||
border-color 0.15s linear,
|
||
box-shadow 0.15s linear;
|
||
}
|
||
|
||
#root {
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* 滚动条美化 */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #c7d2fe;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #a5b4fc;
|
||
}
|
||
|
||
/* 暗色模式滚动条 */
|
||
[data-theme='dark'] ::-webkit-scrollbar-thumb {
|
||
background: #4338ca;
|
||
}
|
||
|
||
[data-theme='dark'] ::-webkit-scrollbar-thumb:hover {
|
||
background: #4f46e5;
|
||
}
|
||
|
||
/* 聚焦环 — 使用品牌色 */
|
||
:focus-visible {
|
||
outline: 2px solid var(--color-primary);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* 选中文本 — 使用品牌色 */
|
||
::selection {
|
||
background: rgba(99, 102, 241, 0.2);
|
||
color: var(--color-text-base);
|
||
}
|
||
|
||
[data-theme='dark'] ::selection {
|
||
background: rgba(129, 140, 248, 0.3);
|
||
color: var(--color-text-base);
|
||
}
|
||
|
||
/* 链接全局样式 */
|
||
a {
|
||
color: var(--color-primary);
|
||
text-decoration: none;
|
||
transition: color var(--transition-duration) var(--transition-timing);
|
||
}
|
||
|
||
a:hover {
|
||
color: var(--color-primary-light);
|
||
}
|
||
|
||
/* ============================================================
|
||
* 窗口拖拽区域(自定义导航栏)
|
||
*
|
||
* macOS hiddenInset 模式下,整个导航栏区域可拖拽移动窗口。
|
||
* .navbar-drag-region → 可拖拽(背景区域)
|
||
* .navbar-no-drag → 不可拖拽(按钮、链接等交互元素)
|
||
* ============================================================ */
|
||
|
||
.navbar-drag-region {
|
||
-webkit-app-region: drag;
|
||
user-select: none;
|
||
}
|
||
|
||
.navbar-no-drag {
|
||
-webkit-app-region: no-drag;
|
||
}
|
||
|
||
/* macOS 红绿灯按钮区域不遮挡内容 */
|
||
@supports (-webkit-app-region: drag) {
|
||
.navbar-drag-region {
|
||
-webkit-app-region: drag;
|
||
}
|
||
}
|