feat: 全面UI优化 — 5套主题系统 + 设计体系 + 菜单重组 + 页面优化

1. 主题系统扩展 (5套主题)
   - ocean-deep(深海蓝) / light-professional(明亮白) / midnight-purple(午夜紫)
   - pine-green(松林绿) / orange-vitality(活力橙)
   - useTheme hook 重写支持5主题 + 向后兼容
   - AppearanceSettings 5主题预览卡片

2. 设计体系 (index.css)
   - 按钮系统: dg-btn-primary/secondary/ghost/danger + 尺寸变体
   - 徽章系统: dg-badge-* (8色)
   - 卡片/表格: dg-card-section, dg-table-row, dg-table-header
   - 状态点: dg-status-dot-* (4色)
   - 动效关键帧: slideInLeft, scaleIn

3. 菜单重组 (menu.ts)
   - 5分组: 概览/数据源/工单/权限/系统
   - 6个图标更新: Code2/FileEdit/ShieldCheck/FolderLock/ScrollText/Timer
   - 新增 permission 权限码字段

4. Layout 侧边栏增强
   - 左侧渐变指示条 + 发光效果
   - hover 微动画 + 图标变色
   - 面包屑导航头部
   - 主题切换循环5主题

5. 核心页面优化
   - TicketsPage: 设计系统类 + 颜色对齐
   - QueriesPage: 去Card依赖 + dg-table/dg-btn统一
   - Breadcrumb组件 + useBreadcrumb hook
This commit is contained in:
logan committed 2026-05-27 01:59:08 +08:00
1 parent 4e2df4d3d7
commit 746a000e8a
25 files changed
+7717 -177

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
# SQL Platform 全面评估与优化方案 - 执行计划
## 阶段 1: 代码库克隆与结构分析
- 克隆代码库 `https://gitee.com/attacker/sql-platform.git`
- 分析项目结构(前后端技术栈、目录组织)
- 阅读 docs 目录下所有文档
## 阶段 2: 前端代码深入分析
- 分析5个登录页面的实现和现有设计
- 分析登录后页面主题系统
- 分析菜单结构和路由配置
- 重点分析核心功能:审批变更、查询数据、系统设置的实现
## 阶段 3: 问题评估与方案设计
- 加载 deep-research-swarm 技能,进行UI/UX最佳实践调研
- 加载 report-writing 技能,撰写评估报告
- 设计5个专业登录页风格方案
- 设计5个登录后主题风格方案
- 设计核心功能页面优化方案
## 阶段 4: 输出优化方案文档
- 输出完整的评估报告(.docx格式)
- 包含:现状分析、问题清单、5套登录页设计方案、5套主题设计方案、核心功能优化方案、实施路线图
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,78 @@
# SQL-Platform 全面评估与UI/UX优化方案
## 1. 项目概况与技术架构评估
- 1.1 项目定位与核心功能
- 1.2 技术栈分析(React 19 + Go/Gin)
- 1.3 前端架构现状评估
- 1.4 现有UI设计体系梳理
## 2. 现状问题诊断
- 2.1 登录页面五大问题分析
- 2.1.1 视觉层次薄弱
- 2.1.2 交互体验不足
- 2.1.3 品牌感缺失
- 2.1.4 组件化程度低
- 2.1.5 动效与反馈匮乏
- 2.2 主题系统局限性
- 2.3 核心功能页面问题
- 2.3.1 审批变更工单系统
- 2.3.2 查询数据页面
- 2.3.3 系统设置页面
- 2.4 菜单体系问题
- 2.5 设计体系不一致性
## 3. 五套专业登录页设计方案
- 3.1 方案一「极光科技」— 动态粒子科技风
- 3.2 方案二「企业金融」— 银行级安全可信风
- 3.3 方案三「云端数据」— 云计算SaaS风
- 3.4 方案四「极简矩阵」— 开发者极客风
- 3.5 方案五「自然流动」— 有机渐变自然风
- 3.6 各方案组件设计要点
- 3.7 各方案动效设计规范
## 4. 五套登录后主题设计方案
- 4.1 主题一「深蓝专业」— 默认深色增强版
- 4.2 主题二「浅色商务」— 企业办公风
- 4.3 主题三「午夜紫韵」— 高端紫色调
- 4.4 主题四「松绿自然」— 护眼绿调
- 4.5 主题五「橙光活力」— 活力暖色调
- 4.6 各主题CSS变量规范
- 4.7 主题切换实现方案
## 5. 核心功能页面优化方案
- 5.1 审批变更工单系统优化
- 5.1.1 工单列表页优化
- 5.1.2 工单创建页优化
- 5.1.3 工单详情页优化
- 5.1.4 审批流程可视化优化
- 5.2 查询数据页面优化
- 5.2.1 SQL编辑器优化
- 5.2.2 结果展示优化
- 5.2.3 侧边栏导航优化
- 5.2.4 历史记录优化
- 5.3 系统设置页面优化
- 5.3.1 设置分类重组
- 5.3.2 表单交互优化
- 5.3.3 安全设置增强
- 5.3.4 外观设置增强
## 6. 菜单体系全面优化
- 6.1 菜单结构重组方案
- 6.2 图标体系规范化
- 6.3 权限映射优化
- 6.4 面包屑导航补充
## 7. 设计体系统一方案
- 7.1 色彩体系规范
- 7.2 字体排版规范
- 7.3 间距体系规范
- 7.4 组件交互规范
- 7.5 动效体系规范
## 8. 实施路线图
- 8.1 优先级划分
- 8.2 阶段一:登录页改造(2周)
- 8.3 阶段二:主题系统扩展(1周)
- 8.4 阶段三:核心功能优化(3周)
- 8.5 阶段四:菜单与体系统一(1周)
- 8.6 预期效果
@@ -0,0 +1,237 @@
# 1. 项目概况与技术架构评估
## 1.1 项目定位与核心功能
### 1.1.1 平台定位
SQL-Platform 是一款面向企业内部数据操作的**自助查询与数据变更审批流平台**,其核心定位是为开发、运维、数据分析等角色提供安全可控的数据库操作入口。平台覆盖 MySQL、PostgreSQL、Redis、MongoDB 四种主流数据库类型,通过工单驱动的审批机制,在操作便捷性与数据安全性之间建立平衡。
该平台在生态位上直接对标 **Yearning**(Go + Vue 技术栈,侧重 MySQL 审计)与 **Archery**(Python + Vue 技术栈,支持多数据库),同时吸收了 **Bytebase** 在工单审批流程设计上的先进理念。相较于竞品,本项目采用 React 19 + Go/Gin 的现代化全栈组合,在前后端解耦程度、UI 可定制性以及开发者体验(DX)方面具备差异化竞争力。
### 1.1.2 核心功能模块
平台功能围绕"**数据操作全生命周期管理**"展开,可划分为五大核心域:
| 功能域 | 核心能力 | 对应菜单 |
|--------|----------|----------|
| 数据操作 | SQL 自助查询(支持 CodeMirror 6 编辑器)、数据变更提交 | 查询数据、数据变更 |
| 审批工作流 | 工单创建 → 审批节点流转 → 执行/驳回 → 归档 | 工单管理 |
| 数据源治理 | 多类型数据库实例注册、连接池管理、Schema 浏览 | 数据源管理 |
| 权限管控 | RBAC 角色权限模型、权限组划分、用户生命周期管理 | 权限组、角色权限、用户管理 |
| 审计与合规 | 操作日志全量记录、审批流程自定义、自动任务调度 | 审计日志、审批流程、自动任务 |
### 1.1.3 对标产品分析
| 维度 | SQL-Platform | Yearning | Archery | Bytebase |
|------|-------------|----------|---------|----------|
| 后端技术 | Go/Gin | Go/Beego | Python/Django | Go |
| 前端技术 | React 19 + TS | Vue 2 | Vue 2 | Vue 3 |
| 支持数据库 | MySQL/PgSQL/Redis/MongoDB | MySQL | 多数据库 | 全品类 |
| UI 组件体系 | shadcn/ui + Tailwind | Element UI | Element UI | 自研组件 |
| 编辑器 | CodeMirror 6 | ACE Editor | ACE Editor | 自研 Monaco |
| 代码开源度 | 全开源 | 开源核心 | 全开源 | 开源核心 |
相较 Yearning,本项目在多数据库支持广度与前端技术栈现代化程度上具有明显优势;相较 Archery,Go 后端的性能表现与部署便捷性更为突出。当前主要差距集中在**工单审批流程的可视化设计**与**数据库变更的 Schema 版本管理(VCS 集成)**方面,这两项可作为后续版本的重点演进方向。
---
## 1.2 技术栈分析
### 1.2.1 前后端技术选型评估
**后端架构**
后端采用 **Go 1.26 + Gin v1.10.1** 的组合,Gin 作为目前 Go 生态中 Stars 数最高(>80k)的 Web 框架,其高性能路由与极简的 Middleware 设计非常适合 API 密集型企业应用。ORM 层选用 **GORM v1.26.0**,借助其 AutoMigrate、关联预加载与插件扩展能力,可显著降低数据库操作层的样板代码量。元数据库采用 **SQLite**(通过 `gorm.io/driver/sqlite` 驱动),这一设计决策使得平台在部署时**零依赖外部数据库**,极大降低了私有化部署的门槛——这对于中小团队或对运维复杂度敏感的企业极具吸引力。
认证层采用 **JWT HS256 + bcrypt** 方案,Token 默认 24h 过期,在安全性与用户体验之间取得合理平衡。RBAC 权限中间件的独立设计体现了安全架构的前置思维,建议后续版本考虑引入 Refresh Token 机制以降低长期会话的泄露风险。
**前端架构**
前端基于 **React 19 + TypeScript**,构建工具采用 **Vite 7**,这一组合代表了 2024-2025 年 React 生态的技术前沿。React 19 的 Server Components 支持(尽管当前项目以 CSR 为主)、Compiler 自动优化以及改进的 Suspense 边界处理,为后续性能演进预留了充足空间。Vite 7 相较于 Webpack 在冷启动速度(提升约 10-20 倍)与 HMR 响应速度上具备代际优势,显著改善开发体验。
UI 层采用 **shadcn/ui + Tailwind CSS 4 + Radix UI** 的三层架构:Radix UI 提供无样式、无障碍(A11y)完备的底层原语;Tailwind CSS 4 提供原子化样式能力;shadcn/ui 则在此基础上封装了一套设计一致、可高度定制的高阶组件。这种"无头组件 + 原子样式 + 组件封装"的分层模式,是当前前端 UI 架构的最佳实践之一。
代码编辑器选用 **CodeMirror 6**(`@codemirror/lang-sql` 语言包),相较于 Monaco Editor,CodeMirror 6 在包体积(Tree-shakeable 后约 100KB vs Monaco 的 1MB+)、移动端适配以及主题定制灵活性上表现更优,非常适合本项目这种以 SQL 编辑为核心但非纯 IDE 场景的应用。
图表层采用 **Recharts**,作为 React 生态中最成熟的 D3 封装库,其与 React 组件模型的深度整合使得在仪表盘页面中构建数据可视化变得直观高效。
### 1.2.2 技术栈优势分析
1. **部署零依赖**:SQLite 作为元数据库 + Go 单二进制部署,使得整个平台可以编译为单一可执行文件分发,运维成本极低。
2. **类型安全贯穿全栈**:TypeScript(前端)+ Go(后端)的双重静态类型保障,配合 GORM 的模型定义与前端接口类型的共享(可通过 OpenAPI/Swagger 代码生成进一步强化),可显著降低因类型不匹配导致的运行时错误。
3. **组件体系可扩展性强**:shadcn/ui 的组件代码直接存在于项目源码中(非 npm 黑盒),团队可根据业务需求任意修改组件内部实现而不受上游版本锁定。
4. **性能基线优秀**:Go 的 goroutine 并发模型 + Gin 的高性能路由 + Vite 的快速构建 + React.lazy 代码分割,从后端到前端形成了一条完整的性能优化链路。
### 1.2.3 潜在技术债务
| 风险点 | 影响等级 | 说明与建议 |
|--------|----------|-----------|
| GORM v1.26.0 版本锁定 | 中 | GORM v2 与 v1 存在 API 不兼容变更,长期停留在 v1 会阻碍安全补丁获取。建议规划迁移至 GORM v2。 |
| SQLite 并发写入瓶颈 | 中 | SQLite 在 WAL 模式下可支持多读单写,但在高并发工单场景下可能成为瓶颈。建议评估是否需要支持 PostgreSQL/MySQL 作为可选元数据库。 |
| React 19 生态适配 | 低-中 | React 19 尚属较新版本,部分第三方库可能存在兼容性问题。建议建立依赖版本锁定策略,并持续跟踪关键依赖的 React 19 适配进度。 |
| JWT 无 Refresh Token | 中 | 24h 固定过期时间导致用户需要频繁重新登录,影响体验。建议引入 Refresh Token + Access Token 双 Token 机制。 |
| 缺少 API 文档自动化 | 低 | 后端 120+ 路由缺乏自动化文档输出。建议集成 Swagger/OpenAPI 自动生成,降低前后端协作成本。 |
---
## 1.3 前端架构现状评估
### 1.3.1 页面结构与路由设计
项目当前包含 **24+ 页面**,覆盖平台五大功能域,页面层级设计遵循"功能域 → 功能页 → 操作面板"的三层信息架构原则:
```
概览
└── 仪表盘(Dashboard)— 运营指标总览
数据源
└── 数据源管理(Instances)— CRUD + 连接测试
工单管理
├── 数据变更(Tickets/Change)— 变更工单列表 + 审批流
└── 查询数据(Tickets/Query)— 查询工单列表 + SQL 执行
权限管理
├── 权限组(PermissionGroups)— 权限集合管理
├── 角色权限(Roles)— RBAC 角色定义
└── 用户管理(Users)— 用户生命周期管理
系统配置
├── 审计日志(AuditLogs)— 操作记录检索
├── 审批流程(ApprovalFlows)— 审批节点自定义
└── 自动任务(ScheduledTasks)— 定时任务管理
配置平台
└── 系统设置(Settings)— 全局参数配置
```
路由设计层面,项目已采用 **React.lazy 进行代码分割**,这是一个重要的性能优化信号——通过将各功能域拆分为独立的异步 Chunk,可显著降低首屏加载时间。建议进一步结合 React Router v6 的 `loader` 与 `action` API 实现路由级的数据预取,减少页面切换时的感知延迟。
平台提供 **5 种登录页主题**(SplitView / CenteredCard / SidePanel / FullImage / MinimalDark),这一设计体现了对部署场景多样性的考虑——不同企业可根据自身品牌调性选择最适合的登录体验。从维护性角度,建议将这 5 种主题抽象为统一的 "LoginLayout" 组件,通过配置对象驱动渲染,减少重复逻辑。
### 1.3.2 组件化程度分析
项目组件体系可分为三个层级,整体呈现良好的分层架构:
| 层级 | 组件数量 | 代表组件 | 职责说明 |
|------|----------|----------|----------|
| 基础组件 | 50+ | Button, Input, Dialog, Table, Dropdown | shadcn/ui 封装,平台无关,可复用性最高 |
| 业务组件 | 9 | Layout, SqlEditor, SchemaSidebar, EditInstanceDialog | 领域特定,封装业务逻辑与交互模式 |
| 页面组件 | 24+ | Dashboard, Instances, Tickets, Settings | 路由级入口,组合业务组件完成特定功能 |
**值得肯定的方面**:
- 自定义组件的命名遵循 PascalCase + 语义化约定(如 `SchemaSidebar`、`EditInstanceDialog`),可读性良好。
- `SqlEditor` 组件作为核心交互组件被独立封装,基于 CodeMirror 6 提供了 SQL 语法高亮、关键词补全等能力,是可复用性最高的业务资产之一。
- `Layout` 组件的统一设计确保了全站导航结构的一致性。
**建议改进的方向**:
- 业务组件层当前仅有 9 个,相对于 24+ 页面的规模略显单薄,存在页面间逻辑重复的可能性。建议对高频交互模式(如表单创建/编辑、列表筛选/分页、审批操作确认等)进行进一步抽象。
- 建议建立组件文档体系(如 Storybook),记录各组件的 Props 接口、使用示例与视觉规格,降低团队协作成本。
### 1.3.3 状态管理方案
基于代码架构分析,项目当前的状态管理呈现 **"React 内置状态 + 局部上下文"** 的轻量级模式。对于 SQL-Platform 这类以表单操作和列表管理为核心的应用,这种状态管理策略是合理且务实的:
- **服务端状态**(工单列表、实例配置、用户数据等):通过 API 调用获取,建议引入 **TanStack Query (React Query)** 进行封装,可自动处理缓存、去重、重试、乐观更新等复杂逻辑。
- **客户端状态**(UI 展开/折叠、编辑器内容、筛选条件):以 `useState` / `useReducer` 管理,复杂度可控。
- **全局共享状态**(用户认证信息、主题偏好、权限节点):当前可能通过 React Context 或 Props Drilling 传递,建议评估是否需要引入轻量级全局状态库(如 **Zustand**)以避免深层嵌套组件的 Props 传递开销。
### 1.3.4 性能优化措施
目前已实施的优化措施:
| 优化手段 | 实施状态 | 效果评估 |
|----------|----------|----------|
| React.lazy 代码分割 | 已实施 | 降低首屏 JS 体积,提升 FCP/LCP |
| Vite 7 快速构建 | 已实施 | 开发阶段 HMR < 50ms,构建产出高效 |
| Tailwind CSS 原子样式 | 已实施 | 无运行时开销,产物体积小 |
建议补充的优化方向:
1. **虚拟列表**:工单列表与审计日志在数据量较大时可能出现渲染性能瓶颈,建议对长列表引入 `@tanstack/react-virtual` 或类似方案。
2. **编辑器按需加载**:CodeMirror 6 及其语言包体积不可忽视,建议确保编辑器组件仅在 SQL 相关路由中被异步加载。
3. **Tree Shaking 审查**:定期使用 `rollup-plugin-visualizer` 或类似工具分析产物体积,剔除未使用的依赖代码。
4. **图片资源优化**:如有图表截图、操作指南配图等静态资源,建议采用 WebP/AVIF 格式并配置懒加载。
---
## 1.4 现有 UI 设计体系梳理
### 1.4.1 CSS 变量体系评估
项目已建立一套以 `--dg-*` 为前缀的 **CSS 自定义属性体系**,覆盖了 Dark / Light 两种主题模式以及共享的渐变色系统。这一设计决策体现了良好的工程化思维——通过将颜色值抽象为语义化变量,可实现全站一键换肤并降低维护成本。
**Dark 主题色板**:
| 变量名 | 色值 | 语义角色 | 评估 |
|--------|------|----------|------|
| `--dg-bg-primary` | `#0d1117` | 页面主背景 | GitHub Dark 同款基底,视觉深邃,适合长时间操作 |
| `--dg-bg-secondary` | `#161b22` | 卡片/面板背景 | 与主背景形成 7% 亮度差,层级感清晰 |
| `--dg-bg-tertiary` | `#21262d` | 输入框/悬停态背景 | 足够的对比度确保交互反馈可感知 |
**Light 主题色板**:
| 变量名 | 色值 | 语义角色 | 评估 |
|--------|------|----------|------|
| `--dg-bg-primary` | `#ffffff` | 页面主背景 | 纯白基底,经典稳妥 |
| `--dg-bg-secondary` | `#f8f9fb` | 卡片/面板背景 | 微冷灰调,避免纯白页面的单调感 |
**共享渐变系统**:
| 变量名 | 色值 | 使用场景 |
|--------|------|----------|
| `--dg-gradient-start` | `#2563eb`(蓝色) | 品牌主色 → 用于 Logo、主按钮、活跃状态 |
| `--dg-gradient-end` | `#9333ea`(紫色) | 品牌辅色 → 用于渐变过渡、装饰性元素 |
蓝紫渐变在视觉上传递了**科技感与专业性**,与数据库管理工具的定位高度契合。该渐变目前被封装为 `.dg-gradient-text` 与 `.dg-gradient-bg` 两个工具类,使用便捷。
**语义化工具类**:
项目还定义了 `.dg-card`、`.dg-input` 等高级工具类,将颜色变量、圆角、阴影、边框等属性聚合为语义化的样式单元。这种"变量 + 工具类"的双层体系兼顾了灵活性与开发效率,是可维护性设计的体现。
### 1.4.2 shadcn/ui 组件库使用情况
项目使用了 **50+ shadcn/ui 基础组件**,覆盖了表单控件(Input、Select、Checkbox、Radio)、数据展示(Table、Card、Badge、Tooltip)、反馈(Dialog、Toast、Alert、Skeleton)、导航(Tabs、Breadcrumb、Sidebar)等完整类别。
shadcn/ui 的核心优势在于**源码级可定制性**——组件代码直接存在于项目的 `components/ui/` 目录中,团队可以:
- 自由修改组件的默认样式、动画参数、无障碍属性
- 根据品牌规范调整颜色、圆角、间距等设计 Token
- 添加业务特有的 Props 扩展(如在 Button 上增加 `isLoading` 的统一处理)
当前需要关注的点:50+ 组件的规模意味着维护成本不可忽视,建议在升级 shadcn/ui 版本时建立变更日志审查机制,确保上游的 Bug 修复与安全更新能够及时同步。
### 1.4.3 主题系统设计
平台当前的主题系统具备以下特征:
1. **双主题覆盖**:Dark / Light 两种模式,通过 CSS 变量切换实现,切换成本极低(仅需切换顶层 class)。
2. **渐变色作为品牌标识**:蓝紫渐变(`#2563eb` → `#9333ea`)贯穿两种主题,形成了强烈的品牌视觉记忆点。
3. **CSS 变量驱动**:所有主题相关属性均通过 `--dg-*` 变量管理,无硬编码色值散落在组件代码中。
**建议完善的方面**:
- 当前主题切换可能依赖 CSS 的 `prefers-color-scheme` 媒体查询或手动切换按钮,建议增加**主题偏好持久化**(localStorage)与**系统主题自动跟随**的双重机制。
- 建议补充 **High Contrast(高对比度)** 模式,满足无障碍合规要求(WCAG 2.1 AA 级)。
- 考虑引入 **主题 Token 文档**,明确定义每种语义色值的使用场景(如 `--dg-bg-primary` 仅用于页面级背景,不可用于卡片),防止变量滥用导致视觉不一致。
### 1.4.4 当前设计体系的优势与不足
**优势**:
| 维度 | 表现 |
|------|------|
| 工程化成熟度 | CSS 变量 + 工具类的双层体系,shadcn/ui 的源码级可控性 |
| 品牌一致性 | 蓝紫渐变标识贯穿全站,Dark/Light 双主题覆盖完整 |
| 开发效率 | Tailwind 原子类 + 语义化工具类,编写样式快速直观 |
| 扩展能力 | 新增主题仅需定义一组 CSS 变量,无需修改组件代码 |
**不足**:
| 维度 | 问题描述 | 改进优先级 |
|------|----------|-----------|
| 设计 Token 粒度 | 当前变量主要集中在颜色维度,缺少间距、字体、阴影、动效的 Token 化定义 | 高 |
| 动效体系缺失 | 页面转场、组件出现/消失、状态切换等交互缺乏统一的动效规范 | 高 |
| 无障碍支持 | 缺少 ARIA 属性系统化检查、键盘导航测试、色彩对比度合规验证 | 中 |
| 响应式设计 | 平台以桌面端操作为主,但缺少不同分辨率下的布局适配策略 | 中 |
| 图标体系 | 未明确图标库选型(如 Lucide、Radix Icons)与使用规范 | 低 |
综上,SQL-Platform 的 UI 设计体系已具备良好的基础骨架,特别是在 CSS 变量体系与组件化封装方面展现了专业水准。后续优化的核心方向应聚焦于**设计 Token 的维度扩展**(补充间距、动效、阴影 Token)、**交互动效体系的建立**以及**无障碍合规性的系统性提升**,这些内容将在后续章节中展开详细方案。
@@ -0,0 +1,171 @@
## 2. 现状问题诊断
经过对 SQL-Platform 前端代码库的全面审阅与走查,我们共识别出 5 大维度、23 项具体问题。这些问题贯穿登录认证、主题系统、核心功能页面、菜单导航以及整体设计体系,严重程度从"视觉瑕疵"到"架构性债务"不等。以下按模块逐项展开诊断。
---
### 2.1 登录页面五大问题分析
平台提供了 5 种登录页面布局(`SplitView`、`CenteredCard`、`SidePanel`、`FullImage`、`MinimalDark`),位于 `/frontend/src/components/login/` 目录下。尽管数量丰富,但 5 个组件共享大量共性问题,且各布局存在特有的设计缺陷。以下从视觉层次、交互体验、品牌感知、组件化程度和动效反馈五个维度进行诊断。
#### 2.1.1 视觉层次薄弱
**共性问题**:5 个登录页均缺乏清晰的视觉焦点层级,背景、装饰元素与核心表单区域之间的对比关系处理随意。
| 布局组件 | 具体视觉问题 | 严重程度 |
|---|---|---|
| `SplitView.tsx` | 左侧特性列表采用简单 icon+文字网格排列,无卡片包裹、无分割线,各 feature 项视觉权重完全一致,用户难以快速扫描核心卖点 | 中 |
| `CenteredCard.tsx` | 渐变背景仅为单色线性过渡,无纹理或光晕层次;卡片阴影硬编码为 `0 8px 32px rgba(0,0,0,0.3)`,缺乏环境光感,呈现"浮空贴图"效果 | 中 |
| `SidePanel.tsx` | 装饰性圆圈使用 `borderRadius: 50%` 的 div 硬编码,无渐变填充、无模糊处理,视觉上如同"开发占位符";品牌区域仅放置 `Database` 图标,无品牌图形标识 | 高 |
| `FullImage.tsx` | 电路板网格背景使用 CSS `repeating-linear-gradient` 实现,线条粗细一致、无辉光效果,整体呈现"半成品 sketch"观感;毛玻璃效果 `backdrop-filter: blur(16px)` 未配合饱和度降低(` saturate`)与边缘高光,通透感不足 | 高 |
| `MinimalDark.tsx` | 几乎无任何装饰性设计元素,纯黑背景+白字表单的极简走向极端,缺乏辅助视觉引导线或光晕点缀,整体显得空旷冷清 | 中 |
**根因分析**:各登录页的开发重心放在"布局差异化"上,却忽视了"视觉深度营造"。装饰元素的实现手段停留在 CSS 基础属性层面,未利用渐变、噪点纹理、多层阴影、边缘发光等现代视觉技法来建立前景/中景/背景的三层空间关系。
#### 2.1.2 交互体验不足
登录作为用户进入平台的第一触点,其交互体验直接影响用户对平台专业度的初始判断。当前 5 个登录组件在表单交互方面存在系统性缺陷:
1. **表单验证视觉反馈缺失**:全部使用原生 `<input>` 元素,未采用 shadcn/ui 的 `<Input>` 组件。输入框在 focus、error、success 三种状态下均无边框颜色变化、无图标提示、无 shake 动画。用户提交错误表单后,仅能看到简单的红色文字提示,错误字段本身没有任何视觉变化。
2. **"记住我"功能缺位**:5 个登录页均未提供"记住账号/保持登录"选项。对于企业级工具而言,频繁的会话过期与重复登录是显著的效率损耗点。
3. **"忘记密码"入口缺失**:没有提供密码找回或重置的链接入口,用户一旦遗忘密码将无从操作。
4. **安全提示文字缺失**:未展示"建议使用强密码"、"登录即表示同意服务条款"等合规性文案,降低了平台的安全可信度。
5. **版本号显示缺失**:登录页未展示当前平台版本号(如 `v2.4.1`),不利于运维排查与用户反馈时的版本确认。
#### 2.1.3 品牌感缺失
品牌名在所有 5 个登录页中统一硬编码为"SQL审核平台",名称过于直白、缺乏品牌辨识度与情感连接。对比业界标杆产品(如 Bytebase 的科技感命名、Archery 的品牌化 Logo),当前的命名策略停留在"功能描述"层面,未形成品牌记忆点。
更关键的是,5 个登录页均**未展示品牌标识**(Logo、品牌图形、吉祥物或标志性配色)。`SidePanel` 仅用 `Database` 通用图标代替品牌图形,`CenteredCard` 完全没有任何品牌标识。登录页作为品牌视觉锤的核心触点,未能承担品牌认知建设的职责。
#### 2.1.4 组件化程度低
从代码实现层面审视,5 个登录组件存在显著的工程化债务:
- **未使用 shadcn/ui `<Input>` 组件**:全部直接使用原生 `<input>`,导致表单样式无法继承主题系统的 CSS 变量,切换主题时可能出现颜色断层。
- **内联样式泛滥**:表单样式大量使用内联 `style` 属性(如 `style={{ padding: '24px', borderRadius: '8px' }}`),而非 Tailwind CSS 工具类。这破坏了项目的样式一致性,也增加了主题切换的维护成本。
- **按钮样式不统一**:有的登录按钮使用渐变背景,有的使用纯色,有的使用 outline 样式,5 个组件之间没有共享按钮规范。
- **登录按钮 loading 状态简陋**:loading 状态仅通过文字变化(如"登录中...")体现,无 spinner 图标、无按钮宽度保持、无禁用状态视觉变化,用户可能因按钮"变形"而产生疑惑。
**改进方向**:应将登录表单抽象为可复用的 `<LoginForm>` 组件,统一使用 shadcn/ui 的 `Input`、`Button`、`Checkbox`、`Label` 等原子组件,并通过 Tailwind class 管理所有样式。
#### 2.1.5 动效与反馈匮乏
当前登录页的动效设计几乎处于空白状态:
| 交互场景 | 当前表现 | 期望表现 |
|---|---|---|
| 登录成功 | 直接跳转,无过渡动画 | 显示成功 checkmark 动画,延迟 400ms 后优雅跳转 |
| 登录失败 | 简单红色文字提示 | 表单 shake 动画 + 错误字段高亮 + 带图标的错误消息 |
| 输入框 focus | 无特殊效果 | 边框颜色过渡 + 柔和外发光(`ring`) |
| 按钮 hover/click | 无反馈或仅有透明度变化 | 微缩放 + 阴影加深 + 涟漪效果 |
| 页面加载 | 静态呈现 | 表单区域的 fade-in + translateY 入场动画 |
**根因分析**:开发过程中未引入动画库(如 `framer-motion`),所有交互反馈停留在浏览器默认行为层面。登录页作为情感化设计的最佳载体,动效的缺失使得整体体验显得"机械"与"冰冷"。
---
### 2.2 主题系统局限性
当前主题系统仅支持 `dark` 与 `light` 两种模式,通过 `data-theme` HTML 属性进行切换,主题选择入口位于 `AppearanceSettings` 页面,以简单的按钮组形式呈现。
**核心局限点**:
1. **色彩方案单一**:缺乏紫色系、绿色系、暖色系、高对比度等多元色彩方案。现代 SaaS 平台(如 Vercel、Notion、Linear)通常提供 4-8 种预设主题,以满足不同用户的审美偏好与无障碍需求。
2. **主题预览简陋**:预览区域仅以 4:3 比例的缩略图展示,用户无法直观感受主题在真实数据密度下的表现。建议提供可交互的实时预览(如展示表格、按钮、标签等组件的组合效果)。
3. **主题切换过渡生硬**:切换主题时无过渡动画,页面瞬间"闪切",视觉体验断裂。应加入 200-300ms 的颜色过渡动画(`transition: color, background-color`)。
4. **自定义能力缺失**:高级用户无法调整主色调、圆角大小、字体密度等参数。建议提供"自定义主题"模式,允许用户通过色盘选择器定义个性化的品牌色。
5. **CSS 变量覆盖范围不足**:部分组件仍使用硬编码颜色值,未完全纳入 CSS 变量体系,导致主题切换时存在"漏色"现象。
---
### 2.3 核心功能页面问题
#### 2.3.1 审批变更工单系统
工单系统包含 3 个核心页面,总计 2742 行代码,存在显著的 UX 债务:
**审批变更页(`TicketsPage.tsx`,742 行)**:
- 表格使用原生 `<table>` 元素,未使用 shadcn/ui 的 `Table` 组件,导致列头排序、列宽调整、行悬停效果等功能全部缺失。
- 状态标签样式极其简单(如 `bg-blue-500/15 text-blue-400`),仅为色块+文字,无圆角规范、无图标辅助、无二态(solid/soft)区分。
- 操作按钮采用简单文字链接形式("查看""编辑""删除"),无图标按钮、无操作菜单(dropdown menu),在窄屏下易折行。
- **缺少批量操作功能**:用户无法对多条工单进行批量审批或批量关闭,效率低下。
- 搜索框为原生 `<input>`,无搜索图标、无清除按钮、无搜索建议。
- 分页组件为手写实现,未使用 shadcn/ui `Pagination`,样式与平台其他分页区域不统一。
**工单创建页(`TicketCreatePage.tsx`,1192 行)**:
- 布局采用上下堆叠的线性结构,左侧 SQL 编辑器与右侧库表浏览面板的空间配比固定,未利用响应式网格最大化编辑区域。
- SQL 编辑器下方的信息条(行数、字符数、语法状态)样式过于简单,无图标、无色彩状态区分。
- 审核结果展示为扁平列表,缺乏视觉层次:通过/失败的规则未使用不同颜色或图标区分,高亮行与错误行的视觉权重相近。
- 库表浏览面板的 tab 切换样式为基础样式,无活跃 tab 的指示器动画。
- **无自动保存草稿功能**:用户误刷新页面后,已编写的 SQL 将全部丢失,这是企业级工具的高风险体验缺口。
**工单详情页(`TicketDetailPage.tsx`,808 行)**:
- 审批流程可视化使用简单圆形+直线实现,无节点状态图标(如 `Check`、`Clock`、`X`)、无连线动画、无进度百分比指示。
- 信息网格采用 2 列 flex 布局直接裸露呈现,无卡片包裹、无分割线、无标签-值的对齐规范。
- 执行结果直接展示原始 JSON 字符串,无格式化、无语法高亮、无可折叠区域,运维人员阅读极为困难。
- 关联工单区域仅为简单的工单编号链接列表,无状态徽章、无优先级标识。
#### 2.3.2 查询数据页面
`QueriesPage.tsx`(896 行)作为数据查询的核心工作区,存在以下 UX 问题:
- 查询结果表格同样使用原生 `<table>`,列宽不可调整(无拖拽调整功能),对于宽表(20+ 列)的浏览体验极差。
- **缺少查询收藏夹功能**:用户无法保存常用的查询语句,每次需重新输入或从历史中翻找。
- 历史记录面板为简单的文字列表,无查询耗时显示、无行数结果、无收藏/删除操作。
- SQL 编辑器与结果表格之间的区域无明确的分割线或可调整拖拽条(resizer),用户无法按需求分配编辑区与结果区的屏幕空间。
#### 2.3.3 系统设置页面
`SettingsPage.tsx`(56 行)作为设置入口,问题尤为突出:
- **Tab 图标使用 Emoji**(`🎨` 外观、`⚙️` 平台、`🛡️` 安全、`🔔` 通知、`🔐` 密码),与平台其他页面统一使用的 `lucide-react` 图标体系严重冲突,视觉风格割裂。Emoji 在不同操作系统下的渲染差异也会导致界面不统一。
- Tab 切换无动画过渡,内容区域的切换为瞬间跳转,体验生硬。
- 各设置子页面的表单设计不一致:有的使用 shadcn/ui 组件,有的(如 `PlatformConfigPage`)直接使用原生 `<input>`,标签对齐方式、间距、按钮位置均不统一。
- **缺少设置变更确认/撤销机制**:用户在修改敏感配置(如数据库连接参数)后,没有二次确认对话框;也没有"重置为默认值"或"撤销本次更改"的功能,误操作风险高。
---
### 2.4 菜单体系问题
平台的侧边栏导航菜单存在以下结构性问题:
1. **图标体系不统一**:`Settings` 页面使用 Emoji 图标,而其他所有页面使用 `lucide-react` 图标库。这种混用破坏了导航的视觉一致性,也降低了专业感。
2. **菜单分组合理性不足**:当前菜单分为 6 个 section,但部分分类逻辑存在问题。例如"自动任务"被归类在"系统配置"分组下,而从用户心智模型来看,自动任务属于"运维操作"范畴,与"系统配置"(偏静态参数)的认知类别不同。建议重新审视信息架构,采用"按用户角色分组"或"按操作频率分组"的策略。
3. **面包屑导航缺失**:页面层级超过 2 层后(如 设置 > 平台配置 > 数据库参数),用户无法通过面包屑快速定位当前位置或返回上级,只能依赖浏览器后退按钮。
4. **Hover/Active 状态过渡动画缺失**:菜单项的悬停和激活状态切换为瞬间变化,无 150-200ms 的颜色/背景过渡动画,整体感觉"卡顿"而非"响应"。
5. **当前页面指示不够醒目**:激活菜单项仅通过轻微的颜色变化区分,缺乏左侧边框高亮(`border-left` accent)或背景色块等强指示手段。
---
### 2.5 设计体系不一致性
从代码层面审视,平台在整体 design system 的贯彻执行上存在严重的碎片化现象:
**卡片组件混用**:部分页面使用 shadcn/ui 的 `<Card>` 组件(如 Dashboard),部分页面使用 `<div>` 手写卡片样式(如工单详情页的信息网格)。手写卡片的圆角、阴影、边框、背景色与 shadcn/ui Card 的默认参数存在细微差异,积累后形成显著的视觉噪音。
**按钮样式分裂**:平台内同时存在 3 种按钮风格——渐变按钮(部分登录页)、纯色填充按钮(shadcn/ui 默认)、outline 描边按钮(部分操作栏)。三种风格的使用没有明确的场景规范,同一页面内也可能混用,破坏了操作隐喻的一致性。
**表单输入框样式差异**:有的页面使用 shadcn/ui `<Input>`(带统一的 focus ring、圆角、高度),有的页面使用原生 `<input>`(样式依赖浏览器默认),有的页面使用内联样式自定义的输入框。三种实现的视觉高度、边框粗细、焦点反馈均不相同。
**颜色管理混乱**:部分组件使用硬编码颜色值(如 `rgba(0,0,0,0.3)`、`#3b82f6`),部分组件使用 CSS 变量(如 `hsl(var(--primary))`),部分组件使用 Tailwind 的语义化 class(如 `bg-primary`)。当主题切换或品牌色调整时,硬编码的颜色值不会跟随变化,导致"漏色"与视觉断层。
**间距系统缺失**:各页面的 `padding` 和 `margin` 取值随意,无统一的 spacing scale。例如有的卡片内边距为 `16px`,有的为 `20px`,有的为 `24px`;页面级的外边距也无统一规范(`pagePadding` token 缺失)。这种不一致使得页面之间的"呼吸感"差异明显,整体缺乏精致感。
**根因总结**:以上不一致性的根本原因在于缺乏一个被严格执行的 Design Token 体系与组件使用规范。开发人员在实现新页面时,没有明确的"应该使用哪个组件、哪个 token"的指引,导致各页面"各自为政"。建议引入一套完整的 Design Token(颜色、间距、字体、阴影、圆角)并配合 ESLint 规则强制在组件层面统一规范。
---
> **本章小结**:SQL-Platform 的现状问题可归纳为"功能可用但体验粗糙"。登录页面作为品牌门面,在视觉层次、交互反馈、品牌感知上均有显著短板;核心功能页面(工单系统、查询页面、设置页面)在组件选型、布局效率、信息架构上存在系统性债务;主题系统与菜单体系的局限性则反映了设计体系在规划层面的不足。这些问题并非孤立存在,而是相互交织:设计体系的碎片化导致了各页面实现的不一致,组件化程度低导致了交互体验的简陋,主题系统的局限又加剧了视觉风格的单一。后续章节将针对这些问题提出具体的优化方案。
@@ -0,0 +1,393 @@
## 3. 五套专业登录页设计方案
针对现有五个登录页组件(`SplitView.tsx`、`CenteredCard.tsx`、`SidePanel.tsx`、`FullImage.tsx`、`MinimalDark.tsx`)"不专业、太 low"的用户反馈,本章提供五套完整的专业级登录页重设计方案。每套方案均基于 React 19 + TypeScript + Tailwind CSS 4 + shadcn/ui + Lucide Icons 技术栈,保留现有的 `useLogin` 与 `useTheme` hook 架构,在视觉表现、交互体验与动效层次上实现全面升级。
五套方案分别对应五种截然不同的企业气质:科技互联、金融安全、云端 SaaS、极客代码、创意自然。设计方案遵循"一次一屏"(Single Screen)原则,确保登录流程简洁高效,同时在第一眼传递出强烈的品牌专业感。
---
### 3.1 方案一「极光科技」— 动态粒子科技风
#### 设计概念与灵感来源
「极光科技」方案的灵感源自现代科技公司的数据可视化美学——深空般的背景中,粒子如数据节点般流动、连接,暗示着数据库节点之间的网络拓扑关系。设计参考了 Vercel、PlanetScale 等现代 DevTools 产品的暗黑登录页风格,将"数据流动"这一抽象概念转化为可感知的视觉语言。深色背景配合冷色调粒子动画,营造出深邃、专注、充满科技张力的第一印象。
#### 适用场景
适合科技互联网公司、数据库工具产品、DevOps 平台、技术驱动型 SaaS 企业。这类企业的用户群体(开发者、DBA、技术人员)对科技感视觉有天然的好感度与信任预期。
#### 视觉规范
| 元素 | 规范值 | 说明 |
|------|--------|------|
| 背景渐变 | `#0a0e27` → `#1a1f4b` | 深邃的深蓝到暗蓝,比纯黑更有空间感 |
| 粒子数量 | 80 个 | 足够形成视觉密度,不过度抢占注意力 |
| 粒子颜色 | `#3b82f680`(半透蓝) | 与品牌蓝色系一致,带透明度避免喧宾夺主 |
| 连线距离 | 120px | 控制连线密度,确保画面不过于繁杂 |
| 卡片背景 | `rgba(255,255,255,0.03)` | 极微弱的白色,配合毛玻璃效果 |
| 卡片毛玻璃 | `backdrop-blur: 20px` | iOS 风格毛玻璃,现代感强烈 |
| 卡片边框 | `1px solid rgba(255,255,255,0.08)` | 微妙的发光边框勾勒卡片轮廓 |
| 卡片圆角 | `16px` | 现代感的较大圆角 |
| 卡片内边距 | `40px 32px` | 宽裕的内部空间 |
| Logo 底圆 | `56px`,渐变 `from #2563eb to #9333ea` | 渐变圆形底托提升品牌质感 |
| Logo 图标 | `Shield`(Lucide),白色,`24px` | 安全感的盾形图标 |
| 品牌名 | "DataGuard",`20px`,白色,字重 600 | 清晰的品牌露出 |
| 输入框高度 | `44px` | 符合现代表单的最佳点击高度 |
| 输入框圆角 | `10px` | 与卡片圆角协调 |
| 输入框聚焦 | 边框 `#3b82f6`,外发光 `0 0 0 3px rgba(59,130,246,0.15)` | 蓝色光晕聚焦指示 |
| 按钮渐变 | `from #2563eb to #7c3aed` | 蓝紫渐变,与现有品牌渐变一致 |
| 按钮高度 | `44px` | 与输入框等高,视觉对齐 |
| 按钮圆角 | `10px` | 与输入框统一 |
| 辅助链接色 | `#8b949e`,hover `#58a6ff` | GitHub 风格的灰色链接 |
#### 动效规范
**粒子系统动画**:使用 Canvas 2D 实现 60fps 粒子系统。每个粒子以随机速度在画布内移动(速度范围 0.3~0.8px/frame),碰到边界反弹。当两个粒子距离小于 120px 时,绘制连线(线条透明度随距离衰减:`opacity = 1 - distance/120 * 0.7`)。鼠标位置额外产生一个"引力点",靠近鼠标的粒子连线距离阈值提升至 180px,形成交互反馈。
**卡片入场动画**:
```css
@keyframes cardEnter {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* duration: 600ms, easing: cubic-bezier(0.16, 1, 0.3, 1) */
```
使用 `cubic-bezier(0.16, 1, 0.3, 1)` 缓动函数——初始阶段快速响应,结尾柔和减速,营造"丝滑吸附"的质感。
**输入框聚焦**:边框颜色过渡 `200ms ease`,外发光同步渐显,给用户明确的焦点反馈。
**按钮 Hover**:`transform: translateY(-1px)` 微上移,`box-shadow` 从 `0 4px 12px rgba(37,99,235,0.25)` 增强为 `0 8px 24px rgba(37,99,235,0.35)`,过渡 `200ms ease`。
**错误状态 Shake 动画**:
```css
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
50% { transform: translateX(5px); }
75% { transform: translateX(-5px); }
}
/* duration: 300ms, 执行 1 次,同时边框变为 #ef4444 */
```
#### 代码实现要点
1. **粒子系统**:使用 `useRef<HTMLCanvasElement>` 获取 Canvas 上下文,在 `useEffect` 中初始化粒子数组并启动 `requestAnimationFrame` 动画循环。粒子系统封装为独立的 `ParticleBackground.tsx` 组件,通过 props 接收 `particleCount`、`connectionDistance`、`color` 等参数。
2. **卡片结构**:使用 `Card`(shadcn/ui)作为容器,外层包裹 `div` 设置 `backdrop-blur-xl bg-white/[0.03] border border-white/[0.08]` 实现毛玻璃效果。
3. **表单组件**:`Input` 和 `Button` 直接使用 shadcn/ui 组件,通过 Tailwind 的 `className` 覆盖默认样式。输入框聚焦外发光使用 `focus:ring-[3px] focus:ring-blue-500/15 focus:border-blue-500`。
4. **状态管理**:继续使用现有的 `useLogin` hook(`username` / `password` / `loading` / `error`),错误触发时通过条件类名绑定 shake 动画。
---
### 3.2 方案二「企业金融」— 银行级安全可信风
#### 设计概念与灵感来源
金融类产品的登录页第一要务是传递"安全感"。「企业金融」方案借鉴了招商银行企业网银、Stripe Dashboard 以及 Bloomberg Terminal 的视觉语言——通过深沉的蓝色调、严谨的版式分割、明确的信任标识,在瞬间建立起用户对系统的信任。左右分屏的布局本身就是一种力量感的表达:左侧沉稳的品牌展示,右侧聚焦的表单操作,信息层级清晰,符合金融用户"高效、安全、专业"的心理预期。
#### 适用场景
金融、银行、保险、证券、支付等强监管行业企业。用户群体包括金融从业者、企业财务人员、风控人员,他们对安全标识(SSL、加密、审计)高度敏感。
#### 视觉规范
| 元素 | 规范值 | 说明 |
|------|--------|------|
| 左侧背景 | `#0c1a3c` → `#1a365d` | 深蓝渐变,传递权威与稳重 |
| 左侧宽度 | 60% | 大面积品牌展示区 |
| 右侧背景 | `#f8fafc` | 极浅灰白,与左侧形成清晰分割 |
| 右侧宽度 | 40% | 聚焦的表单操作区 |
| 品牌 Logo | 白色 Shield 图标 + "DataGuard" | 居中偏上布局 |
| Slogan | "安全的数据库变更管理",`18px`,`#94a3b8` | 简洁定位标语 |
| 信任指标 | 3 个:SSL 加密、权限隔离、审计追踪 | 带 CheckCircle 图标的列表 |
| 信任指标图标 | `CheckCircle2`(Lucide),`#22c55e` | 绿色对勾传递"已验证" |
| 信任指标文字 | `#cbd5e1`,`14px` | 柔和的说明文字 |
| 右侧卡片 | 白色背景,圆角 `16px` | 干净的表单容器 |
| 卡片阴影 | `0 4px 24px rgba(0,0,0,0.08)` | 微妙的浮起感 |
| 卡片内边距 | `48px 40px` | 金融级宽裕间距 |
| 登录标题 | "欢迎回来",`24px`,`#0f172a`,字重 700 | 亲切的问候 |
| 输入框样式 | 底部边框(Underline Input)| 类似 Material Design 的底线输入 |
| 输入框高度 | `48px` | 稍大以凸显重要感 |
| 输入框边框 | 默认 `border-b-2 border-slate-200`,聚焦 `border-[#1a365d]` | 底线聚焦变深蓝 |
| 按钮背景 | `#1a365d` | 沉稳深蓝,呼应左侧 |
| 按钮高度 | `48px` | 与输入框等高 |
| 按钮圆角 | `8px` | 相对克制的圆角,体现严谨 |
| 安全提示 | "🔒 您的连接已加密保护",`12px`,`#64748b` | 明确的安全信号 |
#### 动效规范
**左右面板载入**:页面加载时,左侧面板从左侧滑入(`translateX(-40px) → 0`,`400ms ease-out`),右侧面板从右侧同步滑入(`translateX(40px) → 0`,`400ms ease-out`,延迟 `50ms`)。两侧同时汇聚中间,隐喻"安全通道的建立"。
**信任指标依次淡入**:3 个信任指标在左侧面板就位后依次显示,每个间隔 `150ms`:
```css
.trust-item:nth-child(1) { animation-delay: 200ms; }
.trust-item:nth-child(2) { animation-delay: 350ms; }
.trust-item:nth-child(3) { animation-delay: 500ms; }
/* 统一动画: opacity 0→1, translateX(-10px)→0, 300ms ease-out */
```
**按钮 Hover**:背景色从 `#1a365d` 平滑过渡为 `#122a4a`(加深 10%),同时增加 `box-shadow: 0 4px 16px rgba(12,26,60,0.2)`,过渡 `200ms ease`。
**输入框聚焦**:底部边框从 `#e2e8f0` 变为 `#1a365d`,宽度有微妙的"延展"效果(使用 `scaleX` 从 0→1 配合 `transform-origin: left`),`200ms ease`。
#### 代码实现要点
1. **分屏布局**:使用 CSS Grid `grid-cols-[60%_40%]` 或 Flex `flex-[60]` / `flex-[40]` 实现左右分割。移动端(`< 1024px`)切换为上下布局,左侧折叠为顶部横幅。
2. **底线输入框**:不使用 shadcn/ui 默认的边框输入框,而是自定义 `div` 包裹 `<input>`,通过 `border-b-2` + `bg-transparent` 实现底线样式,聚焦时切换 `border-color`。
3. **信任指标列表**:封装为 `TrustIndicators` 子组件,接收指标数组渲染,配合 `setTimeout` 或 CSS `animation-delay` 实现依次淡入。
4. **安全提示**:底部固定位置,使用 `flex items-center gap-2` 布局,锁图标使用 Lucide `Lock` 组件。
---
### 3.3 方案三「云端数据」— 云计算 SaaS 风
#### 设计概念与灵感来源
云计算和 SaaS 产品的登录页追求的是"轻盈、开放、无门槛"。「云端数据」方案汲取了 Snowflake、Figma、Notion 等现代 SaaS 产品的登录体验——明亮的浅色调、友好的圆角语言、丰富的 SSO 集成选项,向用户传递"即开即用"的产品气质。右上角的装饰性云朵图案是对"云端"概念的直接呼应,但处理为低对比度的纹理,避免过度具象化。
#### 适用场景
云服务商、SaaS 产品、互联网协作工具、中台系统、B2B 软件公司。用户群体包括产品经理、技术团队、普通企业用户,他们期望登录流程简单快捷,SSO 支持是加分项。
#### 视觉规范
| 元素 | 规范值 | 说明 |
|------|--------|------|
| 背景渐变 | `#f0f4f8` → `#e2e8f0` | 柔和的天空感浅灰蓝渐变 |
| 装饰云朵 | SVG 路径,右上角,`opacity: 0.3` | 微妙的云端意象,不干扰主体 |
| 卡片背景 | `#ffffff` 纯白 | 干净、明亮的中心焦点 |
| 卡片圆角 | `20px` | SaaS 产品偏爱的友好大圆角 |
| 卡片阴影 | `0 8px 32px rgba(148,163,184,0.15)` | 柔和的悬浮阴影 |
| 卡片宽度 | `420px` | 适中宽度,聚焦表单 |
| 卡片内边距 | `48px 40px` | 宽裕的内部空间 |
| 顶部品牌区 | `40px` 圆角矩形图标 + "SQL-Platform" | 清晰的身份标识 |
| 品牌图标背景 | `from #0ea5e9 to #2563eb` | 天蓝到蓝的渐变,呼应云端主题 |
| 副标题 | "数据库变更管理平台",`14px`,`#64748b` | 产品定位说明 |
| 输入框背景 | `#f8fafc` | 极浅灰底,区别于卡片白 |
| 输入框圆角 | `12px` | 圆角输入,友好感 |
| 输入框边框 | 默认无边框,聚焦 `1px solid #3b82f6` | 最小化视觉噪音 |
| 输入框高度 | `46px` | 略大于标准,易于点击 |
| 按钮渐变 | `from #0ea5e9 to #2563eb` | 天空蓝到标准蓝 |
| 按钮圆角 | `12px` | 与输入框统一 |
| 按钮高度 | `46px` | 与输入框等高对齐 |
| 分隔线 | "或使用以下方式登录",`13px`,`#94a3b8` | 经典的 SSO 分隔 |
| SSO 图标 | 企业微信/钉钉/飞书,`40px` 圆形按钮 | 图标化登录选项 |
| SSO 按钮背景 | `#f1f5f9`,hover `#e2e8f0` | 灰色底,不与主按钮竞争 |
#### 动效规范
**卡片入场**:从上方轻柔落下,同时伴随微妙的缩放——`opacity 0→1`、`translateY(-16px)→0`、`scale(0.98)→1`,`500ms cubic-bezier(0.34, 1.56, 0.64, 1)`。使用弹性缓动函数(overshoot easing),营造 SaaS 产品特有的"活泼弹动感"。
**输入框聚焦**:无边框输入框在聚焦时,背景色从 `#f8fafc` 微妙提亮至 `#ffffff`,同时蓝色边框从 `opacity 0` 渐显至 `opacity 1`,过渡 `150ms ease`。
**按钮 Hover**:`translateY(-2px)` 上浮,`box-shadow` 增强,同时渐变产生微妙的位置偏移(使用 `background-position` 动画),`200ms ease`。
**SSO 图标 Hover**:圆形背景从 `#f1f5f9` 变为 `#e2e8f0`,图标 `scale(1.1)`,`150ms ease`。
**装饰云朵**:缓慢的水平飘移动画(`translateX(0) → translateX(20px) → translateX(0)`,`20s ease-in-out infinite`),赋予页面呼吸感。
#### 代码实现要点
1. **卡片弹性动画**:使用 Tailwind 的 `animate-in` 配合自定义 CSS keyframe 实现弹性入场。或使用 Framer Motion(如项目已引入)的 `initial/animate` props 配合 `type: "spring", stiffness: 200, damping: 15` 实现物理弹性效果。
2. **无边框输入框**:shadcn/ui 的 `Input` 组件默认有边框,需通过 `border-0 bg-slate-50 focus:bg-white focus:ring-1 focus:ring-blue-500` 覆盖样式。
3. **SSO 按钮组**:使用 Flex 布局 `flex gap-3 justify-center`,每个 SSO 按钮为 `button` 元素包裹品牌 SVG 图标。考虑到不同企业的 SSO 需求,SSO 区域应封装为 `SSOProviders` 组件,通过配置数组动态渲染。
4. **云朵装饰**:将云朵 SVG 设为 `position: absolute; top: -50px; right: -30px; pointer-events: none;`,确保不影响交互。
---
### 3.4 方案四「极简矩阵」— 开发者极客风
#### 设计概念与灵感来源
对于面向开发者的工具平台,过于" polished "的设计反而可能显得不够硬核。「极简矩阵」方案致敬了《黑客帝国》的数字雨(Matrix Rain)以及终端命令行的纯粹美学,将登录页转化为一个"进入系统"的仪式。纯黑背景上绿色字符如雨般落下,输入框如同终端命令行,每一个交互细节都在告诉用户:"这是一个为技术人员打造的严肃工具。"
#### 适用场景
技术团队、开发者工具、DevOps 平台、开源项目、CLI/GUI 混合产品。用户群体是开发者、SRE、运维工程师,他们对终端界面有亲切感,极客风格的 UI 反而能唤起"同道中人"的认同感。
#### 视觉规范
| 元素 | 规范值 | 说明 |
|------|--------|------|
| 背景色 | `#000000` 纯黑 | 终端/命令行的经典底色 |
| 字符雨颜色 | `#22c55e`(绿色),`#16a34a`(深绿) | 双色调绿色增加层次 |
| 字符集 | 大小写字母 + 数字 + 日文片假名 | Matrix 经典字符集 |
| 字符密度 | 比原版 Matrix 减少 40% | 不过度干扰登录表单的可读性 |
| 字符下落速度 | 2~6 px/frame 随机 | 自然的不规则雨效 |
| 卡片背景 | `rgba(0,0,0,0.85)` | 高透明度黑底,字符隐约可见 |
| 卡片边框 | `1px solid rgba(34,197,94,0.3)` | 绿色发光边框,终端窗口感 |
| 卡片圆角 | `4px` | 极小圆角,锐利的技术感 |
| 标题字体 | `JetBrains Mono`,`22px`,`#22c55e` | 等宽字体是终端的灵魂 |
| 标题文字 | `> sql-platform login` | 命令行风格的标题 |
| 副标题 | "请输入您的凭据以继续",`13px`,`#4ade80` | 浅绿说明文字 |
| 输入框背景 | `#0a0a0a` | 深黑,区别于卡片 |
| 输入框边框 | `1px solid rgba(34,197,94,0.25)` | 微弱绿边 |
| 输入框圆角 | `4px` | 锐利 |
| 输入框字体 | `JetBrains Mono`,`14px` | 打字机般的输入体验 |
| 输入框文字色 | `#22c55e` | 绿色输入文字 |
| Placeholder | `#374151`(深灰)| 低调提示 |
| 输入框聚焦 | 边框变为 `rgba(34,197,94,0.6)` | 明亮的绿色边框 |
| 按钮背景 | `#22c55e` | 醒目绿色 |
| 按钮文字 | `#000000`,`JetBrains Mono`,字重 600 | 黑底绿字,高对比 |
| 按钮圆角 | `4px` | 锐利风格统一 |
| 光标 | 自定义绿色闪烁光标 | `2px` 宽竖线,每 `530ms` 闪烁 |
| 底部版本号 | "v2.4.1 — build 20240815",`11px`,`#374151` | 开发者熟悉的版本信息 |
#### 动效规范
**字符雨动画**:使用 Canvas 2D 实现。初始化多列字符,每列以随机速度向下滚动。每个字符有随机的生命周期——新字符高亮(`#4ade80`),旧字符渐暗(`#15803d` → `#166534`),超出画布底部后重置到顶部。字符雨在登录卡片后方运行,使用 `z-index` 分层。
**卡片入场**:模拟终端启动——先显示 `> sql-platform login_`(带闪烁光标),停顿 `300ms` 后表单元素逐行"打印"出现。每个元素使用 `opacity 0→1`,间隔 `80ms`,模拟打字机效果。
**输入框聚焦**:边框绿色亮度提升(`opacity 0.25 → 0.6`),同时产生微弱的绿色外发光 `0 0 8px rgba(34,197,94,0.15)`,`150ms ease`。
**按钮 Hover**:背景色从 `#22c55e` 变为 `#4ade80`(更亮),同时按钮产生 `box-shadow: 0 0 16px rgba(34,197,94,0.3)` 绿光晕染,`150ms ease`。
**光标闪烁**:
```css
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* animation: blink 1.06s step-end infinite */
```
使用 `step-end` 时序函数实现硬切闪烁,更接近真实终端光标。
**错误状态**:终端风格的错误提示——输入框边框变为 `#ef4444`,下方显示红色文字 `[ERROR] 用户名或密码不正确`,字体 `JetBrains Mono`,`12px`。无 shake 动画,用冷静的错误信息替代。
#### 代码实现要点
1. **字符雨 Canvas**:独立的 `MatrixRain.tsx` 组件。核心逻辑:创建 `columns` 数组,每列跟踪当前字符和 y 坐标。在 `requestAnimationFrame` 循环中,以半透明黑色 (`rgba(0,0,0,0.05)`) 覆盖画布而非完全清除,形成字符拖尾效果。字符从预定义字符集中随机选取。
2. **等宽字体**:通过 Google Fonts 或本地引入 JetBrains Mono。Tailwind 配置中添加 `fontFamily: { mono: ['JetBrains Mono', 'monospace'] }`。
3. **打字机入场**:使用自定义 hook `useTypewriterEffect`,控制子元素依次显示。或直接使用 `setTimeout` 链实现简单的延迟序列。
4. **终端光标**:使用一个 `span` 元素(`w-[2px] h-[1.2em] bg-green-500 inline-block`)配合 CSS 动画实现闪烁。可封装为 `TerminalCursor` 小组件。
5. **密码输入**:密码框仍使用 `type="password"`,但自定义样式使圆点/星号也呈现绿色(通过 `color: #22c55e` 和 `caret-color: #22c55e`)。
---
### 3.5 方案五「自然流动」— 有机渐变自然风
#### 设计概念与灵感来源
并非所有企业都喜欢冷峻的科技风。「自然流动」方案面向创意公司、设计团队和文化企业,从自然界的极光、晨曦、水波中汲取配色灵感。三个巨大的模糊色块在背景中缓慢漂移、交叠,产生不断变化的有机渐变——这种"活着的"背景打破了登录页的单调,将等待时间转化为一种视觉享受。设计参考了 Linear、Arc Browser 等产品的有机渐变美学,以及 Blobs 设计趋势。
#### 适用场景
创意设计公司、数字营销机构、文化传媒企业、生活方式品牌、内部创意工具。用户群体包括设计师、创意总监、市场人员,他们对视觉美感有较高要求,偏好温暖、有情感的界面。
#### 视觉规范
| 元素 | 规范值 | 说明 |
|------|--------|------|
| 背景底色 | `#0f0e1a` | 深紫黑,衬托前景彩色光晕 |
| 渐变圆 1 | `#f472b6`(粉色),直径 `600px`,blur `120px` | 左上区域 |
| 渐变圆 2 | `#a78bfa`(紫色),直径 `500px`,blur `120px` | 右侧中部 |
| 渐变圆 3 | `#60a5fa`(蓝色),直径 `550px`,blur `120px` | 底部中央 |
| 模糊方式 | CSS `filter: blur(120px)` 或 SVG `feGaussianBlur` | 产生柔和的色块融合 |
| 卡片背景 | `rgba(255,255,255,0.1)` | 半透明白 |
| 卡片毛玻璃 | `backdrop-blur: 24px` | 更强的模糊,突出背后的色彩流动 |
| 卡片圆角 | `24px` | 有机的大圆角 |
| 卡片边框 | `1px solid rgba(255,255,255,0.15)` | 半透明白色边框 |
| 卡片内边距 | `48px 40px` | 宽裕的空间 |
| 品牌图标 | `64px` 大圆角矩形,`border-radius: 20px` | 渐变背景 `from #f472b6 to #a78bfa` |
| 品牌名 | "DataGuard",`22px`,白色,字重 600 | 居中展示 |
| 品牌标语 | "数据之美,流动不息",`14px`,`rgba(255,255,255,0.6)` | 诗意化标语 |
| 输入框背景 | `rgba(255,255,255,0.08)` | 毛玻璃输入 |
| 输入框边框 | `1px solid rgba(255,255,255,0.12)` | 微妙的边框 |
| 输入框圆角 | `14px` | 圆润 |
| 输入框高度 | `46px` | 舒适点击高度 |
| 输入框文字 | 白色,`rgba(255,255,255,0.9)` | 高对比度 |
| 输入框聚焦 | 边框变为 `rgba(255,255,255,0.4)` | 白色高亮 |
| 按钮背景 | `#ffffff` | 白色主按钮 |
| 按钮文字 | `#0f0e1a`,字重 600 | 深色文字 |
| 按钮圆角 | `12px` | 现代圆角 |
| 按钮高度 | `46px` | 与输入框等高 |
| 卡片外光晕 | `box-shadow: 0 0 80px rgba(167,139,250,0.15)` | 微妙的紫色光晕包裹卡片 |
#### 动效规范
**渐变圆漂移**:三个彩色模糊圆以不同的速度和轨迹缓慢移动,形成不断变化的背景:
- 粉圆:`translate` 在 `(0,0) → (30px,-20px) → (-20px,30px) → (0,0)` 之间循环,`15s ease-in-out infinite`
- 紫圆:`translate` 在 `(0,0) → (-25px,25px) → (20px,-15px) → (0,0)`,`18s ease-in-out infinite`
- 蓝圆:`translate` 在 `(0,0) → (20px,20px) → (-30px,-10px) → (0,0)`,`12s ease-in-out infinite`
三个圆的运动周期互质(15s/18s/12s),确保组合图案永不重复。
**卡片入场**:`opacity 0→1`、`translateY(24px)→0`、`scale(0.96)→1`,`700ms cubic-bezier(0.22, 1, 0.36, 1)`。较长的时间和柔和的缓动,配合背景的流动感,营造"浮出水面"的诗意体验。
**输入框聚焦**:边框透明度提升,同时产生微弱的白色外发光 `0 0 0 3px rgba(255,255,255,0.08)`,`200ms ease`。
**按钮 Hover**:`scale(1.02)` 微放大(而非上移,更柔和),`box-shadow` 增强为 `0 8px 24px rgba(255,255,255,0.2)`,过渡 `200ms ease`。白色按钮在暗色背景下产生"发光"效果。
**光晕脉动**:卡片外层的紫色光晕以 `4s ease-in-out infinite` 的节奏在 `opacity 0.1 ↔ 0.2` 之间呼吸,赋予页面生命力。
#### 代码实现要点
1. **SVG 渐变网格**:使用绝对定位的 `div` 元素包裹三个 `div`(每个设置 `border-radius: 50%`、`filter: blur(120px)`、对应背景色),通过 CSS `@keyframes` 分别控制 `transform` 动画。或使用 SVG `<circle>` 配合 `<filter><feGaussianBlur>` 实现同样的模糊色块效果。
2. **强毛玻璃卡片**:`backdrop-blur-2xl bg-white/10 border border-white/15 rounded-3xl shadow-[0_0_80px_rgba(167,139,250,0.15)]`。注意 `backdrop-blur` 需要浏览器支持,可考虑添加 `-webkit-backdrop-blur` 前缀。
3. **白色主按钮**:在深色背景下,白色按钮天然具有高对比度。Hover 效果避免使用 `translateY`(上移动效偏科技感),改用 `scale(1.02)`(更柔和、有机)。
4. **性能优化**:`blur(120px)` 大模糊是 GPU 密集型操作。建议使用 `will-change: transform` 提示浏览器优化,同时确保动画元素使用 `transform` 而非 `top/left` 属性。在低性能设备上可通过 `@media (prefers-reduced-motion: reduce)` 关闭背景动画。
---
### 3.6 各方案组件设计要点(对比表)
| 设计维度 | 方案一「极光科技」 | 方案二「企业金融」 | 方案三「云端数据」 | 方案四「极简矩阵」 | 方案五「自然流动」 |
|:---------|:------------------|:------------------|:------------------|:------------------|:------------------|
| **布局结构** | 居中卡片 + 全屏粒子背景 | 左右分屏 60:40 | 居中卡片 + 浅色背景 | 居中卡片 + 全屏字符雨 | 居中卡片 + 动态渐变背景 |
| **背景实现** | Canvas 2D 粒子系统 | CSS 渐变 + 纯色分割 | CSS 渐变 + SVG 装饰 | Canvas 2D Matrix Rain | SVG/CSS 模糊渐变圆 |
| **卡片材质** | 毛玻璃 `backdrop-blur: 20px` | 纯白实色 | 纯白实色 | 半透明黑 `rgba(0,0,0,0.85)` | 强毛玻璃 `backdrop-blur: 24px` |
| **卡片圆角** | `16px` | `16px` | `20px` | `4px`(锐利) | `24px`(最大) |
| **卡片边框** | `1px solid rgba(255,255,255,0.08)` | 无 | 无 | `1px solid rgba(34,197,94,0.3)` | `1px solid rgba(255,255,255,0.15)` |
| **主色调** | 蓝 `#2563eb` + 紫 `#7c3aed` | 深蓝 `#1a365d` | 天蓝 `#0ea5e9` | 绿 `#22c55e` | 粉 `#f472b6` + 紫 `#a78bfa` + 蓝 `#60a5fa` |
| **背景色** | `#0a0e27` → `#1a1f4b` | 左 `#0c1a3c` 右 `#f8fafc` | `#f0f4f8` → `#e2e8f0` | `#000000` | `#0f0e1a` |
| **输入框样式** | 标准边框式 | 底部底线式 | 无边框 + 灰底 | 等宽字体 + 绿边框 | 毛玻璃半透明 |
| **按钮样式** | 渐变蓝紫 | 纯色深蓝 | 渐变天蓝 | 纯色绿 | 纯白 |
| **按钮圆角** | `10px` | `8px` | `12px` | `4px` | `12px` |
| **字体选择** | 系统默认 sans-serif | 系统默认 sans-serif | 系统默认 sans-serif | JetBrains Mono 等宽 | 系统默认 sans-serif |
| **SSO 支持** | 无 | 无 | 有(企业微信/钉钉/飞书) | 无 | 无 |
| **安全标识** | Shield 图标暗示 | SSL/权限/审计 明确列出 | 无 | 版本号信息 | 无 |
| **品牌展示** | 渐变圆 + Shield 图标 | 左侧大面积品牌区 | 顶部品牌图标 | 终端命令行风格 | 大圆角渐变图标 |
| **移动端适配** | 卡片全宽 + 边距 | 上下布局折叠 | 卡片全宽 | 卡片全宽 | 卡片全宽 |
### 3.7 各方案动效设计规范(汇总表)
| 动效维度 | 方案一「极光科技」 | 方案二「企业金融」 | 方案三「云端数据」 | 方案四「极简矩阵」 | 方案五「自然流动」 |
|:---------|:------------------|:------------------|:------------------|:------------------|:------------------|
| **背景动画** | 粒子连线 60fps | 无 | 云朵缓慢水平漂移 | 字符雨下落 | 三圆渐变漂移 |
| **入场动画** | 卡片淡入+上移 `600ms` | 左右滑入 `400ms` | 弹性下落 `500ms` | 打字机逐行 `80ms/行` | 柔和浮现 `700ms` |
| **入场缓动** | `cubic-bezier(0.16,1,0.3,1)` | `ease-out` | `cubic-bezier(0.34,1.56,0.64,1)` | `step-end` 风格 | `cubic-bezier(0.22,1,0.36,1)` |
| **输入框聚焦** | 蓝边框+光晕 `200ms` | 底线延展 `200ms` | 背景提亮+边框渐显 `150ms` | 绿边框亮度提升 `150ms` | 白边框透明度提升 `200ms` |
| **按钮 Hover** | 上移 `1px` + 阴影增强 | 背景加深 + 阴影 | 上移 `2px` + 渐变偏移 | 绿光晕染 + 背景变亮 | 缩放 `1.02` + 白影 |
| **错误反馈** | Shake 抖动 `300ms` | 边框变红 + 提示文字 | 边框变红 + 弹性抖动 | 红色终端错误信息 | 边框变红 + 光晕转红 |
| **特殊交互** | 鼠标粒子引力 | 信任指标依次淡入 | SSO 图标放大 | 光标闪烁 | 光晕呼吸脉动 |
| **动画性能** | `requestAnimationFrame` | CSS 过渡 | CSS 过渡/Framer Motion | `requestAnimationFrame` | CSS `transform` + `will-change` |
| **减弱动效** | 保留静态粒子 | 无需处理 | 保留静态布局 | 保留静态字符背景 | 保留静态模糊色块 |
---
#### 方案选型建议
五套方案并非"五选一"的关系,而是为不同的部署场景提供针对性选择:
- **对外技术产品**(如数据库管理工具、开发者平台)→ 方案一「极光科技」或方案四「极简矩阵」
- **金融/政企客户私有化部署** → 方案二「企业金融」
- **SaaS 订阅产品/云端服务** → 方案三「云端数据」
- **创意团队/设计公司内部系统** → 方案五「自然流动」
建议在系统设置中提供登录页主题切换功能,允许管理员根据企业 VI 选择合适的方案,或在不同的子域名/入口自动匹配不同风格(如 `dev.dataguard.com` 使用极客风,`finance.dataguard.com` 使用金融风)。所有方案共享 `useLogin` 和 `useTheme` hook,切换主题仅涉及 UI 层组件的替换,业务逻辑零改动。
@@ -0,0 +1,741 @@
## 4. 五套登录后主题设计方案
为满足不同用户群体在不同使用场景下的视觉偏好,本章为 SQL-Platform 设计五套完整的登录后主题方案。每套主题均包含完整的 CSS 变量规范、色彩心理学依据及典型应用场景说明。五套主题涵盖从专业沉稳到活力温暖的全方位视觉谱系,确保平台在各类企业环境中都能提供最佳的用户体验。
---
### 4.1 主题一「深蓝专业」— 默认深色增强版
**设计定位**:深蓝专业主题是在现有深色主题基础上的全面增强版本,定位为平台的默认主题。设计目标是在保持深色模式优势(降低眼部疲劳、突出内容)的同时,通过更纯正深邃的蓝色调提升整体品质感,并强化视觉层次与可读性。
**色彩设计理念**:该主题摒弃了原主题中偏灰的杂色倾向,将背景色收敛到更纯正的深蓝谱系。主背景色 `#070d19` 接近纯黑但带有微弱的蓝色相,营造出深邃而稳定的基底。次背景色 `#0f172a` 参考了 Slate 色板中最深沉的蓝色调,既保证了与主背景的层次区分,又维持了蓝色谱系的纯粹性。文字色彩采用 `#f8fafc`(极亮蓝白)作为主文字色,相比纯白色更具温润感,长时间阅读不易产生刺眼感。次文字色 `#94a3b8` 提供了充分的对比度,确保辅助信息的可读性。
**强调色体系**:蓝色系强调色选用 `#3b82f6`(Tailwind Blue 500),这是一个饱和度适中、辨识度高的蓝色,在深色背景上呈现明亮但不刺眼的视觉效果。状态色(成功、错误、警告)均经过深色背景适配,确保各色彩在深蓝底上都有足够的明度对比。
**适用场景**:作为默认主题,适用于绝大多数使用场景,特别适合技术团队日常操作、数据库管理、权限配置等需要长时间专注的工作界面。
```css
/* === 主题一:深蓝专业 ocean-deep === */
[data-theme="ocean-deep"] {
/* shadcn/ui 基础变量 */
--background: 222 47% 4%;
--foreground: 210 40% 98%;
--card: 222 47% 7%;
--card-foreground: 210 40% 98%;
--popover: 222 47% 7%;
--popover-foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 217 33% 17%;
--secondary-foreground: 210 40% 98%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
--accent: 217 33% 17%;
--accent-foreground: 210 40% 98%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 217 33% 17%;
--input: 217 33% 17%;
--ring: 217 91% 60%;
--radius: 0.5rem;
/* DataGuard 自定义变量 */
--dg-bg-primary: #070d19;
--dg-bg-secondary: #0f172a;
--dg-bg-tertiary: #172033;
--dg-bg-hover: #233049;
--dg-border: #1e293b;
--dg-border-light: #172033;
--dg-text-primary: #f8fafc;
--dg-text-secondary: #94a3b8;
--dg-text-muted: #64748b;
--dg-blue: #3b82f6;
--dg-green: #22c55e;
--dg-red: #ef4444;
--dg-orange: #f97316;
--dg-purple: #8b5cf6;
--dg-cyan: #06b6d4;
--dg-yellow: #eab308;
--dg-gradient-start: #3b82f6;
--dg-gradient-end: #2563eb;
}
```
---
### 4.2 主题二「浅色商务」— 企业办公风
**设计定位**:浅色商务主题面向白天办公环境优化,采用温暖的灰白色调替代常见的冷白背景,营造舒适、专业且不刺眼的工作界面。该主题适合不习惯深色模式的用户,或在明亮办公室环境中使用。
**色彩设计理念**:背景色摒弃了纯白的冷峻感,主背景色 `#fafaf9` 选用带有微弱暖色相的" zinc white",在显示器上呈现柔和的纸质感。次背景色 `#f5f5f4` 和三级背景色 `#e7e5e4` 构成自然的层次递进,避免了传统浅色主题中背景层次过于平淡的问题。边框色 `#d6d3d1` 同样带有暖色调,使界面分割更加自然和谐。文字色彩方面,主文字色 `#1c1917` 是带有微弱棕色相的深黑色,相比纯黑在暖白背景上对比度更舒适,避免了"黑字白纸"的刺眼感。
**强调色体系**:强调色选用稳重的 `#2563eb`(Tailwind Blue 600),这是一个偏深的标准蓝色,传达专业、可信赖的企业形象。状态色均调整为适合浅色背景的版本,成功绿 `#16a34a`、错误红 `#dc2626`、警告橙 `#ca8a04` 在浅色背景上均保持良好的可读性。
**适用场景**:适合白天办公环境、管理人员日常审批查看、客户演示投屏等场景。浅色界面在投影设备上的显示效果通常优于深色界面,因此该主题也是会议室演示的首选。
```css
/* === 主题二:浅色商务 light-professional === */
[data-theme="light-professional"] {
/* shadcn/ui 基础变量 */
--background: 40 9% 98%;
--foreground: 24 10% 10%;
--card: 40 9% 96%;
--card-foreground: 24 10% 10%;
--popover: 40 9% 96%;
--popover-foreground: 24 10% 10%;
--primary: 221 83% 53%;
--primary-foreground: 0 0% 100%;
--secondary: 40 6% 90%;
--secondary-foreground: 24 10% 10%;
--muted: 40 6% 90%;
--muted-foreground: 25 5% 45%;
--accent: 40 6% 90%;
--accent-foreground: 24 10% 10%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 24 6% 83%;
--input: 24 6% 83%;
--ring: 221 83% 53%;
--radius: 0.5rem;
/* DataGuard 自定义变量 */
--dg-bg-primary: #fafaf9;
--dg-bg-secondary: #f5f5f4;
--dg-bg-tertiary: #e7e5e4;
--dg-bg-hover: #d6d3d1;
--dg-border: #d6d3d1;
--dg-border-light: #e7e5e4;
--dg-text-primary: #1c1917;
--dg-text-secondary: #78716c;
--dg-text-muted: #a8a29e;
--dg-blue: #2563eb;
--dg-green: #16a34a;
--dg-red: #dc2626;
--dg-orange: #ea580c;
--dg-purple: #7c3aed;
--dg-cyan: #0891b2;
--dg-yellow: #ca8a04;
--dg-gradient-start: #2563eb;
--dg-gradient-end: #4f46e5;
}
```
---
### 4.3 主题三「午夜紫韵」— 高端紫色调
**设计定位**:午夜紫韵主题以深紫黑色为基底,搭配明亮的紫色渐变强调色,营造高端、神秘且富有科技感的视觉氛围。该主题适合作为给客户演示时的展示主题,能够传达产品的技术领先性和品质感。
**色彩设计理念**:主背景色 `#0a0612` 是带有明显紫色相的极深色,接近"午夜黑"的概念,为整个界面奠定了神秘而高贵的基调。次背景色 `#130c21` 和三级背景色 `#1c1330` 沿紫色谱系逐步提亮,形成清晰的层次结构。悬停背景色 `#2d1f4a` 是一个中等深度的紫色,在交互时能够产生明显的视觉反馈,同时保持整体色调的一致性。文字色彩方面,主文字色 `#faf5ff` 带有微弱的紫色调,使文字在深色背景上更加柔和雅致。
**强调色体系**:该主题的核心亮点在于紫色渐变强调体系。渐变起点 `#9333ea`(Tailwind Purple 600)到渐变终点 `#c084fc`(Tailwind Purple 400)构成了一段明亮且富有活力的紫色渐变,用于品牌标识、主要按钮和重要数据高亮。蓝色调整为偏紫的靛蓝色 `#818cf8`,绿色调整为翠绿色 `#34d399`,红色调整为玫红色 `#fb7185`,所有状态色都经过紫色调环境的协调适配。
**适用场景**:产品发布会演示、客户汇报展示、高端企业部署、技术峰会展台等需要彰显产品品质的正式场合。
```css
/* === 主题三:午夜紫韵 midnight-purple === */
[data-theme="midnight-purple"] {
/* shadcn/ui 基础变量 */
--background: 264 56% 4%;
--foreground: 280 100% 98%;
--card: 264 48% 8%;
--card-foreground: 280 100% 98%;
--popover: 264 48% 8%;
--popover-foreground: 280 100% 98%;
--primary: 270 91% 65%;
--primary-foreground: 0 0% 100%;
--secondary: 264 40% 20%;
--secondary-foreground: 280 100% 98%;
--muted: 264 40% 20%;
--muted-foreground: 280 30% 60%;
--accent: 264 40% 20%;
--accent-foreground: 280 100% 98%;
--destructive: 348 100% 72%;
--destructive-foreground: 264 56% 4%;
--border: 264 40% 20%;
--input: 264 40% 20%;
--ring: 270 91% 65%;
--radius: 0.5rem;
/* DataGuard 自定义变量 */
--dg-bg-primary: #0a0612;
--dg-bg-secondary: #130c21;
--dg-bg-tertiary: #1c1330;
--dg-bg-hover: #2d1f4a;
--dg-border: #2d1f4a;
--dg-border-light: #1c1330;
--dg-text-primary: #faf5ff;
--dg-text-secondary: #b794c7;
--dg-text-muted: #7c5a8a;
--dg-blue: #818cf8;
--dg-green: #34d399;
--dg-red: #fb7185;
--dg-orange: #fb923c;
--dg-purple: #c084fc;
--dg-cyan: #22d3ee;
--dg-yellow: #facc15;
--dg-gradient-start: #9333ea;
--dg-gradient-end: #c084fc;
}
```
---
### 4.4 主题四「松绿自然」— 护眼绿调
**设计定位**:松绿自然主题面向长时间使用平台的开发团队设计,以墨绿色为基调,利用绿色光谱对人眼最为友好的特性,最大程度降低长时间注视屏幕带来的视觉疲劳。
**色彩设计理念**:该主题的核心设计原则是"护眼优先"。主背景色 `#0a1210` 是带有明显绿色相的极深色,类似深夜松林深处的颜色,极低明度确保了基础视觉负荷的最小化。次背景色 `#111f1a`(深墨绿)和三级背景色 `#192c24`(墨绿灰)沿绿色谱系递进,在保持低明度的同时构建清晰的视觉层次。悬停背景色 `#253d32` 选用了中等深度的松绿色,交互时呈现自然、柔和的状态变化。边框色 `#253d32` 与悬停色共用,确保交互反馈与界面分割的视觉统一。主文字色 `#f0fdf4` 是极淡的薄荷白,在墨绿背景上明亮而不刺眼,具有极佳的阅读舒适度。
**强调色体系**:绿色系强调色选用 `#4ade80`(Tailwind Green 400),这是一个明亮、清新的绿色,在墨绿暗底上如同松针上的晨露般醒目。信息色 `#2dd4bf` 偏向青绿色,与主色调完美协调。蓝色调整为天蓝色 `#38bdf8`,在绿调环境中起到色彩平衡的作用,避免界面过于单调。
**适用场景**:开发团队日常长时间使用、DBA 持续监控数据库状态、运维人员驻屏值班等需要长时间面对屏幕的工作场景。
```css
/* === 主题四:松绿自然 pine-green === */
[data-theme="pine-green"] {
/* shadcn/ui 基础变量 */
--background: 150 25% 5%;
--foreground: 140 60% 97%;
--card: 150 25% 9%;
--card-foreground: 140 60% 97%;
--popover: 150 25% 9%;
--popover-foreground: 140 60% 97%;
--primary: 142 71% 45%;
--primary-foreground: 150 25% 5%;
--secondary: 150 20% 18%;
--secondary-foreground: 140 60% 97%;
--muted: 150 20% 18%;
--muted-foreground: 150 20% 55%;
--accent: 150 20% 18%;
--accent-foreground: 140 60% 97%;
--destructive: 0 90% 72%;
--destructive-foreground: 150 25% 5%;
--border: 150 20% 18%;
--input: 150 20% 18%;
--ring: 142 71% 45%;
--radius: 0.5rem;
/* DataGuard 自定义变量 */
--dg-bg-primary: #0a1210;
--dg-bg-secondary: #111f1a;
--dg-bg-tertiary: #192c24;
--dg-bg-hover: #253d32;
--dg-border: #253d32;
--dg-border-light: #192c24;
--dg-text-primary: #f0fdf4;
--dg-text-secondary: #86b8a0;
--dg-text-muted: #5a7c6b;
--dg-blue: #38bdf8;
--dg-green: #4ade80;
--dg-red: #f87171;
--dg-orange: #fb923c;
--dg-purple: #c084fc;
--dg-cyan: #2dd4bf;
--dg-yellow: #facc15;
--dg-gradient-start: #059669;
--dg-gradient-end: #10b981;
}
```
---
### 4.5 主题五「橙光活力」— 活力暖色调
**设计定位**:橙光活力主题以深棕暗橙为基底,搭配明亮的橙色渐变,传递温暖、积极、富有活力的品牌情绪。该主题面向年轻化团队和创新型企业,打破传统数据安全平台冰冷刻板的印象。
**色彩设计理念**:主背景色 `#110d08` 是带有明显棕色相的极深色,类似深烘焙咖啡豆的颜色,温暖而沉稳。次背景色 `#1d1610` 和三级背景色 `#2b1f16` 沿棕色谱系递进,营造出如同暖色调咖啡馆般的舒适感。悬停背景色 `#3d2d20` 是一个中等深度的暖棕色,交互反馈如同木材被温暖光线照亮。文字色彩方面,主文字色 `#fef7ed` 是极淡的暖白色,如同烛光下的纸张,在暗棕底上既明亮又温暖。
**强调色体系**:核心渐变从 `#ea580c`(Tailwind Orange 600)到 `#f59e0b`(Tailwind Amber 500),这是一段充满能量感的橙黄渐变,用于重要按钮、品牌标识和数据高亮时极具视觉冲击力。蓝色调整为柔和的 `#60a5fa`,在暖色调环境中起到中和平衡的作用。紫色调整为偏暖的 `#a78bfa`,与整体暖调和谐共存。
**适用场景**:互联网创业公司、年轻化的技术团队、创意型企业、产品内部分享会等追求活力与创新氛围的使用场景。
```css
/* === 主题五:橙光活力 orange-vitality === */
[data-theme="orange-vitality"] {
/* shadcn/ui 基础变量 */
--background: 30 35% 5%;
--foreground: 30 100% 97%;
--card: 30 30% 9%;
--card-foreground: 30 100% 97%;
--popover: 30 30% 9%;
--popover-foreground: 30 100% 97%;
--primary: 25 95% 53%;
--primary-foreground: 0 0% 100%;
--secondary: 30 25% 18%;
--secondary-foreground: 30 100% 97%;
--muted: 30 25% 18%;
--muted-foreground: 30 25% 55%;
--accent: 30 25% 18%;
--accent-foreground: 30 100% 97%;
--destructive: 0 80% 70%;
--destructive-foreground: 30 35% 5%;
--border: 30 25% 18%;
--input: 30 25% 18%;
--ring: 25 95% 53%;
--radius: 0.5rem;
/* DataGuard 自定义变量 */
--dg-bg-primary: #110d08;
--dg-bg-secondary: #1d1610;
--dg-bg-tertiary: #2b1f16;
--dg-bg-hover: #3d2d20;
--dg-border: #3d2d20;
--dg-border-light: #2b1f16;
--dg-text-primary: #fef7ed;
--dg-text-secondary: #d4a574;
--dg-text-muted: #9c7a54;
--dg-blue: #60a5fa;
--dg-green: #4ade80;
--dg-red: #f87171;
--dg-orange: #fb923c;
--dg-purple: #a78bfa;
--dg-cyan: #2dd4bf;
--dg-yellow: #fcd34d;
--dg-gradient-start: #ea580c;
--dg-gradient-end: #f59e0b;
}
```
---
### 4.6 各主题 CSS 变量规范对比表
下表汇总了五套主题的全部 DataGuard 自定义 CSS 变量值,便于开发实现时快速查阅与对比。
| CSS 变量 | 深蓝专业 | 浅色商务 | 午夜紫韵 | 松绿自然 | 橙光活力 |
|:---|:---|:---|:---|:---|:---|
| `--dg-bg-primary` | `#070d19` | `#fafaf9` | `#0a0612` | `#0a1210` | `#110d08` |
| `--dg-bg-secondary` | `#0f172a` | `#f5f5f4` | `#130c21` | `#111f1a` | `#1d1610` |
| `--dg-bg-tertiary` | `#172033` | `#e7e5e4` | `#1c1330` | `#192c24` | `#2b1f16` |
| `--dg-bg-hover` | `#233049` | `#d6d3d1` | `#2d1f4a` | `#253d32` | `#3d2d20` |
| `--dg-border` | `#1e293b` | `#d6d3d1` | `#2d1f4a` | `#253d32` | `#3d2d20` |
| `--dg-border-light` | `#172033` | `#e7e5e4` | `#1c1330` | `#192c24` | `#2b1f16` |
| `--dg-text-primary` | `#f8fafc` | `#1c1917` | `#faf5ff` | `#f0fdf4` | `#fef7ed` |
| `--dg-text-secondary` | `#94a3b8` | `#78716c` | `#b794c7` | `#86b8a0` | `#d4a574` |
| `--dg-text-muted` | `#64748b` | `#a8a29e` | `#7c5a8a` | `#5a7c6b` | `#9c7a54` |
| `--dg-blue` | `#3b82f6` | `#2563eb` | `#818cf8` | `#38bdf8` | `#60a5fa` |
| `--dg-green` | `#22c55e` | `#16a34a` | `#34d399` | `#4ade80` | `#4ade80` |
| `--dg-red` | `#ef4444` | `#dc2626` | `#fb7185` | `#f87171` | `#f87171` |
| `--dg-orange` | `#f97316` | `#ea580c` | `#fb923c` | `#fb923c` | `#fb923c` |
| `--dg-purple` | `#8b5cf6` | `#7c3aed` | `#c084fc` | `#c084fc` | `#a78bfa` |
| `--dg-cyan` | `#06b6d4` | `#0891b2` | `#22d3ee` | `#2dd4bf` | `#2dd4bf` |
| `--dg-yellow` | `#eab308` | `#ca8a04` | `#facc15` | `#facc15` | `#fcd34d` |
| `--dg-gradient-start` | `#3b82f6` | `#2563eb` | `#9333ea` | `#059669` | `#ea580c` |
| `--dg-gradient-end` | `#2563eb` | `#4f46e5` | `#c084fc` | `#10b981` | `#f59e0b` |
**shadcn/ui 基础变量对比表**
| HSL 变量 | 深蓝专业 | 浅色商务 | 午夜紫韵 | 松绿自然 | 橙光活力 |
|:---|:---|:---|:---|:---|:---|
| `--background` | `222 47% 4%` | `40 9% 98%` | `264 56% 4%` | `150 25% 5%` | `30 35% 5%` |
| `--foreground` | `210 40% 98%` | `24 10% 10%` | `280 100% 98%` | `140 60% 97%` | `30 100% 97%` |
| `--card` | `222 47% 7%` | `40 9% 96%` | `264 48% 8%` | `150 25% 9%` | `30 30% 9%` |
| `--primary` | `217 91% 60%` | `221 83% 53%` | `270 91% 65%` | `142 71% 45%` | `25 95% 53%` |
| `--secondary` | `217 33% 17%` | `40 6% 90%` | `264 40% 20%` | `150 20% 18%` | `30 25% 18%` |
| `--muted` | `217 33% 17%` | `40 6% 90%` | `264 40% 20%` | `150 20% 18%` | `30 25% 18%` |
| `--muted-foreground` | `215 20% 65%` | `25 5% 45%` | `280 30% 60%` | `150 20% 55%` | `30 25% 55%` |
| `--destructive` | `0 84% 60%` | `0 72% 51%` | `348 100% 72%` | `0 90% 72%` | `0 80% 70%` |
| `--border` | `217 33% 17%` | `24 6% 83%` | `264 40% 20%` | `150 20% 18%` | `30 25% 18%` |
| `--ring` | `217 91% 60%` | `221 83% 53%` | `270 91% 65%` | `142 71% 45%` | `25 95% 53%` |
---
### 4.7 主题切换实现方案
#### 4.7.1 CSS 变量组织方式
五套主题的 CSS 变量采用 **"分散定义、统一命名空间"** 的组织策略。每套主题的所有变量定义在一个独立的 `data-theme` 属性选择器下,避免变量冲突和覆盖混乱。
**文件结构建议**:
```
frontend/src/styles/
├── themes/
│ ├── index.css /* 主题入口文件,导入所有主题 */
│ ├── ocean-deep.css /* 主题一:深蓝专业 */
│ ├── light-professional.css /* 主题二:浅色商务 */
│ ├── midnight-purple.css /* 主题三:午夜紫韵 */
│ ├── pine-green.css /* 主题四:松绿自然 */
│ └── orange-vitality.css /* 主题五:橙光活力 */
└── index.css /* 现有主样式文件 */
```
**入口文件 `themes/index.css` 内容**:
```css
/* ===== SQL-Platform 主题系统入口 ===== */
/* 按加载顺序:默认主题(深蓝专业)无需 data-theme 属性 */
@import "./ocean-deep.css";
@import "./light-professional.css";
@import "./midnight-purple.css";
@import "./pine-green.css";
@import "./orange-vitality.css";
```
> **实现说明**:`ocean-deep.css` 同时提供 `:root` 级别的默认变量(确保无 `data-theme` 属性时回退到默认主题)和 `[data-theme="ocean-deep"]` 选择器下的显式定义,保证其他主题切换回来时变量能正确覆盖。
#### 4.7.2 useTheme Hook 增强实现
现有的 `useTheme` hook 需要从双值切换(dark/light)扩展为多主题选择。以下是增强后的实现方案:
```typescript
// frontend/src/hooks/useTheme.ts
import { useState, useEffect, useCallback } from 'react';
/** 支持的主题标识列表 */
export const THEMES = [
{ id: 'ocean-deep', label: '深蓝专业', description: '默认深色增强版' },
{ id: 'light-professional', label: '浅色商务', description: '企业办公风' },
{ id: 'midnight-purple', label: '午夜紫韵', description: '高端紫色调' },
{ id: 'pine-green', label: '松绿自然', description: '护眼绿调' },
{ id: 'orange-vitality', label: '橙光活力', description: '活力暖色调' },
] as const;
export type ThemeId = typeof THEMES[number]['id'];
const STORAGE_KEY = 'sqlp-theme-id';
const DEFAULT_THEME: ThemeId = 'ocean-deep';
/**
* 从 localStorage 读取保存的主题
*/
function getSavedTheme(): ThemeId {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved && THEMES.some(t => t.id === saved)) {
return saved as ThemeId;
}
} catch {
// localStorage 不可用时忽略
}
return DEFAULT_THEME;
}
/**
* 检测系统色彩偏好
*/
function getSystemPreference(): 'dark' | 'light' {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
/**
* 根据系统偏好映射默认主题
*/
function getDefaultBySystem(): ThemeId {
return getSystemPreference() === 'dark' ? 'ocean-deep' : 'light-professional';
}
export function useTheme() {
const [theme, setThemeState] = useState<ThemeId>(() => {
const saved = getSavedTheme();
return saved || getDefaultBySystem();
});
/**
* 切换主题的核心方法
*/
const setTheme = useCallback((newTheme: ThemeId) => {
setThemeState(newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
try {
localStorage.setItem(STORAGE_KEY, newTheme);
} catch {
// localStorage 不可用时静默处理
}
}, []);
/**
* 初始化主题:首次加载时应用
*/
useEffect(() => {
const saved = getSavedTheme();
const initial = saved || getDefaultBySystem();
document.documentElement.setAttribute('data-theme', initial);
setThemeState(initial);
}, []);
/**
* 监听系统主题变化,仅在用户未手动选择主题时自动切换
*/
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => {
// 检查用户是否已手动设置过主题
const hasUserPreference = localStorage.getItem(STORAGE_KEY) !== null;
if (!hasUserPreference) {
const newTheme = e.matches ? 'ocean-deep' : 'light-professional';
setTheme(newTheme);
}
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [setTheme]);
return { theme, setTheme, themes: THEMES };
}
```
#### 4.7.3 主题预览组件设计
AppearanceSettings 页面需新增主题选择区域,包含五张主题预览卡片。每张卡片展示主题的核心色彩样本和简要描述。
```typescript
// frontend/src/components/settings/ThemePreviewCard.tsx
import { Check } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ThemeId } from '@/hooks/useTheme';
interface ThemePreviewCardProps {
themeId: ThemeId;
label: string;
description: string;
colors: {
bg: string;
secondary: string;
accent: string;
text: string;
};
isActive: boolean;
onClick: () => void;
}
export function ThemePreviewCard({
label,
description,
colors,
isActive,
onClick,
}: ThemePreviewCardProps) {
return (
<button
onClick={onClick}
className={cn(
"relative flex flex-col gap-2 rounded-lg border-2 p-3 transition-all duration-200",
"hover:shadow-md hover:-translate-y-0.5",
isActive
? "border-dg-blue ring-2 ring-dg-blue/20"
: "border-dg-border hover:border-dg-border-light"
)}
aria-pressed={isActive}
>
{/* 预览色块区域 */}
<div
className="h-16 w-full rounded-md flex items-end justify-end p-1.5 gap-1"
style={{ backgroundColor: colors.bg }}
>
<div
className="w-4 h-4 rounded-sm"
style={{ backgroundColor: colors.secondary }}
/>
<div
className="w-4 h-4 rounded-sm"
style={{ backgroundColor: colors.accent }}
/>
<div
className="w-4 h-4 rounded-sm"
style={{ backgroundColor: colors.text }}
/>
</div>
{/* 文字信息 */}
<div className="text-left">
<p className="text-sm font-medium text-dg-text-primary">{label}</p>
<p className="text-xs text-dg-text-muted">{description}</p>
</div>
{/* 选中标记 */}
{isActive && (
<div className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-dg-blue rounded-full flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
</div>
)}
</button>
);
}
```
**AppearanceSettings 页面中的主题选择区域布局**:
```tsx
// 在 AppearanceSettings 页面中使用
import { useTheme, THEMES } from '@/hooks/useTheme';
import { ThemePreviewCard } from '@/components/settings/ThemePreviewCard';
// 各主题预览色值配置
const THEME_PREVIEW_COLORS: Record<string, { bg: string; secondary: string; accent: string; text: string }> = {
'ocean-deep': { bg: '#070d19', secondary: '#0f172a', accent: '#3b82f6', text: '#f8fafc' },
'light-professional': { bg: '#fafaf9', secondary: '#f5f5f4', accent: '#2563eb', text: '#1c1917' },
'midnight-purple': { bg: '#0a0612', secondary: '#130c21', accent: '#c084fc', text: '#faf5ff' },
'pine-green': { bg: '#0a1210', secondary: '#111f1a', accent: '#4ade80', text: '#f0fdf4' },
'orange-vitality': { bg: '#110d08', secondary: '#1d1610', accent: '#fb923c', text: '#fef7ed' },
};
function ThemeSelector() {
const { theme: activeTheme, setTheme } = useTheme();
return (
<div className="space-y-3">
<h3 className="text-sm font-medium text-dg-text-primary">界面主题</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
{THEMES.map((t) => (
<ThemePreviewCard
key={t.id}
themeId={t.id}
label={t.label}
description={t.description}
colors={THEME_PREVIEW_COLORS[t.id]}
isActive={activeTheme === t.id}
onClick={() => setTheme(t.id)}
/>
))}
</div>
</div>
);
}
```
#### 4.7.4 localStorage 持久化方案
主题持久化遵循以下策略:
| 策略项 | 说明 |
|:---|:---|
| **存储键名** | `sqlp-theme-id`,与现有 `theme` 键分离,避免升级冲突 |
| **存储值** | 主题标识字符串,如 `ocean-deep`、`light-professional` |
| **回退逻辑** | 读取不到有效值时,检测 `prefers-color-scheme` 系统偏好,暗色默认 `ocean-deep`,亮色默认 `light-professional` |
| **版本兼容** | 首次升级时,若检测到旧版 `theme: "dark"` 键,自动迁移至 `ocean-deep`;`theme: "light"` 迁移至 `light-professional`,随后删除旧键 |
| **异常处理** | localStorage 不可用时(隐私模式、存储已满),降级为仅内存状态,不抛出错误 |
| **跨设备同步** | 通过 `storage` 事件监听同源其他标签页的主题变更,实现多标签页同步: |
```typescript
/**
* 跨标签页主题同步
*/
useEffect(() => {
const handler = (e: StorageEvent) => {
if (e.key === STORAGE_KEY && e.newValue) {
const newTheme = e.newValue as ThemeId;
if (THEMES.some(t => t.id === newTheme)) {
setThemeState(newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
}
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
}, []);
```
#### 4.7.5 系统主题偏好检测
平台需响应操作系统的 `prefers-color-scheme` 媒体查询,提供智能化的默认主题推荐。
**检测逻辑**:
```typescript
// 检测系统暗色/浅色偏好
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersLight = window.matchMedia('(prefers-color-scheme: light)').matches;
```
**偏好映射规则**:
| 系统偏好 | 默认推荐主题 | 推荐逻辑 |
|:---|:---|:---|
| `prefers-color-scheme: dark` | **深蓝专业** (`ocean-deep`) | 暗色系统偏好用户通常偏好深色界面 |
| `prefers-color-scheme: light` | **浅色商务** (`light-professional`) | 亮色系统偏好用户通常偏好浅色界面 |
| 未指定(no-preference) | **深蓝专业** (`ocean-deep`) | 默认采用平台默认主题 |
**动态响应机制**:
```typescript
/**
* 系统主题偏好动态响应
* 仅在用户未手动设置主题时自动跟随系统
*/
useEffect(() => {
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
const lightQuery = window.matchMedia('(prefers-color-scheme: light)');
const handleChange = () => {
const hasUserPreference = localStorage.getItem(STORAGE_KEY) !== null;
if (hasUserPreference) return; // 用户已手动选择,不自动切换
if (darkQuery.matches) {
setTheme('ocean-deep');
} else if (lightQuery.matches) {
setTheme('light-professional');
}
};
darkQuery.addEventListener('change', handleChange);
lightQuery.addEventListener('change', handleChange);
return () => {
darkQuery.removeEventListener('change', handleChange);
lightQuery.removeEventListener('change', handleChange);
};
}, [setTheme]);
```
**重要原则**:系统偏好仅影响**首次访问**或**清除 localStorage 后**的默认主题选择。一旦用户手动选择了主题,该选择将被持久化并优先于系统偏好,避免在用户主动选择后因系统时间变化(如 macOS 自动切换暗色模式)导致界面意外变更。
#### 4.7.6 切换过渡动效
主题切换时应提供平滑的视觉过渡,避免色彩突变带来的不适感。
```css
/* 全局主题过渡效果 —— 添加到 index.css */
html {
/* 对所有 CSS 变量变化应用 200ms 缓动过渡 */
transition: background-color 200ms ease, color 200ms ease;
}
html *, html *::before, html *::after {
/* 谨慎使用:仅对关键属性过渡,避免性能问题 */
transition: background-color 200ms ease,
border-color 200ms ease,
color 150ms ease;
}
/* 可选:切换瞬间的淡入效果 */
[data-theme] {
animation: themeFadeIn 200ms ease;
}
@keyframes themeFadeIn {
from { opacity: 0.92; }
to { opacity: 1; }
}
```
**性能优化建议**:全局 `*` 过渡规则可能影响大型列表渲染性能。生产环境中建议仅对需要过渡的元素添加过渡规则,或通过 JavaScript 在切换主题时临时添加过渡 class,切换完成后再移除:
```typescript
/**
* 带过渡动画的主题切换
*/
const setThemeWithTransition = useCallback((newTheme: ThemeId) => {
const root = document.documentElement;
root.classList.add('theme-transitioning');
setTheme(newTheme);
// 300ms 后移除过渡 class
setTimeout(() => {
root.classList.remove('theme-transitioning');
}, 300);
}, [setTheme]);
```
```css
/* 仅在切换时启用过渡 */
html.theme-transitioning,
html.theme-transitioning * {
transition: background-color 200ms ease,
border-color 200ms ease,
color 150ms ease !important;
}
```
以上实现方案确保五套主题能够稳定、高效、优雅地在平台中运行,为用户提供丰富的个性化选择。
@@ -0,0 +1,533 @@
## 5. 核心功能页面优化方案
本章针对 SQL-Platform 三个核心功能模块——审批变更(工单系统)、查询数据、系统设置——进行逐页拆解分析,提出可落地的 UI/UX 优化方案。每个方案均包含具体的组件选型、布局参数、交互细节与视觉规范,确保开发团队能够直接转化为实现任务。
---
### 5.1 审批变更工单系统优化
审批变更是平台的核心业务流,涉及工单列表、工单创建、工单详情三个主要页面。当前实现存在布局松散、信息层级不清、操作流程割裂等问题。以下按页面逐一给出优化方案。
#### 5.1.1 工单列表页优化
**组件选型与布局重构**
将原生 `<table>` 全面替换为 `@tanstack/react-table` + `shadcn/ui DataTable` 组合。DataTable 内置列排序、列过滤、行选择、分页等能力,可显著减少 742 行中的手写逻辑。表格容器采用 `rounded-xl border border-border/60 bg-card` 样式,阴影参数为 `shadow-sm`。
**状态卡片升级**
原状态卡片(全部/待审批/执行中/完成/失败)改为 Stats Cards。每张卡片包含:状态图标(`Lucide`)、工单数量(`text-2xl font-bold`)、环比趋势箭头(绿色上升/红色下降)、环比百分比(`text-xs text-muted-foreground`)。卡片使用 `cn()` 动态绑定激活状态的边框色——激活时边框为 `ring-2 ring-primary/40 border-primary`,未激活为 `border-transparent hover:border-border/80`。卡片网格采用 `grid grid-cols-5 gap-4`,在 < 1024px 下退化为 `grid-cols-3` + `grid-cols-2`。
**高级筛选栏**
在状态卡片下方新增折叠式筛选栏,包含以下控件:
| 筛选项 | 组件 | 说明 |
|---------|------|------|
| 实例 | `Select`(多选) | 从实例列表异步加载 |
| 数据库类型 | `Select`(多选) | MySQL/PostgreSQL/MongoDB 等 |
| 提交人 | `Input` + `Avatar` | 支持用户名/邮箱搜索 |
| 时间范围 | `DateRangePicker` | shadcn/ui 官方组件 |
| 优先级 | `ToggleGroup` | 高/中/低三态切换 |
筛选栏默认折叠,点击"高级筛选"按钮展开,展开高度动画 `duration-300 ease-in-out`。所有筛选条件通过 URL query params 同步,支持刷新后状态保持。
**表格交互增强**
- **优先级标识**:每行左侧增加 4px 宽色条,高=`bg-red-500`、中=`bg-amber-500`、低=`bg-slate-400`,使用绝对定位贴左边框。
- **行 Hover 效果**:`hover:bg-muted/60`,同时左侧出现 `border-l-2 border-primary` 指示条,过渡 `transition-colors duration-150`。
- **操作列优化**:将分散的查看/通过/拒绝/执行/撤销按钮收缩为图标按钮组(`Eye`, `CheckCircle`, `XCircle`, `Play`, `RotateCcw`)。当操作超过 3 个时,溢出部分收入 `DropdownMenu`。按钮使用 `variant="ghost" size="icon"`。
- **批量操作**:顶部新增批量操作栏,选中行后出现(`AnimatePresence` 滑入),包含"批量审批"、"批量拒绝"、"导出选中"三个主操作,危险操作使用 `variant="destructive"`。
- **列自定义**:列头右键菜单支持"隐藏此列"、"重置列宽",列宽拖拽使用 `react-resizable` 实现,拖拽手柄 4px 宽,hover 时显示 `bg-primary/30`。
**搜索与分页**
搜索框使用 `Input` + `Search` 图标,支持 `Cmd+K` 快捷键聚焦。分页组件采用 `shadcn/ui Pagination`,页码按钮尺寸 `h-9 w-9`,当前页 `bg-primary text-primary-foreground`。每页条数可选 10/25/50/100。
---
#### 5.1.2 工单创建页优化
**布局重构:左右分栏**
将当前 1192 行的单栏布局改为左右分栏:左侧编辑器区占 `w-3/5`(60%),右侧辅助信息面板占 `w-2/5`(40%),中间分隔线 `border-r border-border/60`。两栏均独立滚动,避免编辑器滚动时右侧信息丢失。分栏在 < 1280px 下退化为上下堆叠布局。
**步骤指示器(Step Indicator)**
在分栏顶部增加横向步骤条,共三步:
```
① 填写信息 ──→ ② SQL 审核 ──→ ③ 提交审批
```
步骤条使用 `shadcn/ui` 风格的自定义组件。已完成步骤圆圈 `bg-primary text-primary-foreground`,当前步骤 `bg-primary ring-4 ring-primary/20`,未到达步骤 `bg-muted text-muted-foreground`。连线使用 `h-0.5 w-16`,已完成段 `bg-primary`,未完成段 `bg-border`。步骤切换允许回退(已填写内容保留),但前进需通过当前步骤的验证(如 SQL 审核必须通过才能到第三步)。
**左侧编辑器区**
- **顶部工具栏**:实例选择 + 数据库选择使用 `Popover` + `Command` 组合(支持搜索筛选),定时执行使用 `Switch` + `DateTimePicker`,格式化按钮(`AlignLeft` 图标)使用 `variant="outline" size="sm"`。工具栏使用 `flex items-center gap-3 p-3 border-b border-border/60 bg-muted/30`。
- **SQL 编辑器**:保留 CodeMirror 6,新增配置:行号显示(`lineNumbers()`)、迷你地图(`@codemirror/view showMinimap` 或自定义 SVG 缩略图)、当前行高亮(`highlightActiveLineGutter()`)。编辑器高度 `h-[480px]`,边框 `border-0`(融入容器)。
- **SQL 模板快捷插入**:编辑器左下角增加"模板"按钮,点击弹出 `Popover`,内含分类模板列表(DDL/DML/查询),点击后 `editor.dispatch()` 插入模板 SQL。
**右侧辅助面板**
- **审核结果卡片**:使用三个独立卡片分别展示 Error(`bg-red-50 border-red-200`)、Warning(`bg-amber-50 border-amber-200`)、Info(`bg-blue-50 border-blue-200`)。每张卡片包含:数量 badge(`rounded-full px-2 py-0.5 text-xs font-medium`)、发现项列表(每项包含行号、规则名、描述)、展开/折叠按钮。Error 卡片默认展开,其他折叠。
- **变更原因输入**:将 `<textarea>` 替换为轻量级 Markdown 编辑器(`react-simplemde-editor` 或自定义实现),支持粗体、斜体、代码块、列表快捷按钮。高度 `min-h-[120px]`,占位符文本"请描述本次变更的业务原因和影响范围..."
- **工单预览**:在提交按钮左侧增加"预览"按钮,点击弹出 `Dialog`(宽 `max-w-3xl`),展示完整工单信息的只读视图,包括 SQL 摘要、变更原因、元数据。
- **自动保存草稿**:每 30 秒将表单状态序列化到 `localStorage`,键名为 `ticket_draft_${userId}`。页面加载时检测草稿,若有则提示"检测到未提交的草稿,是否恢复?"(`Toast` + 操作按钮)。提交成功后清除草稿。
---
#### 5.1.3 工单详情页优化
**头部 Hero Section**
将原头部改为 Hero Section 风格:
- 背景:`bg-gradient-to-r from-primary/5 via-primary/3 to-transparent`,底部边框 `border-b border-border/60`。
- 标题:`text-3xl font-bold tracking-tight`,状态 badge 紧跟标题右侧,badge 尺寸加大(`text-sm px-3 py-1`)。
- 元信息行:`flex gap-6 text-sm text-muted-foreground`,包含提交人(Avatar + 用户名)、提交时间(`Calendar` 图标)、实例名(`Database` 图标)、数据库(`Table` 图标)。
- 操作按钮组:头部右侧放置主操作按钮,审批通过用 `variant="default"`(`CheckCircle` 图标),拒绝用 `variant="destructive"`(`XCircle` 图标),执行用 `variant="outline"`(`Play` 图标)。操作按钮根据当前状态条件渲染(如已完成的工单不显示"通过"按钮)。
**审批流程 Timeline**
将原圆形+连线流程改为横向 Timeline:
```
[创建]──→[待审批]──→[已通过]──→[执行中]──→[已完成]
张三 李四 王五 系统 系统
10:23 14:05 16:30 16:31 16:33
```
- 节点形状:操作步骤使用圆形(`w-10 h-10 rounded-full`),判断步骤(如"审核通过?")使用菱形(CSS `rotate(45deg)` 实现,内部图标反向旋转)。
- 当前节点:`ring-4 ring-primary/30 bg-primary text-white`,带脉冲动画(`animate-pulse` 或自定义 CSS keyframes `scale 1→1.05→1`)。
- 已完成连线:`h-0.5 bg-primary`。
- 未完成连线:`h-0.5 bg-border border-dashed`。
- 鼠标悬停节点时,弹出 `Tooltip` 显示步骤详情(操作人、时间、备注)。
**内容区域**
- **SQL 内容区**:只读编辑器上方增加工具栏,包含"复制"(`Copy` 图标,点击后图标变为 `Check` 持续 2 秒)、"下载 SQL"(`Download` 图标)、"全屏查看"(`Maximize` 图标)。
- **执行结果 Dashboard**:将原本平铺的执行结果改为 Dashboard 风格,使用 `grid grid-cols-4 gap-4` 布局:
- 执行状态:大图标 + 状态文字,成功 `text-green-600`、失败 `text-red-600`。
- 影响行数:`text-3xl font-bold` + "行" 单位。
- 执行耗时:`text-3xl font-bold` + "秒" 单位。
- 警告数:如有则显示 badge,无则显示 "-" 并置灰。
- **回滚 SQL**:使用 `Accordion` 包裹,默认折叠,类型为 `type="single" collapsible`。展开后显示只读编辑器 + 复制按钮。
- **审批意见输入**:在审批流程下方新增意见输入区,使用 `Textarea` + 提交按钮。支持 `@` 提及功能(输入 `@` 后弹出用户列表 `Popover`,使用 `Command` 组件筛选)。已提交的审批意见以评论卡片形式展示,包含 Avatar、用户名、时间、意见内容。
- **关联工单 Carousel**:将关联工单列表改为横向滚动卡片组,每张卡片显示工单标题、状态 badge、最后更新时间。使用 `scroll-snap-type: x mandatory` 实现平滑滚动,左右箭头按钮控制滚动。
- **操作日志时间轴**:左侧竖线 `w-px bg-border absolute left-4 top-0 bottom-0`,每个日志节点包含:圆点(操作类型颜色编码)、时间(`text-xs text-muted-foreground`)、操作描述、操作人 Avatar。
---
#### 5.1.4 审批流程可视化优化
**流程图实现方案**
推荐采用自定义 SVG 方案(相比 Mermaid.js 更轻量、更易定制样式)。流程图容器使用 `aspect-[16/9] max-h-[360px]`,SVG 内部使用 `<defs>` 定义箭头标记和渐变色。
**节点设计**
| 节点类型 | 形状 | 尺寸 | 颜色 |
|---------|------|------|------|
| 开始/结束 | 圆角矩形(rx=20) | 100×48px | `fill: hsl(var(--primary))` |
| 操作步骤 | 圆角矩形(rx=8) | 140×56px | `fill: hsl(var(--card)), stroke: hsl(var(--border))` |
| 判断节点 | 菱形(polygon) | 112×80px | `fill: hsl(var(--amber-50)), stroke: hsl(var(--amber-400))` |
| 当前步骤 | 任意形状 + 脉冲 | 基础尺寸 | `ring` + `animate-pulse` |
节点文本居中显示,使用 `text-sm font-medium fill-foreground`。当前步骤节点的边框使用 `stroke: hsl(var(--primary))`,线宽 `stroke-width: 2.5`,并附加 SVG `<animate>` 脉冲效果(`opacity 0.6→1→0.6`,周期 2s)。
**连线设计**
- 正常路径:`stroke: hsl(var(--primary))`,`stroke-width: 2`,带箭头标记。
- 回退路径(如"拒绝"分支):`stroke: hsl(var(--destructive))`,`stroke-width: 2`,`stroke-dasharray: 6 3`,箭头标记颜色同步。
- 条件分支标签:连线中点放置文本标签,如"通过"/"拒绝",使用 `text-xs fill-muted-foreground`,背景白色矩形 `fill: hsl(var(--background))` 防止线穿过文字。
- 鼠标悬停连线时,线宽动画增至 `stroke-width: 3`,同时显示 `Tooltip` 说明该路径的触发条件。
**交互增强**
- 点击任意节点,右侧(或下方)弹出详情面板,显示该步骤的完整信息(操作人、操作时间、耗时、备注、相关规则)。
- 流程图支持缩放(鼠标滚轮,范围 0.5x-2x)和平移(拖拽),使用 `transform` 属性实现。
- 提供"重置视图"按钮,点击后平滑动画回归初始缩放和位置。
- 当前正在进行的步骤,节点右上角显示旋转的 `Loader2` 图标(`animate-spin`)。
---
### 5.2 查询数据页面优化
查询数据页面(QueriesPage.tsx,896 行)是开发人员最高频使用的功能模块。当前页面在编辑器体验、结果展示、导航效率方面均有较大提升空间。
#### 5.2.1 SQL 编辑器优化
**主题同步**
CodeMirror 6 编辑器主题通过 `Compartment` 动态切换,监听 `theme` 状态变化:
```typescript
const themeCompartment = new Compartment();
// 深色主题使用 oneDark,浅色使用 lightTheme
const themeExtension = theme === 'dark' ? oneDark : lightTheme;
view.dispatch({ effects: themeCompartment.reconfigure(themeExtension) });
```
编辑器背景色使用 `bg-card`,确保与页面主题无缝融合。光标颜色使用 `caret-color: hsl(var(--primary))`。
**全屏编辑模式**
监听 `F11` 按键(`preventDefault` 阻止浏览器默认全屏),触发自定义全屏:编辑器容器使用 `fixed inset-0 z-50 bg-background` 定位,动画 `animate-in zoom-in-95 duration-200`。全屏模式下隐藏侧边栏,工具栏收缩为单行。退出方式:再次按 `F11` 或点击右上角 `Minimize` 图标按钮。使用 `useHotkeys` hook 管理快捷键。
**代码折叠**
启用 CodeMirror 6 内置的 `@codemirror/language foldGutter` 扩展,折叠指示器使用 `&bull;` 或自定义图标(`ChevronDown`/`ChevronRight`)。折叠区域背景色使用 `bg-muted/30` 以视觉区分。
**多标签页支持**
在编辑器顶部增加标签页栏,高度 `h-10`,样式类似 VS Code:
- 活动标签:`bg-card border-t-2 border-primary text-foreground`。
- 非活动标签:`bg-muted/50 border-t-2 border-transparent text-muted-foreground hover:bg-muted`。
- 标签内容:查询前 20 个字符 + 未保存指示点(`bg-primary rounded-full w-2 h-2`)。
- 关闭按钮:标签 hover 时显示 `X` 图标,`size="sm" variant="ghost"`。
- 新建标签:右侧 `+` 按钮,点击后创建空查询标签。
- 标签状态存储在 `zustand` store 中,切换标签时通过 `editor.dispatch` 替换文档内容。
**查询参数化**
支持 `:paramName` 语法,编辑器通过自定义 `MatchDecorator` 高亮参数(`background: hsl(var(--primary)/0.15), border-radius: 3px`)。编辑器下方增加参数输入区:自动解析 SQL 中的参数,为每个参数生成 `Input` 字段(标签 = 参数名,占位符 = 数据类型推断)。参数值随查询一起提交到后端。
---
#### 5.2.2 结果展示优化
**DataTable 替换**
将原生结果表格替换为 `shadcn/ui DataTable`(`@tanstack/react-table`)。关键配置:
- `enableColumnResizing: true`:列宽可拖拽调整,拖拽手柄在列头右侧边缘,hover 时显示 `w-1 h-full bg-primary/30`。
- `enableSorting: true`:点击列头排序,排序状态循环:无 → 升序 → 降序 → 无。排序图标使用 `ArrowUpDown`,激活时根据方向切换为 `ArrowUp` 或 `ArrowDown`。
- `enableRowSelection: true`:支持行选择,批量导出。
**列头固定(Sticky Header)**
结果表格容器设置 `max-h-[480px] overflow-auto`,表头使用 `sticky top-0 z-10 bg-card border-b` 固定,确保纵向滚动时列头始终可见。
**大数据量虚拟滚动**
当结果行数超过 1000 行时,启用 `@tanstack/react-virtual` 虚拟滚动。虚拟滚动配置:
- `estimateSize: () => 40`:每行预估高度 40px。
- `overscan: 10`:上下各预渲染 10 行。
- 行元素使用绝对定位 `position: absolute; top: 0; transform: translateY(${virtualRow.start}px)`。
**单元格内容预览**
- 文本/数字列:超出列宽时显示 `truncate`,hover 时弹出 `Popover` 展示完整内容,Popover 最大宽度 `max-w-sm`,支持文本选择。
- JSON 列:使用 `lucide-react` 的 `Braces` 图标标识,内容折叠显示 `{...}` 或 `[...]`(包含元素个数 badge)。点击展开后使用 `react-json-view` 或自定义树形组件格式化展示,支持层级折叠。
- NULL 值:特殊样式 `text-muted-foreground italic` 显示 "NULL"。
**结果导出**
工具栏增加导出 `DropdownMenu`,包含:
| 格式 | 说明 |
|------|------|
| CSV | 逗号分隔,包含 BOM 解决中文乱码 |
| JSON | 对象数组格式,字段名作为 key |
| Excel | 通过 `xlsx` 库生成 `.xlsx` 文件 |
导出范围支持:当前页 / 全部结果 / 选中行。大数据量导出(>10,000 行)时显示进度条 `Dialog`。
**结果图表视图**
在结果表格上方增加视图切换 `ToggleGroup`:`Table` / `BarChart` / `LineChart`。
- 柱状图:使用 `recharts` 的 `<BarChart>`,选择两列(X 轴 + Y 轴)后渲染。X 轴标签旋转 45° 防止重叠。
- 折线图:使用 `<LineChart>`,适合时间序列数据。支持多系列(选择多个数值列)。
- 图表容器高度 `h-[360px]`,空状态显示 `BarChart3` 图标 + "请选择数据列生成图表" 提示。
- 图表配置面板:X 轴选择、Y 轴选择(可多选)、图表标题输入。
---
#### 5.2.3 侧边栏导航优化
**Schema 树搜索过滤**
Schema 侧边栏顶部增加搜索输入框(`Input size="sm"` + `Search` 图标),实时过滤数据库/表/字段名。过滤逻辑:
- 匹配数据库名:展示该数据库下所有表。
- 匹配表名:展示该表及其所在数据库(其他数据库折叠)。
- 匹配字段名:展示该字段及其所属表和数据库。
使用 `fuse.js` 实现模糊搜索,配置 `threshold: 0.3, keys: ['name']`。
**表节点增强**
每个表节点左侧显示 `Table2` 图标(`w-4 h-4 text-muted-foreground`),右侧显示行数 badge(`rounded-full bg-muted px-1.5 py-0 text-[10px] text-muted-foreground`)。行数从 `INFORMATION_SCHEMA` 或后端缓存获取,显示 "~" 前缀表示近似值。
**收藏表功能**
表节点 hover 时显示星标按钮(`Star` 图标,`size="sm" variant="ghost"`),点击切换收藏状态。已收藏的表显示实心 `Star` 图标(`fill: hsl(var(--amber-400))`)。收藏表固定在侧边栏顶部"收藏"分组下,分组标题使用 `text-xs font-semibold text-muted-foreground uppercase tracking-wider`。
**最近访问表**
在"收藏"分组下方增加"最近访问"分组,最多展示 10 个最近点击过的表,按访问时间倒序。每个表项显示访问时间相对文本(`2分钟前`)。使用 `lru-cache` 或简单的 `Map` 实现客户端缓存,持久化到 `localStorage`。
**拖拽插入 SQL**
实现从 Schema 树拖拽表名/字段名到编辑器:
- 拖拽开始:设置 `dataTransfer.setData('text/plain', tableName)`,拖拽图像为半透明标签显示表名。
- 拖拽经过编辑器:显示插入位置指示线。
- 拖拽释放:根据拖拽对象类型插入不同内容:
- 表名:插入 `` `tableName` ``
- 字段名:插入 `` `tableName`.`columnName` ``
- 数据库名:插入 `` `databaseName` ``
使用 CodeMirror 6 的 `dropCursor` 扩展显示插入位置。
---
#### 5.2.4 历史记录优化
**分类标签页**
将历史记录抽屉中的列表改为 `shadcn/ui Tabs` 分类:
- 全部(`History` 图标):所有查询记录。
- 成功(`CheckCircle` 图标,绿色):执行成功的查询。
- 失败(`XCircle` 图标,红色):执行失败的查询。
- 收藏(`Star` 图标,琥珀色):用户收藏的查询。
每个标签显示记录数量 badge。标签切换使用 `AnimatePresence` 实现内容淡入淡出。
**记录项增强**
每条历史记录显示:
- 左侧:状态圆点(成功 `bg-green-500`、失败 `bg-red-500`),直径 8px。
- 标题行:查询前 40 个字符 + 执行时间 badge(`rounded-md bg-muted px-1.5 py-0.5 text-xs`,如 "23ms"、"1.2s")。
- 展开/折叠:点击记录行展开 SQL 预览(`ChevronDown` / `ChevronRight` 图标指示),展开的 SQL 使用只读 CodeMirror 实例(高度 `max-h-[120px]`)或 `<pre>` 标签 + 语法高亮。
- 操作按钮组(hover 显示):"运行"(`Play` 图标,将 SQL 插入编辑器并执行)、"插入"(`CornerDownLeft` 图标,仅插入编辑器)、"收藏"(`Star` 图标)、"删除"(`Trash2` 图标,`variant="ghost"` 且 hover 变红)。
**一键对比功能**
在历史记录操作菜单中增加"对比"选项。选择两条记录后,弹出对比 `Dialog`(宽 `max-w-5xl`),使用左右分栏展示两条 SQL 的差异。差异高亮使用 `diff-match-patch` 库或自定义实现:
- 相同行:正常显示。
- 删除行:背景 `bg-red-50`,左侧红色竖线标记。
- 新增行:背景 `bg-green-50`,左侧绿色竖线标记。
- 修改行:字符级差异高亮,删除字符 `bg-red-200 line-through`,新增字符 `bg-green-200`。
**文件夹/标签组织**
允许用户创建自定义文件夹("日常巡检"、"业务报表"等)来组织历史查询。文件夹使用侧边栏树形结构展示,支持:
- 新建文件夹:输入名称,使用 `Dialog` + `Input`。
- 拖拽排序:文件夹和历史记录均支持拖拽重新排序。
- 颜色标签:每个文件夹可选颜色标签(8 种预设色),用于快速视觉识别。
- 搜索:文件夹和历史记录支持全文搜索(SQL 内容 + 备注)。
---
### 5.3 系统设置页面优化
当前设置页面(SettingsPage.tsx 仅 56 行)结构过于简单,PlatformConfigPage.tsx 和 AppearanceSettings.tsx 也存在分类不清晰、交互不一致的问题。
#### 5.3.1 设置分类重组
**新分类结构**
将原有分类重新组织为 7 个一级分类,每个分类分配独立路由(`/settings/:category`):
| 分类 | 路由 | 包含内容 | 图标 |
|------|------|----------|------|
| 外观设置 | `/settings/appearance` | 主题/语言/密度/字体/圆角/自定义CSS | `Palette` |
| 安全设置 | `/settings/security` | 密码策略/登录限制/2FA/IP白名单 | `Shield` |
| 通知设置 | `/settings/notifications` | 渠道配置/模板管理/订阅规则 | `Bell` |
| 执行设置 | `/settings/execution` | 超时/LIMIT/并发/执行器配置 | `PlayCircle` |
| 审计设置 | `/settings/audit` | 保留策略/敏感词/审计级别 | `ClipboardList` |
| 集成设置 | `/settings/integrations` | LDAP/Webhook/API密钥 | `Plug` |
| 高级设置 | `/settings/advanced` | 清理/备份/日志/导入导出 | `Cog` |
**导航布局**
设置页面采用左右布局:左侧导航栏宽 `w-64`(固定),右侧内容区 `flex-1`。左侧导航使用 `shadcn/ui` 的 `ScrollArea` 包裹,每个导航项包含图标(`w-4 h-4`)+ 分类名 + 设置项数量 badge(如"12 项")。当前激活项使用 `bg-primary/10 text-primary border-r-2 border-primary`。
导航栏底部放置"危险区域"入口(`AlertTriangle` 图标,红色文字),包含恢复默认设置、清除所有数据等操作。
---
#### 5.3.2 表单交互优化
**统一表单架构**
所有设置表单统一使用 `shadcn/ui Form` + `react-hook-form` + `zod` 验证。每个设置项包裹在卡片组件中:
```
┌─────────────────────────────────────┐
│ 设置项标题 │ ← text-base font-medium
│ 设置项描述,说明此配置的作用和影响范围 │ ← text-sm text-muted-foreground
│ │
│ [输入控件] [重置] │ ← 控件右对齐,重置按钮变灰
└─────────────────────────────────────┘
```
卡片样式:`rounded-xl border border-border/60 bg-card p-5 shadow-sm`,hover 时 `shadow-md transition-shadow duration-200`。卡片间距 `space-y-4`。
**设置项搜索**
页面顶部增加全局搜索栏(`Input` + `Search` 图标 + `Command+K` 快捷键),支持跨分类搜索设置项。搜索范围包括:设置项标题、描述、关键词标签。搜索结果按分类分组展示,点击后滚动定位到对应设置项并高亮(`bg-primary/10` 闪烁 2 秒后消退)。使用 `fuse.js` 实现,`threshold: 0.25`。
**变更指示与保存**
- 变更指示:每个设置卡片在值发生变化时,底部显示 "已修改" 文字(`text-xs text-amber-600`)+ "撤销" 链接,同时卡片顶部边框变为 `border-t-2 border-amber-400`。
- 保存策略:每个分类页右下角固定悬浮保存栏(`sticky bottom-6`),包含"保存更改"(`variant="default"`)和"重置"(`variant="outline"`)按钮。保存栏使用 `bg-card border shadow-lg rounded-xl px-6 py-3`。
- 自动保存:可选开启,每 5 秒自动保存已变更的设置(防抖处理)。
- 保存成功:顶部显示 `Toast`("设置已保存",`CheckCircle` 图标),保存按钮短暂变为成功状态(文字 "已保存" + `Check` 图标,2 秒后恢复)。
**变更历史**
在设置页面右上角增加"变更历史"按钮(`History` 图标),点击后弹出 `Sheet`(右侧滑出,宽 `w-[480px]`)。历史记录展示最近 50 条设置变更,每条包含:
- 时间(相对格式,如"3 分钟前")
- 分类名
- 设置项标题
- 旧值 → 新值(差异高亮)
- 操作人 Avatar + 用户名
历史记录数据由后端提供,前端使用 `useSWR` 轮询获取(间隔 30 秒)。
**危险设置二次确认**
对于高风险设置项(如"允许删除生产环境数据"、"关闭审计日志"),使用以下交互设计:
- 卡片边框:`border-red-200`。
- 卡片顶部:`bg-red-50 px-5 py-2 -mx-5 -mt-5 mb-3 rounded-t-xl border-b border-red-100`,内含 `AlertTriangle` 图标 + "危险设置" 警告文字。
- 开关/输入控件旁增加确认 `Dialog`,用户点击变更时弹出,要求输入操作密码或二次确认文字(如"请输入 DELETE 确认")。
**设置导入/导出**
在高级设置分类中增加导入/导出功能:
- 导出:将所有设置序列化为 JSON 文件下载,包含元数据(导出时间、版本、操作人)。
- 导入:拖拽或选择 JSON 文件,预览变更内容(使用 diff 视图展示与当前设置的差异),确认后应用。导入前自动创建备份点。
---
#### 5.3.3 安全设置增强
**密码策略配置**
新增密码策略卡片,包含以下设置项:
| 设置项 | 控件 | 默认值 | 验证规则 |
|--------|------|--------|----------|
| 最小密码长度 | `Slider` (6-32) + `Input` | 8 | zod `min(6).max(32)` |
| 必须包含大写字母 | `Switch` | true | — |
| 必须包含小写字母 | `Switch` | true | — |
| 必须包含数字 | `Switch` | true | — |
| 必须包含特殊字符 | `Switch` | false | — |
| 密码过期时间(天) | `Input` (数字) | 90 | `min(1).max(365)` |
| 历史密码禁止 reuse 次数 | `Slider` (0-10) | 3 | `min(0).max(10)` |
密码强度实时预览:下方显示密码示例框,输入测试密码后动态显示强度条(红/黄/绿三段)和文字评价("弱"/"中"/"强")。
**登录失败锁定策略**
- 启用开关:`Switch`,默认开启。
- 最大尝试次数:`Slider` (3-10),默认 5 次。
- 锁定时间:`Select`(5分钟/15分钟/30分钟/1小时/手动解锁),默认 15 分钟。
- 通知管理员:`Switch`,锁定事件发生时通知管理员。
**IP 白名单配置**
使用 `Textarea` 输入,每行一个 IP 或 CIDR 段(如 `192.168.1.0/24`)。实时验证输入格式,非法格式行显示红色下划线 + `Tooltip` 说明错误原因。支持从文件导入(`.txt` 或 `.csv` 格式)。
白名单列表以标签云形式展示,每个 IP 标签可单独删除(`X` 图标)。空状态时显示 `Globe` 图标 + "未配置 IP 白名单,允许所有 IP 访问" 提示。
**Session 超时设置**
- 超时时间:`Select`(15分钟/30分钟/1小时/2小时/4小时/8小时/永不),默认 30 分钟。
- 超时前警告:`Switch`,在超时前 2 分钟弹出警告 `Dialog`,提供"保持登录"按钮。
- 多设备登录限制:`Select`(不限制/1台/2台/3台),默认不限制。
**操作审计级别配置**
使用 `RadioGroup` 三选一:
- 基础:仅记录登录/登出和审批操作。
- 标准(默认):额外记录查询执行和设置变更。
- 完整:记录所有操作包括 Schema 浏览和导出行为。
每个级别旁显示预估日志存储量(基于当前使用量计算)。
---
#### 5.3.4 外观设置增强
**主题实时预览**
主题模式切换(深色/浅色/跟随系统)改为实时预览:用户点击选项后立即应用主题(`document.documentElement.classList`),但此时不保存到后端。页面显示"主题已临时切换,保存以永久生效"的提示条(`bg-primary/10 border border-primary/20 rounded-lg px-4 py-2 mb-4`),包含"保存"和"恢复"按钮。用户刷新页面后未保存的主题恢复为已保存值。
**界面密度选择**
新增三档密度选择,使用 `ToggleGroup` 展示:
| 密度 | 效果 | 适用场景 |
|------|------|----------|
| 紧凑 (`compact`) | 表格行高 32px,卡片内边距 16px,字体 13px | 数据密集型用户 |
| 默认 (`default`) | 表格行高 40px,卡片内边距 20px,字体 14px | 平衡 |
| 舒适 (`comfortable`) | 表格行高 48px,卡片内边距 24px,字体 14px | 触控设备或视力辅助 |
选择后立即生效,通过 CSS 变量控制:`--density-factor: 0.8 | 1.0 | 1.2`,各组件使用 `calc()` 基于该变量计算尺寸。
**侧边栏宽度可调**
在"布局"子分类中增加侧边栏宽度设置:
- 控件:`Slider` (200px-400px),默认 280px。
- 实时预览:拖动滑块时侧边栏宽度实时变化。
- 重置按钮:点击恢复默认 280px。
**字体大小设置**
- 基础字体大小:`Select`(12px/13px/14px/15px/16px),默认 14px。
- 代码字体大小:`Select`(12px/13px/14px/15px/16px),默认 14px。
- 代码字体族:`Select`('JetBrains Mono', 'Fira Code', 'Source Code Pro', 'monospace'),默认 'JetBrains Mono'。
字体族选择时,预览文字实时展示该字体的渲染效果。
**圆角风格设置**
使用三个可视化选项卡片:
| 风格 | CSS 变量 | 视觉描述 |
|------|----------|----------|
| 锐利 (`sharp`) | `--radius: 0px` | 直角,无圆角 |
| 默认 (`default`) | `--radius: 0.5rem` | 中等圆角,卡片 8px |
| 圆润 (`rounded`) | `--radius: 0.75rem` | 大圆角,卡片 12px |
每个选项卡片展示缩略图:一个矩形 + 一个按钮 + 一个 badge,使用对应圆角值渲染。选中状态 `ring-2 ring-primary`。
**自定义 CSS(高级用户)**
在"高级"子分类中增加自定义 CSS 输入区:
- 使用 CodeMirror 6 编辑器(只开启基础高亮,无语法检查),高度 `min-h-[200px]`。
- 占位符:"/* 在此输入自定义 CSS,将覆盖默认样式 */\n\n.custom-sidebar {\n /* your styles */\n}"。
- 实时预览开关:`Switch`,开启时 CSS 立即应用到页面。
- 安全提示:输入区上方显示 `Info` 图标 + "自定义 CSS 仅影响当前用户,错误样式可能导致页面显示异常" 警告。
- CSS 作用域:通过 Shadow DOM 或 `style` 标签 + 特定 class 前缀隔离,避免影响全局样式。
**登录页风格选择**
将原有的 5 个缩略图卡片优化为:
- 卡片尺寸:`aspect-[16/10] rounded-lg overflow-hidden border-2`。
- 选中状态:`border-primary ring-2 ring-primary/20`。
- 缩略图内容:使用实际登录页的静态截图或实时 iframe 预览。
- 卡片底部:风格名称 + 简短描述。
- 新增"上传自定义背景"按钮:支持上传图片(JPG/PNG,最大 5MB),上传后显示预览和"删除"按钮。
---
以上优化方案覆盖了审批变更、查询数据、系统设置三大核心模块的全部关键页面。每个方案均提供了具体的组件选型、布局参数、交互细节和视觉规范,可直接转化为前端开发任务。建议按照"查询数据(高频使用)→ 审批变更(核心流程)→ 系统设置(低频但影响全局)"的优先级顺序逐步实施。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,209 @@
## 7. 设计体系统一方案
### 7.1 色彩体系规范
**主色板定义**
| 角色 | 色值 | 用途 |
|------|------|------|
| Primary | `#58a6ff` (--dg-blue) | 主要操作、链接、高亮 |
| Secondary | `#8957e5` (--dg-purple) | 次级强调、标签、分类 |
| Accent | `#39c5cf` (--dg-cyan) | 提示点、新功能标记、数据高亮 |
**功能色定义**
| 角色 | 色值 | 使用场景 |
|------|------|----------|
| Success | `#3fb950` | 成功状态、通过标识、正向数据 |
| Warning | `#f0883e` | 警告提示、需谨慎操作 |
| Error | `#f85149` | 错误状态、删除操作、阻断性提示 |
| Info | `#58a6ff` | 信息提示、帮助引导 |
**中性色(Gray Scale)**
以现有变量为锚点扩展完整灰阶:`#f0f6fc` (50) → `#c9d1d9` (200) → `#8b949e` (400) → `#6e7681` (500) → `#484f58` (700) → `#30363d` (800) → `#21262d` (850) → `#161b22` (900) → `#0d1117` (950)。分别对应主文本、次级文本、弱化文本、悬浮背景、边框、深边框、卡片背景、次级背景、主背景。
**渐变规范**
- 品牌渐变(`.dg-gradient-bg`):`linear-gradient(135deg, #2563eb, #9333ea)`,用于Hero区域、登录页背景、Banner
- 文字渐变(`.dg-gradient-text`):`linear-gradient(90deg, #60a5fa, #c084fc)`,仅用于大标题强调
- 边框渐变(`.dg-gradient-br`):`linear-gradient(135deg, #2563eb, #9333ea)` 作为 border-image,用于特色卡片边框
- 使用规则:同一页面渐变不超过2种,文字渐变仅限H1/H2级别
**Tailwind配置扩展**
```js
colors: {
dg: {
bg: { primary: '#0d1117', secondary: '#161b22', tertiary: '#21262d', hover: '#30363d' },
border: { DEFAULT: '#30363d', light: '#21262d' },
text: { primary: '#f0f6fc', secondary: '#8b949e', muted: '#6e7681' },
blue: '#58a6ff', green: '#3fb950', red: '#f85149',
orange: '#f0883e', purple: '#8957e5', cyan: '#39c5cf', yellow: '#d29922',
}
}
```
### 7.2 字体排版规范
**字体栈**:`font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;` 系统字体优先,中文回退依次为Noto Sans SC、PingFang SC、Microsoft YaHei。
**字号Scale**(基于1.25倍率):
| Token | 尺寸 | 行高 | 用途 |
|-------|------|------|------|
| text-xs | 12px | 18px | 辅助文字、时间戳、徽章内文字 |
| text-sm | 14px | 20px | 表单标签、表格内容、按钮文字 |
| text-base | 16px | 24px | 正文、菜单项、对话框内容 |
| text-lg | 18px | 28px | 卡片标题、区块小标题 |
| text-xl | 20px | 30px | 页面次级标题 |
| text-2xl | 24px | 32px | 页面标题 |
| text-3xl | 30px | 38px | 模块大标题 |
| text-4xl | 36px | 44px | Hero标题、登录页品牌名 |
**字重规范**:normal (400) 用于正文,medium (500) 用于菜单和按钮,semibold (600) 用于标题,bold (700) 用于强调数据。H1-H6统一semibold,避免过粗导致的中文笔画粘连。
**等宽字体**:代码块与数据展示使用 `font-family: "JetBrains Mono", "Fira Code", "SF Mono", monospace;`,字号14px,行高1.6。
### 7.3 间距体系规范
**基于4px的Spacing Scale**:0(0px)、1(4px)、2(8px)、3(12px)、4(16px)、5(20px)、6(24px)、8(32px)、10(40px)、12(48px)、16(64px)、20(80px)、24(96px)。
**页面内边距**:内容区水平 `px-6`(24px),最大宽度1440px居中;垂直方向页面级 `py-8`(32px),模块级 `py-6`(24px)。
**卡片内边距**:标准卡片 `p-6`(24px),紧凑卡片 `p-4`(16px),数据卡片 `px-5 py-4`(20px/16px)。
**表单间距**:标签与输入框间距 `mb-1.5`(6px),表单项之间 `mb-5`(20px),表单组之间 `mb-8`(32px)。
**表格间距**:表头内边距 `px-4 py-3`(16px/12px),行内边距 `px-4 py-3`(16px/12px),行间距0(以边框分隔)。
**常见布局模板**:页面标题区(mb-8)→ 统计卡片区(grid gap-4, mb-8)→ 主内容卡片区(gap-6)。
### 7.4 组件交互规范
**按钮体系**
| 变体 | 背景 | 文字 | 圆角 | 内边距 | 使用场景 |
|------|------|------|------|--------|----------|
| Primary | `#58a6ff` | `#0d1117` | 6px | px-4 py-2 | 主要提交、确认操作 |
| Secondary | `#21262d` | `#f0f6fc` | 6px | px-4 py-2 | 取消、返回操作 |
| Ghost | transparent | `#8b949e` | 6px | px-4 py-2 | 次要操作、工具栏按钮 |
| Danger | `#f85149` | `#fff` | 6px | px-4 py-2 | 删除、阻断性操作 |
| Link | transparent | `#58a6ff` | 0 | px-2 py-1 | 导航跳转、文字链接 |
尺寸:sm (h-8, px-3, text-xs),md (h-10, px-4, text-sm),lg (h-12, px-6, text-base)。圆角统一6px,hover时亮度提升10%,transition 150ms。
**输入框体系**:默认边框`#30363d`,focus边框`#58a6ff`并附加`ring-2 ring-blue-500/20`,error边框`#f85149`带红色ring,disabled背景`#161b22`文字`#6e7681`。统一圆角6px,内边距px-3 py-2,高度40px。
**卡片体系**:默认卡片(bg `#161b22` border `#30363d` 圆角8px),可悬浮卡片(hover时border-color过渡为`#484f58`、translateY(-1px)、shadow-lg),可选中卡片(选中态border `#58a6ff`、bg `#58a6ff/5%`)。
**表格体系**:表头bg `#161b22` 文字`#8b949e` font-semibold,行hover bg `#21262d`,操作列按钮间距gap-2。边框1px `#21262d`。
**弹窗体系**:确认弹窗(宽400px),表单弹窗(宽560px),抽屉(宽480px/640px两档)。统一圆角12px,阴影`shadow-2xl`,入场动画scaleIn 200ms。
**徽章体系**:状态徽章(圆角全角,px-2 py-0.5,text-xs),标签徽章(圆角4px,带边框),计数徽章(圆形,min-w-5,h-5)。
### 7.5 动效体系规范
**缓动函数定义**
| 名称 | 值 | 用途 |
|------|-----|------|
| ease-default | cubic-bezier(0.4, 0, 0.2, 1) | 常规过渡 |
| ease-in | cubic-bezier(0.4, 0, 1, 1) | 元素退出 |
| ease-out | cubic-bezier(0, 0, 0.2, 1) | 元素入场 |
| ease-spring | cubic-bezier(0.34, 1.56, 0.64, 1) | 弹性交互 |
**时长Scale**:fast 150ms(hover、focus环、颜色过渡),normal 250ms(弹窗、展开/收起),slow 350ms(页面转场、大型组件)。
**入场动画**:fadeIn(opacity 0→1, 200ms ease-out)、slideUp(translateY 8px→0 + opacity, 250ms ease-out)、scaleIn(scale 0.95→1 + opacity, 200ms ease-out)。弹窗统一scaleIn,列表项使用slideUp带stagger 30ms。
**交互动画**:hover过渡统一150ms,按钮active时scale 0.98,卡片hover时translateY(-1px) + border-color过渡,输入框focus时ring 150ms。所有交互元素必须有hover反馈。
**Loading动画**:旋转器(border-2 border-t-blue-500,animate-spin,20px),骨架屏(shimmer动画,bg-gradient横向扫光,1.5s linear infinite)。
**CSS Keyframes**
```css
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
@keyframes slideUp { from { opacity: 0; transform: translateY(8px) } to { opacity: 1; transform: translateY(0) } }
@keyframes scaleIn { from { opacity: 0; transform: scale(0.95) } to { opacity: 1; transform: scale(1) } }
```
---
## 8. 实施路线图
### 8.1 优先级划分
| 优先级 | 范围 | 目标 |
|--------|------|------|
| P0 关键 | 登录页改造、设计体系统一 | 解决当前最大用户体验短板,建立可复用的设计基础 |
| P1 重要 | 主题系统扩展、核心功能优化 | 提升产品专业感,优化高频使用路径 |
| P2 优化 | 菜单体系、动效体系 | 增强导航效率与交互质感 |
| P3 增强 | 高级功能 | 数据可视化、个性化等增值能力 |
### 8.2 阶段一:登录页改造(2周)
**Week 1:组件重构**
- Day 1-2:开发粒子动画背景组件(Canvas实现,可配置粒子密度与连线颜色)
- Day 3:品牌展示区(渐变文字、系统名称、副标题、版本号)
- Day 4:登录表单重构(统一输入框规范、密码可见性切换、记住登录态)
- Day 5:安全提示区块(SSL标识、多因子认证引导、安全公告轮播)
**Week 2:动效与联调**
- Day 1-2:入场动效串联(粒子→标题→表单依次fadeIn+slideUp,stagger 150ms)
- Day 3:表单验证增强(实时校验、错误动画抖动、聚焦引导)
- Day 4-5:联调测试、响应式适配、浏览器兼容性验证
交付物:全新登录页、可复用粒子背景组件、登录表单组件。验收标准:Lighthouse性能分≥90,首屏加载<1.5s,动画帧率≥55fps。
### 8.3 阶段二:主题系统扩展(1周)
- Day 1-2:CSS变量体系扩展(将7.1节色彩规范完整植入,新增主题变量层 `--theme-*`)
- Day 3-4:主题切换UI(设置面板主题选择器、实时预览、本地持久化)
- Day 5:测试优化(变量回退机制、主题切换无闪烁、50+ shadcn/ui组件全覆盖)
交付物:扩展后的CSS变量体系、主题切换组件、Light/Dark/High-Contrast三主题。验收标准:主题切换<100ms无闪烁,所有组件颜色100%变量驱动。
### 8.4 阶段三:核心功能优化(3周)
**Week 1:审批变更工单系统**
- 工单列表卡片化重构(统一.dg-card规范)
- 审批流可视化(状态徽章体系、进度条组件)
- 批量操作工具栏(按钮体系统一)
**Week 2:查询数据页面**
- SQL编辑器区域优化(等宽字体规范、行号样式)
- 结果表格重构(表格体系统一、列宽记忆)
- 查询历史面板(间距体系应用、空状态设计)
**Week 3:系统设置页面**
- 设置项卡片分组(间距模板应用)
- 表单交互优化(输入框规范、保存反馈动效)
- 权限矩阵表格重构(表格规范全面落地)
交付物:三个核心页面重构完成、对应组件更新。验收标准:无硬编码颜色,间距100%符合scale,交互动效完整。
### 8.5 阶段四:菜单与体系统一(1周)
- Day 1-2:菜单结构重组(按用户角色分组、图标体系统一、激活态样式)
- Day 3-4:全局设计体系统一(替换所有硬编码颜色为CSS变量,按钮/卡片/输入框标准化)
- Day 5:动效体系实现(全局加载动画、页面转场、交互反馈动效)
交付物:重组后的菜单组件、全局样式清理报告、动效配置文件。验收标准:代码库零硬编码色值(除渐变色定义外),全局交互动效一致性100%。
### 8.6 预期效果
**定性效果**:登录页从功能型升级为品牌展示窗口;核心操作流程视觉层级清晰,用户认知负担降低;全产品交互反馈一致,专业感显著提升。
**定量效果**:
| 指标 | 当前预估 | 目标 |
|------|----------|------|
| 首屏加载时间 | ~2.5s | <1.5s |
| Lighthouse性能分 | ~70 | ≥90 |
| 全局样式不一致点 | 35+处 | 0处 |
| CSS变量覆盖率 | ~60% | 100% |
**风险控制**:每阶段独立分支开发,P0/P1阶段完成后可独立上线;所有样式变更基于现有变量扩展,保持向后兼容;关键页面保留回滚开关,确保24小时内可回退至原版本。
+139
View File
@@ -0,0 +1,139 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { ChevronRight, Home } from 'lucide-react';
import { menuItems, menuSections } from '@/constants/menu';
interface BreadcrumbItem {
label: string;
path?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
}
export default function Breadcrumb({ items }: BreadcrumbProps) {
const navigate = useNavigate();
if (items.length === 0) return null;
return (
<nav aria-label="breadcrumb" className="flex items-center text-xs">
<ol className="flex items-center gap-1">
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<li key={index} className="flex items-center gap-1">
{index > 0 && (
<ChevronRight className="w-3 h-3 text-[var(--dg-text-muted)] flex-shrink-0" />
)}
{isLast ? (
<span className="text-[var(--dg-text-primary)] font-medium">
{item.label}
</span>
) : item.path ? (
<button
type="button"
onClick={() => navigate(item.path!)}
className="text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors inline-flex items-center gap-1 cursor-pointer"
>
{index === 0 && <Home className="w-3 h-3" />}
{item.label}
</button>
) : (
<span className="text-[var(--dg-text-secondary)]">
{item.label}
</span>
)}
</li>
);
})}
</ol>
</nav>
);
}
/**
* Hook that builds breadcrumb items based on the current route.
*
* It matches the current pathname against menuItems, then constructs:
* Home > Section label > Page label
*
* For detail/sub pages (e.g. /instances/:id, /tickets/:id, /tickets/create),
* it prepends the parent breadcrumb entries.
*/
export function useBreadcrumb(): { items: BreadcrumbItem[] } {
const location = useLocation();
const pathname = location.pathname;
const items: BreadcrumbItem[] = [];
// Find a direct menu item match by id (exact route)
const directMatch = menuItems.find(
(item) => item.id === pathname.replace(/^\//, '')
);
if (directMatch) {
// Exact menu match: Home > Section > Page
const section = menuSections.find((s) => s.id === directMatch.section);
items.push({ label: '首页', path: '/dashboard' });
if (section && section.id !== 'overview') {
items.push({ label: section.label });
}
items.push({ label: directMatch.label });
return { items };
}
// Parse the pathname for sub/detail pages
const segments = pathname.split('/').filter(Boolean); // e.g. ['instances', '123'] or ['tickets', 'create']
if (segments.length === 0) {
return { items: [{ label: '首页', path: '/dashboard' }] };
}
const rootSegment = segments[0];
const parentMenuItem = menuItems.find((item) => item.id === rootSegment);
if (!parentMenuItem) {
// Unknown route — just show Home
return { items: [{ label: '首页', path: '/dashboard' }] };
}
const section = menuSections.find((s) => s.id === parentMenuItem.section);
// Home
items.push({ label: '首页', path: '/dashboard' });
// Section (skip for overview section like dashboard)
if (section && section.id !== 'overview') {
items.push({ label: section.label });
}
// Parent page (clickable for detail pages)
if (segments.length > 1) {
items.push({ label: parentMenuItem.label, path: `/${rootSegment}` });
}
// Sub-page label
if (segments.length > 1) {
const subSegment = segments[1];
const isDetailPage = !isNaN(Number(subSegment));
if (isDetailPage) {
// Numeric ID — use a generic "详情" label
items.push({ label: '详情' });
} else {
// Named sub-pages like 'create'
const subPageLabels: Record<string, string> = {
create: '新建',
edit: '编辑',
};
items.push({ label: subPageLabels[subSegment] || subSegment });
}
} else {
// Single segment — this is the current page
items.push({ label: parentMenuItem.label });
}
return { items };
}
+89 -23
View File
@@ -1,18 +1,19 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import {
LayoutDashboard, Database, Server, Zap, Leaf,
ClipboardList, Search, UserCog, Users, Lock, Shield, GitBranch, Bell,
LogOut, Settings, Clock, Megaphone, Sun, Moon, BookOpen
Code2, FileEdit, Users, ShieldCheck, FolderLock, Shield, GitBranch, Bell,
LogOut, Settings, Timer, ScrollText, Megaphone, Sun, Moon, BookOpen,
ChevronRight, Monitor
} from 'lucide-react';
import { menuSections } from '@/constants/menu';
import { menuSections, menuItems } from '@/constants/menu';
import { useAuth } from '@/hooks/useAuth';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
const iconMap: Record<string, React.ElementType> = {
LayoutDashboard, Database, Server, Zap, Leaf,
ClipboardList, Search, UserCog, Users, Lock, Shield, GitBranch, Bell,
Settings, Clock, Megaphone, BookOpen,
Code2, FileEdit, Users, ShieldCheck, FolderLock, Shield, GitBranch, Bell,
Settings, Timer, ScrollText, Megaphone, BookOpen,
};
export default function Layout() {
@@ -29,10 +30,25 @@ export default function Layout() {
.catch(() => {});
}, [location.pathname]);
const getPageTitle = () => {
const item = visibleMenuItems.find(m => m.id === currentPath);
return item?.label || '仪表盘';
// Build breadcrumb info from current path
const breadcrumbInfo = useMemo(() => {
const currentItem = menuItems.find(m => m.id === currentPath);
if (!currentItem) {
return { sectionLabel: '', pageLabel: '仪表盘' };
}
const section = menuSections.find(s => s.id === currentItem.section);
return {
sectionLabel: section?.label || '',
pageLabel: currentItem.label,
};
}, [currentPath]);
const themeOption = useMemo(() => THEME_OPTIONS.find(o => o.id === theme), [theme]);
const themeIcon = useMemo(() => {
if (!themeOption) return <Monitor className="w-4 h-4" />;
if (themeOption.isDark) return <Moon className="w-4 h-4" />;
return <Sun className="w-4 h-4" />;
}, [themeOption]);
return (
<div className="flex h-screen bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] overflow-hidden">
@@ -68,16 +84,33 @@ export default function Layout() {
key={item.id}
onClick={() => navigate('/' + item.id)}
className={
'flex items-center mx-2 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 relative text-[13px] '
'group flex items-center mx-2 px-3 py-2 rounded-lg cursor-pointer '
+ 'transition-all duration-200 relative text-[13px] '
+ (isActive
? 'bg-blue-500/10 text-blue-400 font-medium'
: 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)]')
? 'bg-[var(--dg-blue)]/10 text-[var(--dg-blue)] font-medium'
: 'text-[var(--dg-text-secondary)] '
+ 'hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] '
+ 'hover:ml-0.5')
}
>
{/* Active indicator bar - gradient with glow */}
{isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-blue-400 rounded-r" />
<div
className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 rounded-r animate-[slideInLeft_200ms_ease-out]"
style={{
background: 'linear-gradient(180deg, var(--dg-blue), var(--dg-purple, var(--dg-blue)))',
boxShadow: '0 0 8px 1px var(--dg-blue), 0 0 16px 2px color-mix(in srgb, var(--dg-blue) 40%, transparent)',
}}
/>
)}
<Icon className="w-4 h-4 flex-shrink-0" />
<Icon
className={
'w-4 h-4 flex-shrink-0 transition-colors duration-200 '
+ (isActive
? 'text-[var(--dg-blue)]'
: 'group-hover:text-[var(--dg-blue)]')
}
/>
<span className="ml-2.5">{item.label}</span>
</div>
);
@@ -92,27 +125,44 @@ export default function Layout() {
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
{/* 顶部栏 */}
<header className="h-14 bg-[var(--dg-bg-secondary)] border-b border-[var(--dg-border)] flex items-center justify-between px-5 flex-shrink-0">
<div className="flex items-center gap-2 text-sm">
<span className="text-[var(--dg-text-muted)]">{getPageTitle()}</span>
{/* Breadcrumb area */}
<div className="flex items-center gap-1.5 text-sm">
<span className="text-[var(--dg-text-muted)] font-medium">SQL-Platform</span>
{breadcrumbInfo.sectionLabel && (
<>
<ChevronRight className="w-3.5 h-3.5 text-[var(--dg-text-muted)] opacity-50" />
<span className="text-[var(--dg-text-muted)] opacity-70">{breadcrumbInfo.sectionLabel}</span>
</>
)}
<ChevronRight className="w-3.5 h-3.5 text-[var(--dg-text-muted)] opacity-50" />
<span className="text-[var(--dg-text-primary)] font-medium">{breadcrumbInfo.pageLabel}</span>
</div>
{/* Right actions */}
<div className="flex items-center gap-2">
{/* Theme toggle with tooltip */}
<button
onClick={toggleTheme}
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] transition-colors"
title={theme === 'dark' ? '浅色模式' : '深色模式'}
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] transition-all duration-200"
title={`当前主题: ${themeOption?.name || theme}\n点击切换下一个主题`}
>
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
{themeIcon}
</button>
{/* Notification bell */}
<button onClick={() => navigate('/tickets')}
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] transition-colors relative">
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] transition-all duration-200 relative">
<Bell className="w-4 h-4" />
{pendingCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 bg-red-500 rounded-full text-[10px] text-white font-bold flex items-center justify-center">
<span className="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 bg-red-500 rounded-full text-[10px] text-white font-bold flex items-center justify-center animate-pulse">
{pendingCount > 99 ? '99+' : pendingCount}
</span>
)}
</button>
<div className="w-px h-5 bg-[var(--dg-border)] mx-1" />
{/* User area */}
<div className="flex items-center gap-2 pl-1">
<div className="w-7 h-7 rounded-full dg-gradient-br flex items-center justify-center text-white text-xs font-semibold flex-shrink-0">
{user.avatar}
@@ -120,7 +170,7 @@ export default function Layout() {
<span className="text-sm text-[var(--dg-text-secondary)] max-w-[80px] truncate">{user.username}</span>
<button
onClick={() => { logout(); navigate('/login'); }}
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-red-500/10 hover:text-red-400 transition-colors"
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-red-500/10 hover:text-red-400 transition-colors duration-200"
title="退出登录"
>
<LogOut className="w-4 h-4" />
@@ -134,6 +184,22 @@ export default function Layout() {
<Outlet />
</div>
</main>
{/* Keyframe animation for active indicator */}
<style>{`
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateY(-50%) scaleX(0);
transform-origin: left;
}
to {
opacity: 1;
transform: translateY(-50%) scaleX(1);
transform-origin: left;
}
}
`}</style>
</div>
);
}
@@ -1,10 +1,11 @@
import { useLogin } from '@/hooks/useLogin';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
import { Sun, Moon } from 'lucide-react';
export default function CenteredCard() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
const { toggleTheme, isDark } = useTheme();
const { theme, toggleTheme } = useTheme();
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, position: 'relative' }}>
+3 -2
View File
@@ -1,10 +1,11 @@
import { useLogin } from '@/hooks/useLogin';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
import { Sun, Moon } from 'lucide-react';
export default function FullImage() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
const { toggleTheme, isDark } = useTheme();
const { theme, toggleTheme } = useTheme();
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, position: 'relative', overflow: 'hidden' }}>
@@ -1,10 +1,11 @@
import { useLogin } from '@/hooks/useLogin';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
import { Sun, Moon, Database } from 'lucide-react';
export default function MinimalDark() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
const { toggleTheme, isDark } = useTheme();
const { theme, toggleTheme } = useTheme();
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
+3 -2
View File
@@ -1,10 +1,11 @@
import { useLogin } from '@/hooks/useLogin';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
import { Sun, Moon, Database } from 'lucide-react';
export default function SidePanel() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
const { toggleTheme, isDark } = useTheme();
const { theme, toggleTheme } = useTheme();
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
return (
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
+3 -2
View File
@@ -1,5 +1,5 @@
import { useLogin } from '@/hooks/useLogin';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
import { Sun, Moon, Database, Code, GitBranch, Bell, History, UserCog } from 'lucide-react';
const features = [
@@ -13,7 +13,8 @@ const features = [
export default function SplitView() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
const { theme, toggleTheme, isDark } = useTheme();
const { theme, toggleTheme } = useTheme();
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
return (
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
+15 -16
View File
@@ -1,22 +1,21 @@
export const menuSections = [
{ id: 'overview', label: '概览' },
{ id: 'datasource', label: '数据源' },
{ id: 'ticket', label: '工单管理' },
{ id: 'permission', label: '权限管理' },
{ id: 'system', label: '系统配置' },
{ id: 'config', label: '配置平台' },
{ id: 'data', label: '数据源' },
{ id: 'ticket', label: '工单' },
{ id: 'permission', label: '权限' },
{ id: 'system', label: '系统' },
];
export const menuItems = [
{ id: 'dashboard', label: '仪表盘', icon: 'LayoutDashboard', section: 'overview', roles: ['admin', 'dba', 'developer', 'auditor'] },
{ id: 'instances', label: '数据源', icon: 'Database', section: 'datasource', roles: ['admin', 'dba'] },
{ id: 'instance-groups', label: '权限组', icon: 'Lock', section: 'permission', roles: ['admin'] },
{ id: 'tickets', label: '数据变更', icon: 'ClipboardList', section: 'ticket', roles: ['admin', 'dba', 'developer', 'auditor'] },
{ id: 'queries', label: '查询数据', icon: 'Search', section: 'ticket', roles: ['admin', 'dba', 'developer'] },
{ id: 'roles', label: '角色权限', icon: 'UserCog', section: 'permission', roles: ['admin'] },
{ id: 'users', label: '用户管理', icon: 'Users', section: 'permission', roles: ['admin'] },
{ id: 'audit-logs', label: '审计日志', icon: 'FileText', section: 'system', roles: ['admin', 'dba'] },
{ id: 'workflow', label: '审批流程', icon: 'GitBranch', section: 'system', roles: ['admin', 'dba'] },
{ id: 'auto-task', label: '自动任务', icon: 'Clock', section: 'system', roles: ['admin'] },
{ id: 'settings', label: '系统设置', icon: 'Settings', section: 'config', roles: ['admin'] },
{ id: 'dashboard', label: '仪表盘', icon: 'LayoutDashboard', section: 'overview', roles: ['admin', 'dba', 'developer', 'auditor'], permission: 'dashboard.view' },
{ id: 'instances', label: '数据源', icon: 'Database', section: 'data', roles: ['admin', 'dba'], permission: 'instances.manage' },
{ id: 'queries', label: '查询数据', icon: 'Code2', section: 'data', roles: ['admin', 'dba', 'developer'], permission: 'queries.execute' },
{ id: 'tickets', label: '数据变更', icon: 'FileEdit', section: 'ticket', roles: ['admin', 'dba', 'developer', 'auditor'], permission: 'tickets.manage' },
{ id: 'users', label: '用户管理', icon: 'Users', section: 'permission', roles: ['admin'], permission: 'users.manage' },
{ id: 'roles', label: '角色权限', icon: 'ShieldCheck', section: 'permission', roles: ['admin'], permission: 'roles.manage' },
{ id: 'instance-groups', label: '权限组', icon: 'FolderLock', section: 'permission', roles: ['admin'], permission: 'instance-groups.manage' },
{ id: 'workflow', label: '审批流程', icon: 'GitBranch', section: 'system', roles: ['admin', 'dba'], permission: 'workflow.manage' },
{ id: 'audit-logs', label: '审计日志', icon: 'ScrollText', section: 'system', roles: ['admin', 'dba'], permission: 'audit-logs.view' },
{ id: 'auto-task', label: '自动任务', icon: 'Timer', section: 'system', roles: ['admin'], permission: 'auto-task.manage' },
{ id: 'settings', label: '系统设置', icon: 'Settings', section: 'system', roles: ['admin'], permission: 'settings.manage' },
];
+9 -2
View File
@@ -1,7 +1,7 @@
import { useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS, type Theme } from '@/hooks/useTheme';
import api from '@/api/request';
export interface LoginState {
@@ -33,7 +33,14 @@ export function useLogin(): LoginState {
// Apply user preferences from backend (cross-browser sync)
try {
const prefs = await api.get<{ theme?: string; login_style?: string }>('/user/preferences');
if (prefs?.theme) setTheme(prefs.theme as 'dark' | 'light');
if (prefs?.theme) {
const t = prefs.theme;
if (t === 'dark' || t === 'light') {
setTheme(t === 'dark' ? 'ocean-deep' : 'light-professional');
} else if (THEME_OPTIONS.some(o => o.id === t)) {
setTheme(t as Theme);
}
}
if (prefs?.login_style) localStorage.setItem('login-style', prefs.login_style);
} catch { /* ignore */ }
navigate('/dashboard');
+41 -10
View File
@@ -1,22 +1,54 @@
import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
export type Theme = 'dark' | 'light';
export type Theme = 'ocean-deep' | 'light-professional' | 'midnight-purple' | 'pine-green' | 'orange-vitality';
export interface ThemeOption {
id: Theme;
name: string;
description: string;
isDark: boolean;
primaryColor: string;
}
export const THEME_OPTIONS: ThemeOption[] = [
{ id: 'ocean-deep', name: '深海蓝', description: '专业稳重的深蓝色调', isDark: true, primaryColor: '#3b82f6' },
{ id: 'light-professional', name: '明亮白', description: '清爽干净的浅色主题', isDark: false, primaryColor: '#2563eb' },
{ id: 'midnight-purple', name: '午夜紫', description: '神秘优雅的紫色调', isDark: true, primaryColor: '#a855f7' },
{ id: 'pine-green', name: '松林绿', description: '自然清新的绿色主题', isDark: true, primaryColor: '#22c55e' },
{ id: 'orange-vitality', name: '活力橙', description: '温暖活力的橙色主题', isDark: true, primaryColor: '#f97316' },
];
const THEME_ORDER: Theme[] = THEME_OPTIONS.map(o => o.id);
const DEFAULT_THEME: Theme = 'ocean-deep';
// Backward compatibility: map old theme names to new ones
function migrateLegacyTheme(value: string | null): Theme | null {
if (value === 'dark') return 'ocean-deep';
if (value === 'light') return 'light-professional';
return null;
}
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
isDark: boolean;
isLight: boolean;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
function getInitialTheme(): Theme {
if (typeof window === 'undefined') return 'dark';
const stored = localStorage.getItem('dg-theme') as Theme | null;
if (stored === 'dark' || stored === 'light') return stored;
return 'dark';
if (typeof window === 'undefined') return DEFAULT_THEME;
const stored = localStorage.getItem('dg-theme');
const migrated = migrateLegacyTheme(stored);
if (migrated) {
// Persist the migrated value so we don't migrate every time
localStorage.setItem('dg-theme', migrated);
return migrated;
}
if (stored && THEME_ORDER.includes(stored as Theme)) {
return stored as Theme;
}
return DEFAULT_THEME;
}
function applyTheme(theme: Theme) {
@@ -32,7 +64,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
const toggleTheme = useCallback(() => {
setThemeState(prev => {
const next = prev === 'dark' ? 'light' : 'dark';
const idx = THEME_ORDER.indexOf(prev);
const next = THEME_ORDER[(idx + 1) % THEME_ORDER.length];
localStorage.setItem('dg-theme', next);
return next;
});
@@ -47,8 +80,6 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
theme,
toggleTheme,
setTheme,
isDark: theme === 'dark',
isLight: theme === 'light',
}), [theme, toggleTheme, setTheme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
+325 -6
View File
@@ -3,8 +3,8 @@
@tailwind utilities;
@layer base {
/* ===== Theme 1: Ocean Deep (深蓝专业) — Default ===== */
:root {
/* ===== Dark Theme (default) ===== */
--background: 220 13% 5%;
--foreground: 210 40% 98%;
--card: 220 13% 7%;
@@ -33,7 +33,6 @@
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 220 13% 18%;
--sidebar-ring: 217 91% 60%;
--dg-bg-primary: #0d1117;
--dg-bg-secondary: #161b22;
--dg-bg-tertiary: #21262d;
@@ -50,7 +49,6 @@
--dg-purple: #8957e5;
--dg-cyan: #39c5cf;
--dg-yellow: #d29922;
--dg-card-hover-border: rgba(88, 166, 255, 0.5);
--dg-gradient-start: #2563eb;
--dg-gradient-end: #9333ea;
@@ -59,8 +57,62 @@
--dg-input-focus-ring: rgba(96, 165, 250, 0.2);
}
[data-theme="ocean-deep"] {
--background: 220 13% 5%;
--foreground: 210 40% 98%;
--card: 220 13% 7%;
--card-foreground: 210 40% 98%;
--popover: 220 13% 7%;
--popover-foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 220 13% 14%;
--secondary-foreground: 210 40% 98%;
--muted: 220 13% 14%;
--muted-foreground: 215 20% 55%;
--accent: 220 13% 14%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 100%;
--border: 220 13% 18%;
--input: 220 13% 14%;
--ring: 217 91% 60%;
--radius: 0.625rem;
--sidebar-background: 220 13% 7%;
--sidebar-foreground: 210 40% 90%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 13% 14%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 220 13% 18%;
--sidebar-ring: 217 91% 60%;
--dg-bg-primary: #0d1117;
--dg-bg-secondary: #161b22;
--dg-bg-tertiary: #21262d;
--dg-bg-hover: #30363d;
--dg-border: #30363d;
--dg-border-light: #21262d;
--dg-text-primary: #f0f6fc;
--dg-text-secondary: #8b949e;
--dg-text-muted: #6e7681;
--dg-blue: #58a6ff;
--dg-green: #3fb950;
--dg-red: #f85149;
--dg-orange: #f0883e;
--dg-purple: #8957e5;
--dg-cyan: #39c5cf;
--dg-yellow: #d29922;
--dg-card-hover-border: rgba(88, 166, 255, 0.5);
--dg-gradient-start: #2563eb;
--dg-gradient-end: #9333ea;
--dg-gradient-text-start: #60a5fa;
--dg-gradient-text-end: #c084fc;
--dg-input-focus-ring: rgba(96, 165, 250, 0.2);
}
/* ===== Theme 2: Light Professional (浅色商务) ===== */
[data-theme="light-professional"],
[data-theme="light"] {
/* ===== Light Theme ===== */
--background: 0 0% 100%;
--foreground: 220 14% 10%;
--card: 0 0% 100%;
@@ -89,7 +141,6 @@
--sidebar-accent-foreground: 220 14% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217 91% 60%;
--dg-bg-primary: #ffffff;
--dg-bg-secondary: #f8f9fb;
--dg-bg-tertiary: #f0f1f5;
@@ -106,7 +157,6 @@
--dg-purple: #7c3aed;
--dg-cyan: #0891b2;
--dg-yellow: #ca8a04;
--dg-card-hover-border: rgba(37, 99, 235, 0.3);
--dg-gradient-start: #2563eb;
--dg-gradient-end: #7c3aed;
@@ -114,6 +164,168 @@
--dg-gradient-text-end: #6d28d9;
--dg-input-focus-ring: rgba(37, 99, 235, 0.15);
}
/* ===== Theme 3: Midnight Purple (午夜紫韵) ===== */
[data-theme="midnight-purple"] {
--background: 264 56% 4%;
--foreground: 280 100% 98%;
--card: 264 48% 8%;
--card-foreground: 280 100% 98%;
--popover: 264 48% 8%;
--popover-foreground: 280 100% 98%;
--primary: 270 91% 65%;
--primary-foreground: 0 0% 100%;
--secondary: 264 40% 20%;
--secondary-foreground: 280 100% 98%;
--muted: 264 40% 20%;
--muted-foreground: 280 30% 60%;
--accent: 264 40% 20%;
--accent-foreground: 280 100% 98%;
--destructive: 348 100% 72%;
--destructive-foreground: 264 56% 4%;
--border: 264 40% 20%;
--input: 264 40% 20%;
--ring: 270 91% 65%;
--radius: 0.625rem;
--sidebar-background: 264 48% 6%;
--sidebar-foreground: 280 100% 92%;
--sidebar-primary: 270 91% 65%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 264 40% 20%;
--sidebar-accent-foreground: 280 100% 98%;
--sidebar-border: 264 40% 20%;
--sidebar-ring: 270 91% 65%;
--dg-bg-primary: #0a0612;
--dg-bg-secondary: #130c21;
--dg-bg-tertiary: #1c1330;
--dg-bg-hover: #2d1f4a;
--dg-border: #2d1f4a;
--dg-border-light: #1c1330;
--dg-text-primary: #faf5ff;
--dg-text-secondary: #b794c7;
--dg-text-muted: #7c5a8a;
--dg-blue: #818cf8;
--dg-green: #34d399;
--dg-red: #fb7185;
--dg-orange: #fb923c;
--dg-purple: #c084fc;
--dg-cyan: #22d3ee;
--dg-yellow: #facc15;
--dg-card-hover-border: rgba(192, 132, 252, 0.5);
--dg-gradient-start: #9333ea;
--dg-gradient-end: #c084fc;
--dg-gradient-text-start: #a855f7;
--dg-gradient-text-end: #c084fc;
--dg-input-focus-ring: rgba(192, 132, 252, 0.2);
}
/* ===== Theme 4: Pine Green (松绿自然) ===== */
[data-theme="pine-green"] {
--background: 150 25% 5%;
--foreground: 140 60% 97%;
--card: 150 25% 9%;
--card-foreground: 140 60% 97%;
--popover: 150 25% 9%;
--popover-foreground: 140 60% 97%;
--primary: 142 71% 45%;
--primary-foreground: 150 25% 5%;
--secondary: 150 20% 18%;
--secondary-foreground: 140 60% 97%;
--muted: 150 20% 18%;
--muted-foreground: 150 20% 55%;
--accent: 150 20% 18%;
--accent-foreground: 140 60% 97%;
--destructive: 0 90% 72%;
--destructive-foreground: 150 25% 5%;
--border: 150 20% 18%;
--input: 150 20% 18%;
--ring: 142 71% 45%;
--radius: 0.625rem;
--sidebar-background: 150 25% 7%;
--sidebar-foreground: 140 40% 88%;
--sidebar-primary: 142 71% 45%;
--sidebar-primary-foreground: 150 25% 5%;
--sidebar-accent: 150 20% 18%;
--sidebar-accent-foreground: 140 60% 97%;
--sidebar-border: 150 20% 18%;
--sidebar-ring: 142 71% 45%;
--dg-bg-primary: #0a1210;
--dg-bg-secondary: #111f1a;
--dg-bg-tertiary: #192c24;
--dg-bg-hover: #253d32;
--dg-border: #253d32;
--dg-border-light: #192c24;
--dg-text-primary: #f0fdf4;
--dg-text-secondary: #86b8a0;
--dg-text-muted: #5a7c6b;
--dg-blue: #38bdf8;
--dg-green: #4ade80;
--dg-red: #f87171;
--dg-orange: #fb923c;
--dg-purple: #c084fc;
--dg-cyan: #2dd4bf;
--dg-yellow: #facc15;
--dg-card-hover-border: rgba(74, 222, 128, 0.5);
--dg-gradient-start: #059669;
--dg-gradient-end: #10b981;
--dg-gradient-text-start: #059669;
--dg-gradient-text-end: #34d399;
--dg-input-focus-ring: rgba(74, 222, 128, 0.2);
}
/* ===== Theme 5: Orange Vitality (橙光活力) ===== */
[data-theme="orange-vitality"] {
--background: 30 35% 5%;
--foreground: 30 100% 97%;
--card: 30 30% 9%;
--card-foreground: 30 100% 97%;
--popover: 30 30% 9%;
--popover-foreground: 30 100% 97%;
--primary: 25 95% 53%;
--primary-foreground: 0 0% 100%;
--secondary: 30 25% 18%;
--secondary-foreground: 30 100% 97%;
--muted: 30 25% 18%;
--muted-foreground: 30 25% 55%;
--accent: 30 25% 18%;
--accent-foreground: 30 100% 97%;
--destructive: 0 80% 70%;
--destructive-foreground: 30 35% 5%;
--border: 30 25% 18%;
--input: 30 25% 18%;
--ring: 25 95% 53%;
--radius: 0.625rem;
--sidebar-background: 30 30% 7%;
--sidebar-foreground: 30 80% 88%;
--sidebar-primary: 25 95% 53%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 30 25% 18%;
--sidebar-accent-foreground: 30 100% 97%;
--sidebar-border: 30 25% 18%;
--sidebar-ring: 25 95% 53%;
--dg-bg-primary: #110d08;
--dg-bg-secondary: #1d1610;
--dg-bg-tertiary: #2b1f16;
--dg-bg-hover: #3d2d20;
--dg-border: #3d2d20;
--dg-border-light: #2b1f16;
--dg-text-primary: #fef7ed;
--dg-text-secondary: #d4a574;
--dg-text-muted: #9c7a54;
--dg-blue: #60a5fa;
--dg-green: #4ade80;
--dg-red: #f87171;
--dg-orange: #fb923c;
--dg-purple: #a78bfa;
--dg-cyan: #2dd4bf;
--dg-yellow: #fcd34d;
--dg-card-hover-border: rgba(251, 146, 60, 0.5);
--dg-gradient-start: #ea580c;
--dg-gradient-end: #f59e0b;
--dg-gradient-text-start: #f97316;
--dg-gradient-text-end: #fbbf24;
--dg-input-focus-ring: rgba(251, 146, 60, 0.2);
}
}
@layer base {
@@ -179,12 +391,109 @@
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: var(--dg-text-muted);
}
[data-theme="light-professional"] .scrollbar-thin::-webkit-scrollbar-thumb,
[data-theme="light"] .scrollbar-thin::-webkit-scrollbar-thumb {
background: #d1d5db;
}
[data-theme="light-professional"] .scrollbar-thin::-webkit-scrollbar-thumb:hover,
[data-theme="light"] .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* ===== Design System Utilities ===== */
/* Buttons */
.dg-btn {
@apply inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200;
}
.dg-btn-primary {
@apply dg-btn dg-gradient-bg text-white hover:opacity-90 active:scale-[0.98];
}
.dg-btn-secondary {
@apply dg-btn bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] active:scale-[0.98];
}
.dg-btn-ghost {
@apply dg-btn text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)];
}
.dg-btn-danger {
@apply dg-btn bg-red-500/10 text-red-400 hover:bg-red-500/20 active:scale-[0.98];
}
.dg-btn-sm {
@apply px-3 py-1.5 text-xs;
}
.dg-btn-lg {
@apply px-6 py-3 text-base;
}
/* Cards */
.dg-card-section {
@apply dg-card p-5;
}
.dg-card-header {
@apply text-sm font-semibold text-[var(--dg-text-primary)] mb-3;
}
/* Badges */
.dg-badge {
@apply inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium;
}
.dg-badge-blue {
@apply dg-badge bg-blue-500/10 text-blue-400;
}
.dg-badge-green {
@apply dg-badge bg-green-500/10 text-green-400;
}
.dg-badge-red {
@apply dg-badge bg-red-500/10 text-red-400;
}
.dg-badge-yellow {
@apply dg-badge bg-yellow-500/10 text-yellow-400;
}
.dg-badge-purple {
@apply dg-badge bg-purple-500/10 text-purple-400;
}
.dg-badge-orange {
@apply dg-badge bg-orange-500/10 text-orange-400;
}
.dg-badge-cyan {
@apply dg-badge bg-cyan-500/10 text-cyan-400;
}
.dg-badge-muted {
@apply dg-badge bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)];
}
/* Spacing helpers */
.dg-gap-section {
@apply gap-6;
}
.dg-gap-card {
@apply gap-4;
}
/* Table rows */
.dg-table-row {
@apply flex items-center px-4 py-3 border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)] transition-colors duration-150;
}
.dg-table-header {
@apply flex items-center px-4 py-2.5 text-xs font-semibold text-[var(--dg-text-muted)] uppercase tracking-wider border-b border-[var(--dg-border)];
}
/* Status dot */
.dg-status-dot {
@apply w-2 h-2 rounded-full;
}
.dg-status-dot-green {
@apply dg-status-dot bg-green-400;
}
.dg-status-dot-red {
@apply dg-status-dot bg-red-400;
}
.dg-status-dot-yellow {
@apply dg-status-dot bg-yellow-400;
}
.dg-status-dot-blue {
@apply dg-status-dot bg-blue-400;
}
}
@keyframes fadeInUp {
@@ -197,3 +506,13 @@
transform: translateY(0);
}
}
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
+37 -17
View File
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS, type Theme } from '@/hooks/useTheme';
import { LOGIN_STYLE_OPTIONS, type LoginStyle } from '@/pages/LoginPage';
import { Sun, Moon, Check, Save, Loader2 } from 'lucide-react';
import { Check, Save, Loader2 } from 'lucide-react';
import api from '@/api/request';
import { toast } from '@/hooks/useToast';
@@ -16,7 +16,15 @@ export default function AppearanceSettings() {
useEffect(() => {
api.get<{ theme?: string; login_style?: string }>('/user/preferences')
.then(prefs => {
if (prefs?.theme) setTheme(prefs.theme as 'dark' | 'light');
if (prefs?.theme) {
// Backward compat: map old values
const t = prefs.theme;
if (t === 'dark' || t === 'light') {
setTheme(t === 'dark' ? 'ocean-deep' : 'light-professional');
} else if (THEME_OPTIONS.some(o => o.id === t)) {
setTheme(t as Theme);
}
}
if (prefs?.login_style) setLoginStyle(prefs.login_style as LoginStyle);
})
.catch(() => {
@@ -27,7 +35,7 @@ export default function AppearanceSettings() {
.finally(() => setLoading(false));
}, []);
const handleThemeChange = (t: 'dark' | 'light') => {
const handleThemeChange = (t: Theme) => {
setTheme(t);
setDirty(true);
};
@@ -66,24 +74,36 @@ export default function AppearanceSettings() {
<div className="space-y-8">
{/* Theme Mode */}
<div>
<h3 className="text-sm font-semibold mb-3">主题模式</h3>
<div className="flex gap-3">
{([
{ id: 'dark' as const, icon: Moon, label: '深色模式' },
{ id: 'light' as const, icon: Sun, label: '浅色模式' },
]).map(opt => (
<h3 className="text-sm font-semibold mb-1">主题模式</h3>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">选择界面配色方案,即时预览效果</p>
<div className="grid grid-cols-5 gap-3">
{THEME_OPTIONS.map(opt => (
<button
key={opt.id}
onClick={() => handleThemeChange(opt.id)}
className={`flex items-center gap-2 px-5 py-3 rounded-xl text-sm font-medium transition-all ${
theme === opt.id
? 'bg-blue-500/15 text-blue-400 ring-1 ring-blue-500/30'
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
className={`group relative rounded-xl p-4 text-left transition-all bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)] ${
theme === opt.id ? 'ring-2 ring-blue-500/50' : ''
}`}
>
<opt.icon className="w-4 h-4" />
{opt.label}
{theme === opt.id && <Check className="w-3.5 h-3.5 ml-1" />}
{/* Color preview swatch */}
<div
className="w-full h-6 rounded-md mb-3"
style={{
background: opt.isDark
? `linear-gradient(135deg, ${opt.primaryColor} 0%, color-mix(in srgb, ${opt.primaryColor} 40%, #1a1a2e) 50%, #0d1117 100%)`
: `linear-gradient(135deg, ${opt.primaryColor} 0%, color-mix(in srgb, ${opt.primaryColor} 60%, #ffffff) 50%, #f8fafc 100%)`,
}}
/>
{/* Active check mark */}
{theme === opt.id && (
<div className="absolute top-2 right-2 w-5 h-5 rounded-full bg-blue-500 flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
</div>
)}
<h4 className="text-sm font-medium mb-0.5">{opt.name}</h4>
<p className="text-xs text-[var(--dg-text-muted)] leading-snug">{opt.description}</p>
</button>
))}
</div>
+37 -47
View File
@@ -7,7 +7,7 @@ import {
KeyRound, FolderOpen, Server, Zap, ClipboardPaste,
History, PanelRightClose, PanelRightOpen,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
// Card components replaced by design system utility classes
import { useActionPermission } from '@/hooks/useAuth';
import SqlEditor from '@/components/SqlEditor';
import SchemaSidebar from '@/components/SchemaSidebar';
@@ -321,28 +321,24 @@ export default function QueriesPage() {
<div className="flex items-center gap-2">
{instanceId && (
<button onClick={() => setShowSidebar(!showSidebar)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-lg border transition-all ${
showSidebar ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : 'text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${showSidebar ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}>
<Database className="w-3.5 h-3.5" />
{showSidebar ? '隐藏' : '导航'}
</button>
)}
<button onClick={() => setShowTemplates(!showTemplates)}
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)] border border-[var(--dg-border)] rounded-lg transition-all">
className="dg-btn dg-btn-ghost dg-btn-sm">
<Sparkles className="w-3.5 h-3.5 text-blue-400" />
模板 ({templates.length})
<ChevronDown className={`w-3 h-3 transition-transform ${showTemplates ? 'rotate-180' : ''}`} />
</button>
<button onClick={() => setShowHistory(!showHistory)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-lg border transition-all ${
showHistory ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : 'text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${showHistory ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}>
{showHistory ? <PanelRightClose className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
历史 {history.length > 0 && <span className="text-[10px] px-1 py-0.5 rounded-full bg-[var(--dg-bg-primary)]">{history.length}</span>}
</button>
<button onClick={handleExecute} disabled={executing || !instanceId}
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 disabled:opacity-40 transition-all">
className="dg-btn dg-btn-primary">
{executing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
{executing ? '执行中...' : '执行'}
</button>
@@ -351,37 +347,35 @@ export default function QueriesPage() {
{/* -- 模板选择器 -- */}
{showTemplates && (
<div className="p-3 bg-[var(--dg-bg-secondary)] rounded-lg border border-[var(--dg-border)]">
<div className="dg-card p-3">
<div className="flex items-center gap-2 mb-3 flex-wrap">
{templateCategories.map(cat => (
<button key={cat} onClick={() => setTemplateCategory(cat)}
className={`px-2.5 py-1 text-[11px] rounded-md transition-all font-medium ${
templateCategory === cat ? 'bg-blue-500/15 text-blue-400 border border-blue-500/30' : 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] border border-transparent'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${templateCategory === cat ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}>
{catLabel[cat] || cat}
</button>
))}
<div className="flex-1" />
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--dg-text-muted)]" />
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 dg-text-muted" />
<input value={templateSearch} onChange={e => setTemplateSearch(e.target.value)}
className="pl-7 pr-3 py-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-md text-xs focus:border-blue-400 focus:outline-none w-40"
placeholder="搜索模板..." />
</div>
</div>
{filteredTemplates.length === 0 ? (
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">无匹配模板</div>
<div className="text-center py-4 text-xs dg-text-muted">无匹配模板</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 max-h-48 overflow-y-auto">
{filteredTemplates.map(tpl => (
<button key={tpl.id} onClick={() => handleApplyTemplate(tpl)}
className="text-left p-2.5 rounded-lg bg-[var(--dg-bg-primary)] hover:bg-[var(--dg-bg-hover)] transition-colors group border border-[var(--dg-border)] hover:border-[var(--dg-text-muted)]"
className="dg-card text-left p-2.5 hover:bg-[var(--dg-bg-hover)] group"
title={tpl.description}>
<div className="flex items-center gap-2 mb-0.5">
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400 font-medium">{tpl.category}</span>
<span className="text-sm font-medium group-hover:text-blue-400 transition-colors truncate">{tpl.name}</span>
</div>
<p className="text-xs text-[var(--dg-text-muted)] line-clamp-1">{tpl.description}</p>
<p className="text-xs dg-text-muted line-clamp-1">{tpl.description}</p>
</button>
))}
</div>
@@ -405,7 +399,7 @@ export default function QueriesPage() {
<SqlEditor value={sql} onChange={setSql} placeholder={placeholderMap[dbType]}
language={editorLang} height="240px" onExecute={handleExecute}
schema={(dbType === 'mysql' || dbType === 'postgres' || dbType === 'postgresql') ? editorSchema : undefined} />
<div className="flex items-center justify-between px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-[11px] text-[var(--dg-text-muted)]">
<div className="flex items-center justify-between px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-[11px] dg-text-muted">
<span className="flex items-center gap-1.5">
{dbTypeIcon[dbType] && <img src={dbTypeIcon[dbType]} alt="" className="w-3 h-3 object-contain" />}
{dbTypeLabel[dbType]} 模式 · {sql.length} 字符
@@ -428,32 +422,32 @@ export default function QueriesPage() {
{/* -- 查询结果 -- */}
{result && (
<Card className="dg-card overflow-hidden">
<CardHeader className="border-b border-[var(--dg-border)] py-2.5">
<div className="dg-card overflow-hidden">
<div className="border-b border-[var(--dg-border)] py-2.5 px-4">
<div className="flex items-center justify-between flex-wrap gap-2">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<span className="text-sm font-semibold flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-400" />
查询结果
</CardTitle>
<div className="flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
</span>
<div className="flex items-center gap-3 text-xs dg-text-secondary">
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{result.execution_time_ms}ms</span>
<span>{result.rows?.length || 0} 行</span>
<span>{result.columns?.length || 0} 列</span>
{result.rows && result.rows.length > 0 && (
<button onClick={handleExportResult}
className="flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-green-500/15 text-green-400 border border-green-500/30 rounded-md hover:bg-green-500/25 transition-all">
className="dg-btn dg-btn-ghost dg-btn-sm">
<Download className="w-3 h-3" />导出 CSV
</button>
)}
</div>
</div>
</CardHeader>
<CardContent className="p-0">
</div>
<div className="p-0">
{result.columns && result.columns.length > 0 ? (
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto max-h-[350px]">
<table className="w-full text-sm">
<thead className="sticky top-0 z-10">
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
<tr className="dg-table-header">
<th className="text-left px-3 py-2 font-semibold text-xs text-[var(--dg-text-muted)] w-10">#</th>
{result.columns.map(col => (
<th key={col} onClick={() => handleSort(col)}
@@ -473,7 +467,7 @@ export default function QueriesPage() {
{(sortedResult?.rows || []).slice(visibleRange.start, visibleRange.end).map((row, i) => {
const ri = visibleRange.start + i;
return (
<tr key={ri} className="border-b border-[var(--dg-border-light)] hover:bg-[var(--dg-bg-hover)] transition-colors" style={{ height: rowHeight }}>
<tr key={ri} className="dg-table-row" style={{ height: rowHeight }}>
<td className="px-3 py-2 text-[10px] text-[var(--dg-text-muted)] font-mono">{ri + 1}</td>
{result.columns.map(col => {
const isSensitive = sensitiveKeywords.some(k => col.toLowerCase().includes(k));
@@ -492,14 +486,14 @@ export default function QueriesPage() {
</table>
</div>
) : (
<div className="p-6 text-center text-[var(--dg-text-muted)] flex flex-col items-center gap-2">
<div className="p-6 text-center dg-text-muted flex flex-col items-center gap-2">
<Eye className="w-5 h-5" />
<span className="text-sm">无结果数据</span>
{result.affected_rows > 0 && <span className="text-xs">影响行数: {result.affected_rows}</span>}
</div>
)}
</CardContent>
</Card>
</div>
</div>
)}
{/* -- 安全提示 -- */}
@@ -516,9 +510,9 @@ export default function QueriesPage() {
<span className="text-sm font-semibold flex items-center gap-2">
<History className="w-4 h-4 text-blue-400" />
查询历史
{history.length > 0 && <span className="text-xs text-[var(--dg-text-muted)] font-normal">({history.length})</span>}
{history.length > 0 && <span className="text-xs dg-text-muted font-normal">({history.length})</span>}
</span>
<button onClick={() => setShowHistory(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">
<button onClick={() => setShowHistory(false)} className="dg-btn dg-btn-ghost dg-btn-sm p-1">
<PanelRightClose className="w-4 h-4" />
</button>
</div>
@@ -527,22 +521,18 @@ export default function QueriesPage() {
{history.length > 0 && (
<div className="px-4 pb-2 space-y-2">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--dg-text-muted)]" />
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3 h-3 dg-text-muted" />
<input className="w-full pl-8 pr-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-xs placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none"
placeholder="搜索历史..." value={searchHistory} onChange={e => setSearchHistory(e.target.value)} />
</div>
<div className="flex items-center gap-2">
<button onClick={() => setFavFilter(!favFilter)}
className={`flex items-center gap-1 px-2 py-1 text-[11px] rounded-md transition-all ${
favFilter ? 'bg-yellow-500/15 text-yellow-400 border border-yellow-500/30' : 'text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] border border-transparent'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${favFilter ? 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30' : ''}`}>
<Star className="w-3 h-3" />仅收藏
</button>
<div className="flex-1" />
<button onClick={handleClearHistory}
className={`flex items-center gap-1 px-2 py-1 text-[11px] rounded-md transition-all ${
clearConfirm ? 'text-red-400 bg-red-500/10' : 'text-[var(--dg-text-muted)] hover:text-red-400'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${clearConfirm ? 'text-red-400 bg-red-500/10' : ''}`}>
{clearConfirm ? <CheckCircle className="w-3 h-3" /> : <X className="w-3 h-3" />}
{clearConfirm ? '确认清除' : '清除'}
</button>
@@ -553,29 +543,29 @@ export default function QueriesPage() {
{/* 历史列表 */}
<div className="flex-1 overflow-y-auto">
{history.length === 0 ? (
<div className="p-6 text-center text-xs text-[var(--dg-text-muted)]">暂无查询历史</div>
<div className="p-6 text-center text-xs dg-text-muted">暂无查询历史</div>
) : filteredHistory.length === 0 ? (
<div className="p-6 text-center text-xs text-[var(--dg-text-muted)]">没有匹配的记录</div>
<div className="p-6 text-center text-xs dg-text-muted">没有匹配的记录</div>
) : (
<div className="divide-y divide-[var(--dg-border-light)]">
<div className="space-y-1.5 px-3 py-2">
{filteredHistory.map(h => (
<div key={h.id} className="px-4 py-2.5 hover:bg-[var(--dg-bg-hover)] transition-colors group">
<div key={h.id} className={`dg-card px-3 py-2.5 group ${h.favorite ? 'bg-yellow-500/5 border-yellow-500/20' : ''}`}>
<div className="flex items-center gap-2 mb-1">
{h.db_type && dbTypeIcon[h.db_type] && <img src={dbTypeIcon[h.db_type]} alt="" className="w-3.5 h-3.5 object-contain" />}
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${h.status === 'success' ? 'bg-green-500/15 text-green-400' : 'bg-red-500/15 text-red-400'}`}>
{h.execution_time_ms}ms
</span>
<span className="text-[10px] text-[var(--dg-text-muted)]">{new Date(h.created_at).toLocaleTimeString()}</span>
<span className="text-[10px] dg-text-muted">{new Date(h.created_at).toLocaleTimeString()}</span>
<div className="flex-1" />
<button onClick={() => toggleFavorite(h.id)}
className={`p-0.5 rounded transition-colors ${h.favorite ? 'text-yellow-400' : 'text-[var(--dg-text-muted)] opacity-0 group-hover:opacity-100 hover:text-yellow-400'}`}>
className={`p-0.5 rounded transition-colors ${h.favorite ? 'text-yellow-400' : 'dg-text-muted opacity-0 group-hover:opacity-100 hover:text-yellow-400'}`}>
{h.favorite ? <Star className="w-3 h-3 fill-current" /> : <StarOff className="w-3 h-3" />}
</button>
</div>
<div className="flex items-center gap-1">
<code className="flex-1 text-[11px] font-mono text-blue-400 truncate">{h.sql.substring(0, 80)}</code>
<button onClick={() => setSql(h.sql)}
className="p-1 rounded text-[var(--dg-text-muted)] hover:text-blue-400 transition-colors opacity-0 group-hover:opacity-100"
className="dg-btn dg-btn-ghost dg-btn-sm p-1 opacity-0 group-hover:opacity-100"
title="粘贴到编辑器">
<ClipboardPaste className="w-3 h-3" />
</button>
+69 -46
View File
@@ -5,7 +5,6 @@ import {
FileCode, Clock, Filter, X, Loader2, Users,
ArrowUpDown, Ban, ChevronLeft, AlertTriangle,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { statusMap, statusColorMap, dbTypeColorMap } from '@/lib/constants';
import { useActionPermission } from '@/hooks/useAuth';
import PageHeader from '@/components/PageHeader';
@@ -104,6 +103,28 @@ const displayStatusColorMap: Record<string, string> = {
rollback: 'bg-orange-500/15 text-orange-400',
};
/** Design-system badge class per status */
const dgBadgeStatusMap: Record<string, string> = {
pending: 'dg-badge-blue',
reviewing: 'dg-badge-blue',
approved: 'dg-badge-blue',
rejected: 'dg-badge-red',
executing: 'dg-badge-orange',
success: 'dg-badge-green',
failed: 'dg-badge-red',
rollback: 'dg-badge-orange',
};
/** Design-system badge class per db type */
const dgBadgeDbMap: Record<string, string> = {
mysql: 'dg-badge-blue',
postgres: 'dg-badge-purple',
postgresql: 'dg-badge-purple',
redis: 'dg-badge-red',
mongo: 'dg-badge-green',
mongodb: 'dg-badge-green',
};
const statusFlow = ['pending', 'reviewing', 'approved', 'executing', 'success'];
/* ------------------------------------------------------------------ */
@@ -211,13 +232,12 @@ export default function TicketsPage() {
return result;
}, [tickets, sortAsc]);
/* ---- Stats Cards ---- */
const statusCards = useMemo(() => [
{ key: '', label: '全部', count: stats?.total || 0, color: 'text-[var(--dg-text-primary)]', bg: 'bg-[var(--dg-bg-tertiary)]' },
{ key: 'pending', label: '待审批', count: (stats?.pending || 0) + (stats?.reviewing || 0) + (stats?.approved || 0), color: 'text-blue-400', bg: 'bg-blue-400/10' },
{ key: 'executing', label: '执行中', count: stats?.executing || 0, color: 'text-purple-400', bg: 'bg-purple-400/10' },
{ key: 'success', label: '完成', count: stats?.success || 0, color: 'text-green-400', bg: 'bg-green-400/10' },
{ key: 'failed', label: '失败', count: stats?.failed || 0, color: 'text-red-400', bg: 'bg-red-400/10' },
/* ---- Status Summary Cards (design system) ---- */
const statusSummaryCards = useMemo(() => [
{ key: 'pending', label: '待审核', count: (stats?.pending || 0) + (stats?.reviewing || 0) + (stats?.approved || 0), dot: 'dg-status-dot-blue' },
{ key: 'executing', label: '执行中', count: stats?.executing || 0, dot: 'dg-status-dot-orange' },
{ key: 'success', label: '已完成', count: stats?.success || 0, dot: 'dg-status-dot-green' },
{ key: 'failed', label: '失败/拒绝', count: (stats?.failed || 0) + (stats?.rejected || 0), dot: 'dg-status-dot-red' },
], [stats]);
/* ---- Open detail dialog ---- */
@@ -238,27 +258,31 @@ export default function TicketsPage() {
} : undefined}
/>
{/* Status Quick Filters */}
<div className="flex gap-2 flex-wrap">
{/* Status Summary Cards */}
<div className="grid grid-cols-4 gap-4">
{stats === null ? (
Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-9 w-24 animate-pulse rounded-lg bg-[var(--dg-bg-tertiary)]" />
Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="dg-card-section animate-pulse">
<div className="dg-card-header">
<div className="h-8 w-12 rounded bg-[var(--dg-bg-tertiary)]" />
<div className="h-3 w-16 rounded bg-[var(--dg-bg-tertiary)]" />
</div>
</div>
))
) : (
statusCards.map(card => (
statusSummaryCards.map(card => (
<button
key={card.key}
onClick={() => { setStatusFilter(card.key); setPage(1); }}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
statusFilter === card.key
? `${card.bg} ${card.color} ring-1 ring-current/20`
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
onClick={() => { setStatusFilter(statusFilter === card.key ? '' : card.key); setPage(1); }}
className={`dg-card-section text-left transition-all cursor-pointer hover:ring-1 hover:ring-[var(--dg-border)] ${
statusFilter === card.key ? 'ring-1 ring-blue-500/30 bg-blue-500/5' : ''
}`}
>
{card.label}
<span className={`text-xs px-1.5 py-0.5 rounded-full ${card.key === '' ? 'bg-[var(--dg-bg-hover)]' : 'bg-black/20'}`}>
{card.count}
</span>
<div className="dg-card-header flex items-center justify-between">
<span className="text-2xl font-bold">{card.count}</span>
<span className={`dg-status-dot ${card.dot}`} />
</div>
<span className="text-xs text-[var(--dg-text-secondary)] mt-1 block">{card.label}</span>
</button>
))
)}
@@ -269,7 +293,7 @@ export default function TicketsPage() {
<div className="relative flex-1 max-w-md">
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
<input
className="w-full pl-10 pr-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none transition-colors"
className="dg-input w-full pl-10 pr-4 py-2.5"
placeholder="搜索工单标题或SQL内容..."
value={search}
onChange={e => { setSearch(e.target.value); setPage(1); }}
@@ -283,13 +307,13 @@ export default function TicketsPage() {
</div>
{/* Ticket Table */}
<Card className="dg-card">
<CardContent className="p-0">
<div className="dg-card overflow-hidden">
<div className="p-0">
{loading && tickets.length === 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
<tr className="dg-table-header">
<th className="text-left px-4 py-3 font-semibold">工单编号</th>
<th className="text-left px-4 py-3 font-semibold">实例名称</th>
<th className="text-left px-4 py-3 font-semibold">提交人</th>
@@ -314,7 +338,7 @@ export default function TicketsPage() {
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
<tr className="dg-table-header">
<th className="text-left px-4 py-3 font-semibold">工单编号</th>
<th className="text-left px-4 py-3 font-semibold">实例名称</th>
<th className="text-left px-4 py-3 font-semibold">提交人</th>
@@ -332,7 +356,7 @@ export default function TicketsPage() {
{sortedTickets.map(ticket => (
<tr
key={ticket.id}
className="border-b border-[var(--dg-border-light)] hover:bg-[var(--dg-bg-hover)] transition-colors"
className="dg-table-row"
>
{/* 工单编号 */}
<td className="px-4 py-3.5">
@@ -345,7 +369,7 @@ export default function TicketsPage() {
{/* 实例名称 */}
<td className="px-4 py-3.5">
<div className="flex items-center gap-2">
<span className={`inline-flex px-1.5 py-0.5 rounded text-[10px] font-medium ${dbTypeColorMap[ticket.db_type] || ''}`}>
<span className={`dg-badge ${dgBadgeDbMap[ticket.db_type] || 'dg-badge-blue'}`}>
{ticket.db_type.toUpperCase()}
</span>
<span className="text-sm">{ticket.instance_name || '-'}</span>
@@ -380,8 +404,7 @@ export default function TicketsPage() {
{/* 状态 */}
<td className="px-4 py-3.5">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${displayStatusColorMap[ticket.status] || ''}`}>
<span className={`w-1.5 h-1.5 rounded-full ${ticket.status === 'executing' || ticket.status === 'reviewing' ? 'animate-pulse' : ''}`} />
<span className={`dg-badge ${dgBadgeStatusMap[ticket.status] || 'dg-badge-blue'}`}>
{displayStatusMap[ticket.status] || statusMap[ticket.status] || ticket.status}
</span>
</td>
@@ -396,7 +419,7 @@ export default function TicketsPage() {
<div className="flex gap-2">
<button
onClick={() => openDetail(ticket.id)}
className="flex items-center gap-1 text-blue-400 hover:text-blue-300 text-xs font-medium transition-colors"
className="dg-btn dg-btn-sm dg-btn-ghost"
>
<Eye className="w-3.5 h-3.5" />
{ticket.status === 'success' || ticket.status === 'failed' ? '详情' : '查看'}
@@ -410,7 +433,7 @@ export default function TicketsPage() {
fetchTickets(); fetchStats();
} catch { /* ignore */ }
}}
className="flex items-center gap-1 text-green-400 hover:text-green-300 text-xs font-medium transition-colors"
className="dg-btn dg-btn-sm dg-btn-ghost text-green-400 hover:text-green-300"
>
<CheckCircle className="w-3.5 h-3.5" />
通过
@@ -425,7 +448,7 @@ export default function TicketsPage() {
fetchTickets(); fetchStats();
} catch { /* ignore */ }
}}
className="flex items-center gap-1 text-red-400 hover:text-red-300 text-xs font-medium transition-colors"
className="dg-btn dg-btn-sm dg-btn-ghost text-red-400 hover:text-red-300"
>
<XCircle className="w-3.5 h-3.5" />
拒绝
@@ -440,7 +463,7 @@ export default function TicketsPage() {
fetchTickets(); fetchStats();
} catch { /* ignore */ }
}}
className="flex items-center gap-1 text-purple-400 hover:text-purple-300 text-xs font-medium transition-colors"
className="dg-btn dg-btn-sm dg-btn-ghost text-purple-400 hover:text-purple-300"
>
<Play className="w-3.5 h-3.5" />
执行
@@ -455,7 +478,7 @@ export default function TicketsPage() {
fetchTickets(); fetchStats();
} catch { /* ignore */ }
}}
className="flex items-center gap-1 text-amber-400 hover:text-amber-300 text-xs font-medium transition-colors"
className="dg-btn dg-btn-sm dg-btn-ghost text-amber-400 hover:text-amber-300"
>
<Ban className="w-3.5 h-3.5" />
撤销
@@ -478,7 +501,7 @@ export default function TicketsPage() {
<button
disabled={page <= 1}
onClick={() => setPage(p => p - 1)}
className="px-3 py-1.5 rounded-md text-sm text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] disabled:opacity-50 transition-colors"
className="dg-btn dg-btn-ghost dg-btn-sm disabled:opacity-50"
>
上一页
</button>
@@ -497,8 +520,8 @@ export default function TicketsPage() {
<button
key={p}
onClick={() => setPage(p)}
className={`px-3 py-1.5 rounded-md text-sm transition-colors ${
p === page ? 'bg-blue-500 text-white' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
className={`dg-btn dg-btn-sm dg-btn-ghost ${
p === page ? 'bg-blue-500 text-white hover:bg-blue-500' : ''
}`}
>
{p}
@@ -508,15 +531,15 @@ export default function TicketsPage() {
<button
disabled={page >= totalPages}
onClick={() => setPage(p => p + 1)}
className="px-3 py-1.5 rounded-md text-sm text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] disabled:opacity-50 transition-colors"
className="dg-btn dg-btn-ghost dg-btn-sm disabled:opacity-50"
>
下一页
</button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
{/* Detail Dialog */}
{detailTicket && (
@@ -581,7 +604,7 @@ function TicketDetailDialog({ ticket, onClose, canApprove, canExecute, onActionD
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--dg-border)]">
<div className="flex items-center gap-3">
<span className="font-mono text-xs text-[var(--dg-text-muted)]">{ticket.ticket_no || `TK-${ticket.id}`}</span>
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${displayStatusColorMap[ticket.status] || ''}`}>
<span className={`dg-badge ${dgBadgeStatusMap[ticket.status] || 'dg-badge-blue'}`}>
{displayStatusMap[ticket.status] || statusMap[ticket.status] || ticket.status}
</span>
</div>
@@ -591,14 +614,14 @@ function TicketDetailDialog({ ticket, onClose, canApprove, canExecute, onActionD
<button
onClick={() => handleReview('approve')}
disabled={loading}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-green-500/15 text-green-400 border border-green-500/30 rounded-md hover:bg-green-500/25 disabled:opacity-50 transition-all"
className="dg-btn dg-btn-sm bg-green-500/15 text-green-400 border border-green-500/30 hover:bg-green-500/25 disabled:opacity-50"
>
<CheckCircle className="w-3.5 h-3.5" /> 通过
</button>
<button
onClick={() => handleReview('reject')}
disabled={loading}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-red-500/15 text-red-400 border border-red-500/30 rounded-md hover:bg-red-500/25 disabled:opacity-50 transition-all"
className="dg-btn dg-btn-sm bg-red-500/15 text-red-400 border border-red-500/30 hover:bg-red-500/25 disabled:opacity-50"
>
<XCircle className="w-3.5 h-3.5" /> 拒绝
</button>
@@ -608,7 +631,7 @@ function TicketDetailDialog({ ticket, onClose, canApprove, canExecute, onActionD
<button
onClick={handleExecute}
disabled={loading}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-purple-500/15 text-purple-400 border border-purple-500/30 rounded-md hover:bg-purple-500/25 disabled:opacity-50 transition-all"
className="dg-btn dg-btn-sm bg-purple-500/15 text-purple-400 border border-purple-500/30 hover:bg-purple-500/25 disabled:opacity-50"
>
<Play className="w-3.5 h-3.5" /> 执行
</button>