diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index dd921c0..98a28f4 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -4,5 +4,6 @@ - 2026-03-25:初始化 `.helloagents/` 知识库骨架与首批模块文档,不代表源码功能变更。 - 2026-03-25:新增 GHCR Docker 发布 workflow,并将 `docker-compose.yml` 的三个业务镜像切换到 `ghcr.io/micah123321/*`。 -- 2026-03-25:`/workspace` 默认布局改为“左侧 Workbench + 中央视终端 + 右侧状态监控”,并在状态监控中新增开机累计上下行流量展示。 +- 2026-03-25:`/workspace` 默认布局改为“左侧 Workbench + 中央终端 + 右侧状态监控”,并在状态监控中新增开机累计上/下行流量展示。 - 2026-03-25:继续微调 `/workspace` Workbench,新增默认“快捷指令”标签、调整三栏宽度到更接近 xterminal 参考图,并修复终端区域鼠标悬停时指针异常消失的问题。 +- 2026-03-25:前端主站视觉语言统一升级为 `Slate Control Center`,新增公共页面壳层与认证壳层,并重做 Dashboard、Settings、Login、Setup、Notifications、Proxies、Audit Logs、Workbench、StatusMonitor 的现代化 UI 表达。 diff --git a/.helloagents/modules/frontend.md b/.helloagents/modules/frontend.md index 366ce5e..442cb28 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -45,3 +45,9 @@ 依赖: workspace-root, backend, remote-gateway, vue-router, pinia 被依赖: 无 ``` + +## 最近变更 + +- 2026-03-25: 前端主站视觉语言统一切换为 `Slate Control Center`,新增 `PageShell.vue` 与 `AuthPanelLayout.vue` 作为主要页面和认证入口的统一壳层。 +- 2026-03-25: `/workspace` 继续保持“三栏工作台”结构,但左侧 `Workbench` 与右侧 `StatusMonitor` 已重做为更现代的 Element Plus 控制中心风格;状态监控保留并展示开机累计上/下行流量。 +- 2026-03-25: `Dashboard / Settings / Login / Setup / Notifications / Proxies / Audit Logs` 已统一接入新的卡片化表达、控制区和统计信息风格,后续同类页面优先复用公共壳层而不是单页散落自定义布局。 diff --git a/.helloagents/plan/202603250419_frontend-slate-control-center/.status.json b/.helloagents/plan/202603250419_frontend-slate-control-center/.status.json new file mode 100644 index 0000000..9aca62b --- /dev/null +++ b/.helloagents/plan/202603250419_frontend-slate-control-center/.status.json @@ -0,0 +1,11 @@ +{ + "status": "completed", + "completed": 8, + "failed": 0, + "pending": 0, + "total": 8, + "done": 8, + "percent": 100, + "current": "已完成 Slate Control Center 全站前端重绘并通过 packages/frontend 构建验证", + "updated_at": "2026-03-25 05:20:00" +} diff --git a/.helloagents/plan/202603250419_frontend-slate-control-center/proposal.md b/.helloagents/plan/202603250419_frontend-slate-control-center/proposal.md new file mode 100644 index 0000000..0d010b9 --- /dev/null +++ b/.helloagents/plan/202603250419_frontend-slate-control-center/proposal.md @@ -0,0 +1,159 @@ +# 变更提案: frontend-slate-control-center + +## 元信息 +```yaml +类型: 重构 / 优化 +方案类型: implementation +优先级: P1 +状态: 草稿 +创建: 2026-03-25 +``` + +--- + +## 1. 需求 + +### 背景 +当前 `packages/frontend` 已接入 `Element Plus`,但绝大多数主页面仍停留在早期 Tailwind 原子类和零散自定义变量的混合状态。页面之间的视觉语言不统一,导航壳层、卡片、表格、筛选区、登录/初始化页、工作区侧边工作台都缺少一套一致的“控制中心”设计表达。用户已确认对整个前端站点做统一视觉重做,并指定采用“方案 A: Slate Control Center”。 + +### 目标 +- 建立一套贯穿全站的 Slate Control Center 视觉语言,包括颜色、排版、圆角、阴影、边框、背景层次和状态语义。 +- 让顶部导航、主内容区、卡片容器、过滤操作区、表格、表单、标签和空状态统一使用更现代的 `Element Plus` 风格与封装。 +- 重做主要页面的壳层和关键交互,包括 `Dashboard`、`Workspace`、`Connections`、`Proxies`、`Notifications`、`Audit Logs`、`Settings`、`Login`、`Setup`。 +- 保持现有业务逻辑、Pinia store、路由和多语言能力不变,尽量将改动集中在壳层和组件表达层。 + +### 约束条件 +```yaml +时间约束: 当前轮次内完成可运行的前端重构与构建验证 +性能约束: 不引入新的重量级 UI 框架,仅基于现有 Element Plus、Vue 3 和样式层重构 +兼容性约束: 保持现有路由、状态管理、组件接口和核心业务流程兼容 +业务约束: /workspace 的三栏工作台结构保持既有决策,仅升级视觉语言和容器表达 +``` + +### 验收标准 +- [ ] 全局样式令牌和 Element Plus 主题变量统一,顶层导航和页面容器具备一致的 Slate Control Center 风格。 +- [ ] `Dashboard`、`Connections`、`Settings`、`Login`、`Setup` 等主页面完成现代化重绘,优先采用 `Element Plus` 容器、表单、标签页、表格、统计卡片等组件。 +- [ ] `/workspace` 的 Workbench、终端主区和状态监控面板在视觉上完成统一升级,保留当前结构与主要交互。 +- [ ] `npm --workspace packages/frontend run build` 通过。 + +--- + +## 2. 方案 + +### 技术方案 +本次改造以“壳层统一 + 样式令牌统一 + 主页面容器重做 + 工作区局部深度优化”为主: + +- 在全局样式层重建 `style.css`,统一品牌色、页面背景、面板层级、边框、阴影、字体和 `Element Plus` CSS 变量映射。 +- 在应用壳层引入统一页面容器、导航信息层、操作条和页面标题表达,逐步替换现有分散的 Tailwind 片段。 +- 对主页面优先使用 `Element Plus` 的 `ElContainer`、`ElCard`、`ElTabs`、`ElTable`、`ElForm`、`ElInput`、`ElButton`、`ElTag`、`ElEmpty`、`ElAlert`、`ElSegmented` 等组件表达。 +- 对 `/workspace` 保持布局树与会话逻辑不变,只重做 Workbench、状态监控、终端外围容器和工作区背景层次。 +- 尽量新增轻量公共组件,减少把整套视觉逻辑硬编码在每个 view 里。 + +### 影响范围 +```yaml +涉及模块: + - frontend: 全局样式令牌、应用壳层、主页面容器、工作区视觉重构 + - workspace-root: 仅同步上下文与变更记录,不改变后端/部署逻辑 +预计变更文件: 12-20 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 全局样式变量重写影响现有细节组件 | 中 | 保留核心语义变量名,优先在壳层和新公共组件内消费 | +| 旧页面使用大量原子类,迁移后局部布局错位 | 中 | 先重做公共壳层与主页面,再做工作区和高复杂页面 | +| Element Plus 引入更多容器后局部交互样式不一致 | 中 | 统一页面级卡片、表单、筛选条和表格封装 | +| 工作区组件过多,若全量重写会超出单轮范围 | 低 | 聚焦外层容器、Workbench、状态监控与终端壳层,不动核心终端协议与会话逻辑 | + +--- + +## 3. 技术设计 + +### 架构设计 +```mermaid +flowchart TD + A[style.css 设计令牌] --> B[Element Plus 主题变量] + B --> C[App 全局导航壳层] + B --> D[页面级容器组件] + D --> E[Dashboard / Connections / Settings / Notifications] + D --> F[Login / Setup / Proxies / Audit Logs] + B --> G[Workspace 容器与 Workbench / StatusMonitor] +``` + +### 关键设计拆分 +- 建立全局设计令牌层:背景分层、标题字体、面板边框、状态颜色、交互高亮、玻璃化和工业控制台感阴影。 +- 构建公共页面壳层:统一页面标题、描述、右侧操作区、统计条和内容区卡片边界。 +- 重做主要页面表达:从“表单/列表堆叠”升级为“控制中心”型信息组织。 +- Workspace 采用“Slate 控制台”表达:左侧工作台像资源侧栏,中部终端维持主位,右侧状态监控更像运维仪表卡片。 + +--- + +## 4. 核心场景 + +### 场景: 主站点统一视觉语言 +**模块**: frontend +**条件**: 用户进入 Dashboard、Connections、Settings 等主要页面 +**行为**: 页面统一使用 Slate Control Center 风格的壳层、卡片、筛选区和内容容器 +**结果**: 主站点视觉语言一致,Element Plus 使用比例显著提升 + +### 场景: 工作区现代化控制台 +**模块**: frontend +**条件**: 用户进入 `/workspace` +**行为**: 维持三栏结构不变,但重做背景层次、Workbench 容器、状态卡片和终端外围外观 +**结果**: 工作区看起来像现代控制中心,而非原始拆分面板 + +### 场景: 认证入口统一品牌化 +**模块**: frontend +**条件**: 用户访问 `/login` 或 `/setup` +**行为**: 登录与初始化页面共享统一品牌板式、表单容器和引导文案层次 +**结果**: 首次进入体验与主站点风格统一 + +--- + +## 5. 技术决策 + +### frontend-slate-control-center#D001: 以全局令牌 + 页面壳层重构替代逐页零散修补 +**日期**: 2026-03-25 +**状态**: 已采纳 +**背景**: 现有页面风格分散,若继续逐页修补会反复复制样式,且无法建立统一设计语言。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 先重建全局令牌和壳层,再逐页替换 | 一致性强,后续页面改造成本更低 | 首次改动面更大 | +| B: 逐页局部替换样式类 | 单页改动小 | 风格难统一,维护成本高 | +**决策**: 选择方案 A +**理由**: 用户明确要求“改整个前端站点,所有主要页面统一重做视觉语言和组件风格”,必须先建立统一底座。 +**影响**: `style.css`、`App.vue`、主要视图文件、部分工作区组件都会调整 + +### frontend-slate-control-center#D002: 主要页面优先采用 Element Plus 容器与表单表达 +**日期**: 2026-03-25 +**状态**: 已采纳 +**背景**: 仓库已引入 `Element Plus`,但当前使用率极低,无法体现组件库一致性。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 以 Element Plus 为主,Tailwind 负责间距和局部布局 | 组件统一、主题变量统一、开发效率更高 | 需补主题映射 | +| B: 继续主要依赖 Tailwind 原子类 | 灵活 | 风格容易继续碎片化 | +**决策**: 选择方案 A +**理由**: 与用户要求直接一致,并且 Element Plus 已在依赖和入口中可用。 +**影响**: 主页面会增加 `ElCard`、`ElInput`、`ElButton`、`ElTabs`、`ElTable` 等使用 + +--- + +## 6. 成果设计 + +### 设计方向 +- **美学基调**: Slate Control Center。整体是石板灰、雾面蓝灰、冷白高光的专业控制中心,不走炫技霓虹,也不回到传统后台白底表格。 +- **记忆点**: 顶层导航和各页面的“控制台头部条”,带有浅层玻璃感、信息徽标和有节奏的卡片层级,形成像现代运维控制中心的视觉记忆。 +- **参考**: xterminal 的控制台感布局重心 + Element Plus 的信息密度与组件一致性 + 工业面板式层级。 + +### 视觉要素 +- **配色**: 主背景使用深浅交叠的 slate 灰蓝体系,内容面板使用高亮浅灰卡片,强调色使用冷蓝与青绿色,危险态保留橙红。 +- **字体**: 标题使用更有控制台气质的窄体/几何感字体栈,正文使用清晰的中文优先无衬线;代码和状态数字保持等宽字体。 +- **布局**: 顶部导航更扁平且信息化;主页面采用“标题信息头 + 统计带 + 主内容卡片”的层次;Workspace 保持三栏但加重容器感与边界感。 +- **动效**: 页面头部、卡片和标签切换采用短促的位移与透明度过渡;悬停强调边框和阴影,不堆砌复杂动画。 +- **氛围**: 背景带有轻微径向渐变和柔和网格/噪点质感,卡片使用浅玻璃边缘和柔和阴影,突出现代专业控制中心质感。 + +### 技术约束 +- **可访问性**: 保持表单、按钮、标签页和表格的键盘可达性,确保浅色文本对比满足阅读要求。 +- **响应式**: 主站页面在桌面优先,保留既有移动端退化逻辑;Workspace 移动端不改变现有交互链路。 diff --git a/.helloagents/plan/202603250419_frontend-slate-control-center/tasks.md b/.helloagents/plan/202603250419_frontend-slate-control-center/tasks.md new file mode 100644 index 0000000..be89f9b --- /dev/null +++ b/.helloagents/plan/202603250419_frontend-slate-control-center/tasks.md @@ -0,0 +1,60 @@ +# 任务清单: frontend-slate-control-center + +```yaml +@feature: frontend-slate-control-center +@created: 2026-03-25 +@status: completed +@mode: R3 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 8 | 0 | 0 | 8 | + +### LIVE_STATUS +```yaml +status: completed +current: 已完成 Slate Control Center 全站前端重绘并通过 packages/frontend 构建验证 +completed: 8 +failed: 0 +pending: 0 +total: 8 +done: 8 +percent: 100 +updated_at: 2026-03-25 05:20:00 +``` + +--- + +## 任务列表 + +### 1. 设计底座与公共壳层 +- [√] 1.1 在 `packages/frontend/src/style.css` 中重建 Slate Control Center 全局设计令牌与 Element Plus 主题变量桥接 | depends_on: [] +- [√] 1.2 在 `packages/frontend/src/App.vue` 中重做全局导航、页面背景和应用壳层表达 | depends_on: [1.1] +- [√] 1.3 新增公共页面容器组件,用于统一主要页面标题、描述、操作区和内容卡片风格 | depends_on: [1.1] + +### 2. 主要页面现代化 +- [√] 2.1 重做 `packages/frontend/src/views/DashboardView.vue`,将概览区升级为控制中心式信息卡片与操作面板 | depends_on: [1.1, 1.3] +- [√] 2.2 重做 `packages/frontend/src/views/ConnectionsView.vue`、`packages/frontend/src/views/ProxiesView.vue`、`packages/frontend/src/views/AuditLogView.vue`、`packages/frontend/src/views/NotificationsView.vue` 的页面容器和主操作区,优先接入 Element Plus 组件 | depends_on: [1.1, 1.3] +- [√] 2.3 重做 `packages/frontend/src/views/SettingsView.vue` 的设置导航和内容容器层次,使其符合统一控制中心风格 | depends_on: [1.1, 1.3] + +### 3. 认证入口与工作区 +- [√] 3.1 重做 `packages/frontend/src/views/LoginView.vue` 与 `packages/frontend/src/views/SetupView.vue`,统一品牌入口视觉和表单表达 | depends_on: [1.1] +- [√] 3.2 重做 `packages/frontend/src/components/WorkspaceWorkbench.vue`、`packages/frontend/src/components/StatusMonitor.vue`,并修复终端区域鼠标进入时的光标表现 | depends_on: [1.1, 1.2] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-03-25 04:19:00 | 创建设计方案包 | completed | `create_package.py` 因编码损坏不可用,已按规则手动降级创建 | +| 2026-03-25 05:20:00 | 完成前端主页面与工作区视觉重绘 | completed | `npm --workspace packages/frontend run build` 通过 | + +--- + +## 执行备注 + +> 本轮以前端视觉语言统一和页面容器重塑为主,不改动后端 API 协议与核心会话逻辑;`/workspace` 继续保持“左侧 Workbench + 中央终端 + 右侧状态监控”的主结构。 diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index 234a836..379a18c 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -25,294 +25,230 @@ const authStore = useAuthStore(); const settingsStore = useSettingsStore(); const appearanceStore = useAppearanceStore(); const layoutStore = useLayoutStore(); -const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++ -const sessionStore = useSessionStore(); // +++ 实例化 Session Store +++ -const dialogStore = useDialogStore(); // +++ 实例化 DialogStore +++ -const { state: dialogState } = storeToRefs(dialogStore); -const favoritePathsStore = useFavoritePathsStore(); // +++ 实例化 favoritePathsStore +++ +const focusSwitcherStore = useFocusSwitcherStore(); +const sessionStore = useSessionStore(); +const dialogStore = useDialogStore(); +const { state: dialogState } = storeToRefs(dialogStore); +const favoritePathsStore = useFavoritePathsStore(); const { isAuthenticated } = storeToRefs(authStore); const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); const { isStyleCustomizerVisible } = storeToRefs(appearanceStore); -const { isLayoutVisible, isHeaderVisible } = storeToRefs(layoutStore); // 添加 isHeaderVisible +const { isHeaderVisible } = storeToRefs(layoutStore); const { isConfiguratorVisible: isFocusSwitcherVisible } = storeToRefs(focusSwitcherStore); -const { isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } = storeToRefs(sessionStore); // +++ 获取 RDP 和 VNC 状态 +++ +const { isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } = storeToRefs(sessionStore); const { isMobile } = useDeviceDetection(); const route = useRoute(); const navRef = ref(null); const underlineRef = ref(null); -// +++ 存储上一次由切换器聚焦的 ID +++ const lastFocusedIdBySwitcher = ref(null); -const isAltPressed = ref(false); // 跟踪 Alt 键是否按下 +const isAltPressed = ref(false); const altShortcutKey = ref(null); -// --- 移除 shortcutTriggeredInKeyDown 标志 --- const updateUnderline = async () => { - await nextTick(); // 等待 DOM 更新 + await nextTick(); if (navRef.value && underlineRef.value) { const activeLink = navRef.value.querySelector('.router-link-exact-active') as HTMLElement; if (activeLink) { - const offsetBottom = 2; // 下划线距离文字底部的距离 (px) underlineRef.value.style.left = `${activeLink.offsetLeft}px`; underlineRef.value.style.width = `${activeLink.offsetWidth}px`; - // underlineRef.value.style.top = `${activeLink.offsetTop + activeLink.offsetHeight + offsetBottom}px`; // 移除 top 设置 - underlineRef.value.style.opacity = '1'; // Make it visible + underlineRef.value.style.opacity = '1'; } else { - underlineRef.value.style.opacity = '0'; // Hide if no active link (e.g., on login page if not a nav link) + underlineRef.value.style.opacity = '0'; } } }; onMounted(() => { - // Initial position update - // Use setTimeout to ensure styles are applied and elements have dimensions setTimeout(updateUnderline, 100); - // +++ 全局 Alt 键监听器 +++ - window.addEventListener('keydown', handleAltKeyDown); // +++ 监听 keydown 设置状态 +++ - window.addEventListener('keyup', handleGlobalKeyUp); // +++ 监听 keyup 执行切换 +++ - - // PWA Install Prompt - window.addEventListener('beforeinstallprompt', (e) => { + window.addEventListener('keydown', handleAltKeyDown); + window.addEventListener('keyup', handleGlobalKeyUp); + + window.addEventListener('beforeinstallprompt', () => { console.log('[App.vue] beforeinstallprompt event fired. Browser will handle install prompt.'); }); window.addEventListener('appinstalled', () => { console.log('[App.vue] PWA was installed'); }); - - // +++ 加载 Header 可见性状态 +++ + layoutStore.loadHeaderVisibility(); - }); -// +++ 监听用户认证状态,登录后初始化收藏路径 +++ -watch(isAuthenticated, (loggedIn) => { - if (loggedIn) { - favoritePathsStore.initializeFavoritePaths(t); - } -}, { immediate: true }); +watch( + isAuthenticated, + (loggedIn) => { + if (loggedIn) { + favoritePathsStore.initializeFavoritePaths(t); + } + }, + { immediate: true } +); -// +++ 卸载钩子以移除监听器 +++ onUnmounted(() => { - window.removeEventListener('keydown', handleAltKeyDown); // +++ 移除 keydown 监听 +++ - window.removeEventListener('keyup', handleGlobalKeyUp); // +++ 移除 keyup 监听 +++ + window.removeEventListener('keydown', handleAltKeyDown); + window.removeEventListener('keyup', handleGlobalKeyUp); }); - -// *** 计算属性,判断是否在 workspace 路由 *** const isWorkspaceRoute = computed(() => route.path === '/workspace'); -watch(route, () => { - updateUnderline(); -}, { immediate: true }); // *** 确保 immediate: true 存在 *** - +watch( + route, + () => { + updateUnderline(); + }, + { immediate: true } +); const handleLogout = () => { authStore.logout(); }; -// 打开样式自定义器的方法现在直接调用 store action const openStyleCustomizer = () => { appearanceStore.toggleStyleCustomizer(true); }; -// 关闭样式自定义器的方法现在也调用 store action const closeStyleCustomizer = () => { appearanceStore.toggleStyleCustomizer(false); }; -// +++ 处理 Alt 键按下的事件处理函数,并记录快捷键 +++ -const handleAltKeyDown = async (event: KeyboardEvent) => { // +++ 改为 async +++ - if (!isWorkspaceRoute.value) return; // 只在 workspace 路由下执行 - // 只在 Alt 键首次按下时设置状态 +const handleAltKeyDown = async (event: KeyboardEvent) => { + if (!isWorkspaceRoute.value) return; + if (event.key === 'Alt' && !event.repeat) { isAltPressed.value = true; altShortcutKey.value = null; - // console.log('[App] Alt key pressed down.'); } else if (isAltPressed.value && !['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) { - // 如果 Alt 正被按住,且按下了非修饰键 (移除 !shortcutTriggeredInKeyDown 检查) let key = event.key; if (key.length === 1) key = key.toUpperCase(); if (/^[a-zA-Z0-9]$/.test(key)) { - altShortcutKey.value = key; // 记录按键 - const shortcutString = `Alt+${key}`; - console.log(`[App] KeyDown: Alt+${key} detected. Checking shortcut: ${shortcutString}`); - const targetId = focusSwitcherStore.getFocusTargetIdByShortcut(shortcutString); + altShortcutKey.value = key; + const shortcutString = `Alt+${key}`; + const targetId = focusSwitcherStore.getFocusTargetIdByShortcut(shortcutString); - if (targetId) { - console.log(`[App] KeyDown: Shortcut match found. Targeting ID: ${targetId}`); - event.preventDefault(); // 阻止默认行为 (如菜单) - const success = await focusSwitcherStore.focusTarget(targetId); // +++ 立即尝试聚焦 +++ - if (success) { - console.log(`[App] KeyDown: Successfully focused ${targetId} via shortcut.`); - lastFocusedIdBySwitcher.value = targetId; - // --- 移除设置标志位 --- - } else { - console.log(`[App] KeyDown: Failed to focus ${targetId} via shortcut action.`); - // 聚焦失败,可以选择是否取消 Alt 状态,暂时不处理,让 keyup 重置 - } - } else { - console.log(`[App] KeyDown: No configured shortcut found for ${shortcutString}.`); - // 没有匹配的快捷键,可以选择取消 Alt 状态以允许默认行为,或保持状态等待 keyup - // isAltPressed.value = false; - // altShortcutKey.value = null; + if (targetId) { + event.preventDefault(); + const success = await focusSwitcherStore.focusTarget(targetId); + if (success) { + lastFocusedIdBySwitcher.value = targetId; } + } } else { - // 按下无效键 (非字母数字),取消 Alt 状态 - isAltPressed.value = false; - altShortcutKey.value = null; - // --- 移除重置标志位 --- - console.log('[App] KeyDown: Alt sequence cancelled by non-alphanumeric key press.'); - } - } else if (isAltPressed.value && ['Control', 'Shift', 'Meta'].includes(event.key)) { - // 按下其他修饰键,取消 Alt 状态 isAltPressed.value = false; altShortcutKey.value = null; - // --- 移除重置标志位 --- - console.log('[App] KeyDown: Alt sequence cancelled by other modifier key press.'); + } + } else if (isAltPressed.value && ['Control', 'Shift', 'Meta'].includes(event.key)) { + isAltPressed.value = false; + altShortcutKey.value = null; } }; -// +++ 全局键盘事件处理函数,监听 keyup,优先处理快捷键 +++ const handleGlobalKeyUp = async (event: KeyboardEvent) => { - if (!isWorkspaceRoute.value) return; // 只在 workspace 路由下执行 - if (event.key === 'Alt') { - const altWasPressed = isAltPressed.value; - const triggeredShortcutKey = altShortcutKey.value; // 记录松开时是否有记录的快捷键 + if (!isWorkspaceRoute.value) return; + if (event.key !== 'Alt') return; - // 总是重置状态 - isAltPressed.value = false; - altShortcutKey.value = null; - // --- 移除重置标志位 --- + const altWasPressed = isAltPressed.value; + const triggeredShortcutKey = altShortcutKey.value; - if (altWasPressed && triggeredShortcutKey === null) { - // 如果 Alt 之前是按下的,并且没有记录到有效的快捷键,则执行顺序切换 - console.log('[App] KeyUp: Alt released without a valid shortcut key captured. Attempting sequential focus switch.'); - event.preventDefault(); // 仅在执行顺序切换时阻止默认行为 + isAltPressed.value = false; + altShortcutKey.value = null; - // --- 顺序切换逻辑 (保持不变) --- - let currentFocusId: string | null = lastFocusedIdBySwitcher.value; - console.log(`[App] Sequential switch. Last focused by switcher: ${currentFocusId}`); + if (altWasPressed && triggeredShortcutKey === null) { + event.preventDefault(); - if (!currentFocusId) { - const activeElement = document.activeElement as HTMLElement; - if (activeElement && activeElement.hasAttribute('data-focus-id')) { - currentFocusId = activeElement.getAttribute('data-focus-id'); - console.log(`[App] Sequential switch. Found focus ID from activeElement: ${currentFocusId}`); - } else { - console.log(`[App] Sequential switch. Could not determine current focus ID.`); - } + let currentFocusId: string | null = lastFocusedIdBySwitcher.value; + + if (!currentFocusId) { + const activeElement = document.activeElement as HTMLElement; + if (activeElement && activeElement.hasAttribute('data-focus-id')) { + currentFocusId = activeElement.getAttribute('data-focus-id'); + } + } + + const order = focusSwitcherStore.sequenceOrder; + if (order.length === 0) { + return; + } + + let focused = false; + for (let i = 0; i < order.length; i += 1) { + const nextFocusId = focusSwitcherStore.getNextFocusTargetId(currentFocusId); + if (!nextFocusId) { + break; } - const order = focusSwitcherStore.sequenceOrder; // ++ 使用新的 sequenceOrder state ++ - if (order.length === 0) { // ++ 检查新的 state ++ - console.log('[App] No focus sequence configured.'); - return; + const success = await focusSwitcherStore.focusTarget(nextFocusId); + if (success) { + lastFocusedIdBySwitcher.value = nextFocusId; + focused = true; + break; } - let focused = false; - for (let i = 0; i < order.length; i++) { // ++ Use order.length for loop condition ++ - const nextFocusId = focusSwitcherStore.getNextFocusTargetId(currentFocusId); - if (!nextFocusId) { - console.warn('[App] Could not determine next focus target ID in sequence.'); - break; - } + currentFocusId = nextFocusId; + } - console.log(`[App] Sequential switch. Trying to focus target ID: ${nextFocusId}`); - const success = await focusSwitcherStore.focusTarget(nextFocusId); - - if (success) { - console.log(`[App] Successfully focused ${nextFocusId} sequentially.`); - lastFocusedIdBySwitcher.value = nextFocusId; - focused = true; - break; - } else { - console.log(`[App] Failed to focus ${nextFocusId} sequentially. Trying next...`); - currentFocusId = nextFocusId; - } - } - - if (!focused) { - console.log('[App] Cycled through sequence, no target could be focused.'); - lastFocusedIdBySwitcher.value = null; - } - // --- 顺序切换逻辑结束 --- - - } else if (altWasPressed && triggeredShortcutKey !== null) { - console.log(`[App] KeyUp: Alt released after capturing key '${triggeredShortcutKey}'. Shortcut logic handled in keydown. No sequential switch.`); - // 快捷键逻辑已在 keydown 处理,keyup 时无需操作,也不阻止默认行为(除非特定需要) - } else { - // Alt 松开,但 isAltPressed 已经是 false (例如被其他键取消了) - console.log('[App] KeyUp: Alt released, but sequence was already cancelled or not active.'); + if (!focused) { + lastFocusedIdBySwitcher.value = null; } } }; - -// +++ 辅助函数:检查元素是否可见且可聚焦 +++ -const isElementVisibleAndFocusable = (element: HTMLElement): boolean => { - if (!element) return false; - // 检查元素是否在 DOM 中,并且没有 display: none - const style = window.getComputedStyle(element); - if (style.display === 'none' || style.visibility === 'hidden') return false; - // 检查元素或其父元素是否被禁用 - if ((element as HTMLInputElement).disabled) return false; - let parent = element.parentElement; - while (parent) { - if ((parent as HTMLFieldSetElement).disabled) return false; - parent = parent.parentElement; - } - // 检查元素是否足够在视口内(粗略检查) - const rect = element.getBoundingClientRect(); - return rect.width > 0 && rect.height > 0; -}; - - - @@ -371,13 +288,197 @@ const isElementVisibleAndFocusable = (element: HTMLElement): boolean => { display: flex; flex-direction: column; min-height: 100vh; - font-family: var(--font-family-sans-serif); /* 使用字体变量 */ + position: relative; + font-family: var(--font-family-sans-serif); } +.app-shell__backdrop { + position: fixed; + inset: 0; + pointer-events: none; + background: + radial-gradient(circle at top right, rgba(60, 105, 231, 0.08), transparent 28%), + linear-gradient(180deg, rgba(255, 255, 255, 0.16), transparent 22%); +} -main { +.app-topbar { + position: sticky; + top: 0; + z-index: 30; + padding: 1rem 1rem 0; +} + +.app-topbar__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.9rem 1.1rem; + border-radius: 24px; + border: 1px solid rgba(103, 124, 155, 0.18); + background: var(--header-bg-color); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px); +} + +.app-topbar__left, +.app-topbar__right { + display: flex; + align-items: center; + gap: 1rem; + min-width: 0; +} + +.app-brand { + display: inline-flex; + align-items: center; + gap: 0.85rem; + padding-right: 0.35rem; +} + +.app-brand__logo { + width: 42px; + height: 42px; + object-fit: contain; +} + +.app-brand__copy { + display: flex; + flex-direction: column; + min-width: 0; +} + +.app-brand__title { + font-family: var(--font-family-display); + font-size: 1rem; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--text-color); +} + +.app-brand__subtitle { + font-size: 0.72rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-color-tertiary); +} + +.app-nav { + position: relative; + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem; + border-radius: 18px; + background: rgba(241, 245, 251, 0.9); +} + +.app-nav__link { + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.72rem 1rem; + border-radius: 14px; + color: var(--text-color-secondary); + font-size: 0.9rem; + font-weight: 600; + transition: color 0.2s ease; +} + +.app-nav__link.is-active { + color: var(--primary-color); +} + +.app-nav__underline { + position: absolute; + bottom: 0.35rem; + height: calc(100% - 0.7rem); + border-radius: 14px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(239, 244, 252, 0.94)); + box-shadow: 0 10px 24px rgba(34, 56, 93, 0.12); + transition: all 0.3s ease-in-out; + opacity: 0; + transform: translateY(0); + pointer-events: none; +} + +.app-icon-button, +.app-auth-link { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 40px; + padding: 0 0.9rem; + border: 1px solid rgba(103, 124, 155, 0.18); + border-radius: 14px; + background: rgba(255, 255, 255, 0.72); + color: var(--text-color-secondary); + font-size: 0.9rem; + font-weight: 600; + transition: all 0.2s ease; +} + +.app-icon-button:hover, +.app-auth-link:hover { + color: var(--text-color); + border-color: rgba(60, 105, 231, 0.26); + background: rgba(255, 255, 255, 0.94); +} + +.app-icon-button { + width: 40px; + padding: 0; +} + +.app-auth-link--primary { + background: linear-gradient(135deg, rgba(60, 105, 231, 0.14), rgba(39, 70, 184, 0.08)); + color: var(--primary-color); +} + +.app-main { flex-grow: 1; - + position: relative; + padding-bottom: 1rem; } +@media (max-width: 1100px) { + .app-topbar__inner { + flex-direction: column; + align-items: stretch; + } + + .app-topbar__left, + .app-topbar__right { + width: 100%; + justify-content: space-between; + flex-wrap: wrap; + } + + .app-nav { + flex-wrap: wrap; + } +} + +@media (max-width: 720px) { + .app-topbar { + padding: 0.75rem 0.75rem 0; + } + + .app-topbar__inner { + padding: 0.8rem; + border-radius: 20px; + } + + .app-brand__subtitle { + display: none; + } + + .app-nav__link { + padding: 0.64rem 0.82rem; + font-size: 0.84rem; + } +} diff --git a/packages/frontend/src/components/AuthPanelLayout.vue b/packages/frontend/src/components/AuthPanelLayout.vue new file mode 100644 index 0000000..1737a38 --- /dev/null +++ b/packages/frontend/src/components/AuthPanelLayout.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/packages/frontend/src/components/PageShell.vue b/packages/frontend/src/components/PageShell.vue new file mode 100644 index 0000000..ecaf1db --- /dev/null +++ b/packages/frontend/src/components/PageShell.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/packages/frontend/src/components/StatusMonitor.vue b/packages/frontend/src/components/StatusMonitor.vue index 4977b89..4367017 100644 --- a/packages/frontend/src/components/StatusMonitor.vue +++ b/packages/frontend/src/components/StatusMonitor.vue @@ -1,204 +1,34 @@ - - - + + diff --git a/packages/frontend/src/components/Terminal.vue b/packages/frontend/src/components/Terminal.vue index c2d0b44..4e2f244 100644 --- a/packages/frontend/src/components/Terminal.vue +++ b/packages/frontend/src/components/Terminal.vue @@ -743,7 +743,7 @@ watchEffect(() => { .terminal-inner-container :deep(.xterm), .terminal-inner-container :deep(.xterm-screen), .terminal-inner-container :deep(.xterm-viewport) { - cursor: default !important; + cursor: text !important; } .terminal-inner-container :deep(.xterm .xterm-cursor-pointer) { diff --git a/packages/frontend/src/components/WorkspaceWorkbench.vue b/packages/frontend/src/components/WorkspaceWorkbench.vue index abbe199..5c7aa8c 100644 --- a/packages/frontend/src/components/WorkspaceWorkbench.vue +++ b/packages/frontend/src/components/WorkspaceWorkbench.vue @@ -49,33 +49,42 @@ const workbenchTabs = computed(() => [ { id: 'quickCommands' as const, label: t('workspace.workbench.tabs.quickCommands', '快捷指令'), + shortLabel: t('workspace.workbench.tabs.quickCommands', '快捷指令'), icon: 'fas fa-bolt', + hint: t('workspace.workbench.quickCommandsHint', '默认面板,用于常用命令与预置脚本。'), }, { id: 'files' as const, label: t('workspace.workbench.tabs.files', '文件'), - icon: 'fas fa-folder-open', + shortLabel: t('workspace.workbench.tabs.files', '文件'), + icon: 'fas fa-folder-tree', + hint: t('workspace.workbench.filesHint', '浏览远程目录、拖放文件与操作资源。'), }, { id: 'history' as const, label: t('workspace.workbench.tabs.history', '历史命令'), - icon: 'fas fa-history', + shortLabel: t('workspace.workbench.tabs.history', '历史命令'), + icon: 'fas fa-clock-rotate-left', + hint: t('workspace.workbench.historyHint', '回放最近命令并快速重发到当前会话。'), }, { id: 'editor' as const, label: t('workspace.workbench.tabs.editor', '编辑器'), - icon: 'fas fa-pen-to-square', + shortLabel: t('workspace.workbench.tabs.editor', '编辑器'), + icon: 'fas fa-pen-ruler', + hint: t('workspace.workbench.editorHint', '在工作台里直接查看并编辑当前打开的文件。'), }, ]); const activeSessionName = computed(() => { - if (!props.sessionId) { - return null; - } - + if (!props.sessionId) return null; return sessions.value.get(props.sessionId)?.connectionName ?? props.sessionId; }); +const activeWorkbenchMeta = computed(() => { + return workbenchTabs.value.find((tab) => tab.id === activeWorkbenchTab.value) ?? workbenchTabs.value[0]; +}); + const hasFileManagerContext = computed(() => { return Boolean(props.sessionId && props.instanceId && props.dbConnectionId && props.wsDeps); }); @@ -97,134 +106,235 @@ watch( diff --git a/packages/frontend/src/style.css b/packages/frontend/src/style.css index 272fd8d..0260d1e 100644 --- a/packages/frontend/src/style.css +++ b/packages/frontend/src/style.css @@ -1,23 +1,24 @@ @import "tailwindcss"; -/* Tailwind Theme Variables Mapping */ @theme inline { - /* Base Colors */ - --color-background: var(--app-bg-color); /* More generic name */ - --color-foreground: var(--text-color); /* More generic name */ - --color-app: var(--app-bg-color); /* Keep specific if needed */ - --color-text-default: var(--text-color); /* Keep specific if needed */ + --color-background: var(--app-bg-color); + --color-foreground: var(--text-color); + --color-app: var(--app-bg-color); + --color-card: var(--card-bg-color); + --color-card-foreground: var(--card-foreground-color); + --color-muted: var(--muted-bg-color); + --color-muted-foreground: var(--muted-foreground-color); + --color-text-default: var(--text-color); --color-text-secondary: var(--text-color-secondary); - --color-border: var(--border-color); /* Simplified name */ - --color-border-default: var(--border-color); /* Keep specific if needed */ + --color-text-alt: var(--text-color-tertiary); + --color-border: var(--border-color); --color-link: var(--link-color); --color-link-hover: var(--link-hover-color); - --color-link-active: var(--link-active-color); /* Also used as primary/theme color */ - --color-primary: var(--link-active-color); /* Map primary to active link color */ - --color-link-active-bg: var(--link-active-bg-color); /* Map active link background */ - --color-nav-active-bg: var(--nav-item-active-bg-color); /* Map specific nav active background */ - - /* Component Colors */ + --color-link-active: var(--link-active-color); + --color-primary: var(--primary-color); + --color-primary-dark: var(--primary-dark-color); + --color-link-active-bg: var(--link-active-bg-color); + --color-nav-active-bg: var(--nav-item-active-bg-color); --color-header: var(--header-bg-color); --color-footer: var(--footer-bg-color); --color-button: var(--button-bg-color); @@ -27,200 +28,336 @@ --color-icon-hover: var(--icon-hover-color); --color-split-line: var(--split-line-color); --color-split-line-hover: var(--split-line-hover-color); + --color-input: var(--input-bg-color); --color-input-focus-border: var(--input-focus-border-color); --color-overlay: var(--overlay-bg-color); - --color-success: var(--color-success); - --color-warning: var(--color-warning); - --color-error: var(--color-error); - --color-success-text: var(--color-success-text); - --color-warning-text: var(--color-warning-text); - --color-error-text: var(--color-error-text); + --color-success: var(--success-color); + --color-warning: var(--warning-color); + --color-error: var(--error-color); } -/* 全局样式和 CSS 变量定义 */ :root { - /* 基础颜色 */ - --app-bg-color: #ffffff; /* 应用背景色 */ - --text-color: #333333; /* 主要文字颜色 */ - --text-color-secondary: #666666; /* 次要文字颜色 */ - --border-color: #cccccc; /* 边框颜色 */ - --link-color: #333; /* 链接颜色 */ - --link-hover-color: #0056b3; /* 链接悬停颜色 */ - --link-active-color: #007bff; /* 激活链接/主题色 */ - --link-active-bg-color: #e0e0ff; /* 激活链接背景色 (类似 indigo-50) */ - --nav-item-active-bg-color: var(--link-active-bg-color); /* 导航选中项背景色, 默认同激活链接背景 */ - - /* 组件颜色 */ - --header-bg-color: #f0f0f0; /* 头部背景色 */ - --footer-bg-color: #f0f0f0; /* 底部背景色 */ - --button-bg-color: #007bff; /* 按钮背景色 */ - --button-text-color: #ffffff; /* 按钮文字颜色 */ - --button-hover-bg-color: #0056b3;/* 按钮悬停背景色 */ - --icon-color: var(--text-color-secondary); /* 图标颜色 */ - --icon-hover-color: var(--link-hover-color); /* 图标悬停颜色 */ - --split-line-color: var(--border-color); /* 分割线颜色 */ - --split-line-hover-color: var(--border-color); /* 分割线悬停颜色 */ - --input-focus-border-color: var(--link-active-color); /* 输入框聚焦边框颜色 */ - --input-focus-glow: var(--link-active-color); /* 输入框聚焦光晕值 */ - --overlay-bg-color: rgba(0, 0, 0, 0.6); /* Added Overlay Background Color */ - - /* Status Colors */ - --color-success: #28a745; /* Green */ - --color-warning: #ffc107; /* Yellow */ - --color-error: #dc3545; /* Red */ - --color-success-text: #ffffff; /* White text for green bg */ - --color-warning-text: #212529; /* Dark text for yellow bg */ - --color-error-text: #ffffff; /* White text for red bg */ - - /* 字体 */ - --font-family-sans-serif: sans-serif; /* 默认字体 */ - - /* 其他 */ - --base-padding: 1rem; /* 基础内边距 */ - --base-margin: 0.5rem; /* 基础外边距 */ + --app-bg-color: #edf2f8; + --app-bg-gradient: radial-gradient(circle at top left, rgba(84, 125, 255, 0.18), transparent 34%), + radial-gradient(circle at right 16%, rgba(0, 170, 170, 0.14), transparent 26%), + linear-gradient(180deg, #f6f8fc 0%, #ecf1f7 52%, #e7edf6 100%); + --shell-surface-color: rgba(255, 255, 255, 0.56); + --card-bg-color: rgba(255, 255, 255, 0.84); + --card-foreground-color: #142033; + --muted-bg-color: #e9eef6; + --muted-foreground-color: #5a6b84; + --text-color: #152338; + --text-color-secondary: #607089; + --text-color-tertiary: #7f8da3; + --border-color: rgba(103, 124, 155, 0.24); + --border-strong-color: rgba(103, 124, 155, 0.36); + --link-color: #355fa8; + --link-hover-color: #214d90; + --link-active-color: #3c69e7; + --primary-color: #3c69e7; + --primary-dark-color: #2746b8; + --primary-soft-color: rgba(60, 105, 231, 0.12); + --link-active-bg-color: rgba(60, 105, 231, 0.12); + --nav-item-active-bg-color: rgba(60, 105, 231, 0.12); + --header-bg-color: rgba(255, 255, 255, 0.74); + --footer-bg-color: rgba(255, 255, 255, 0.78); + --button-bg-color: #3c69e7; + --button-text-color: #ffffff; + --button-hover-bg-color: #2746b8; + --icon-color: #62748e; + --icon-hover-color: #1d4f91; + --split-line-color: rgba(126, 143, 168, 0.22); + --split-line-hover-color: rgba(60, 105, 231, 0.42); + --input-bg-color: rgba(245, 248, 252, 0.9); + --input-focus-border-color: #3c69e7; + --input-focus-glow-rgb: 60, 105, 231; + --overlay-bg-color: rgba(12, 20, 32, 0.58); + --success-color: #22a06b; + --warning-color: #d99b24; + --error-color: #d04b4b; + --success-text-color: #ffffff; + --warning-text-color: #1d1d1d; + --error-text-color: #ffffff; + --shadow-soft: 0 24px 60px rgba(31, 48, 84, 0.14); + --shadow-card: 0 18px 40px rgba(24, 38, 67, 0.1); + --shadow-inset: inset 0 1px 0 rgba(255, 255, 255, 0.72); + --grid-line-color: rgba(116, 136, 167, 0.08); + --font-family-sans-serif: "IBM Plex Sans", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif; + --font-family-display: "Space Grotesk", "IBM Plex Sans", "Noto Sans SC", "PingFang SC", sans-serif; + --font-family-mono: "IBM Plex Mono", "JetBrains Mono", "Cascadia Code", monospace; + --base-padding: 1rem; + --base-margin: 0.5rem; + --el-font-family: var(--font-family-sans-serif); + --el-color-primary: var(--primary-color); + --el-color-primary-light-3: #6789f0; + --el-color-primary-light-5: #8ca5f5; + --el-color-primary-light-7: #b3c3fa; + --el-color-primary-light-8: #cad8fc; + --el-color-primary-light-9: #e3ebff; + --el-color-primary-dark-2: var(--primary-dark-color); + --el-bg-color: rgba(255, 255, 255, 0.9); + --el-bg-color-page: transparent; + --el-bg-color-overlay: rgba(255, 255, 255, 0.96); + --el-text-color-primary: var(--text-color); + --el-text-color-regular: var(--text-color-secondary); + --el-text-color-secondary: var(--text-color-tertiary); + --el-border-color: rgba(103, 124, 155, 0.24); + --el-border-color-light: rgba(103, 124, 155, 0.16); + --el-border-color-lighter: rgba(103, 124, 155, 0.12); + --el-border-radius-base: 16px; + --el-border-radius-small: 12px; + --el-box-shadow-light: var(--shadow-card); +} + +html, +body, +#app { + min-height: 100%; } -/* 应用基础样式 */ body { - margin: 0; /* 移除默认 body margin */ + margin: 0; font-family: var(--font-family-sans-serif); - background-color: var(--app-bg-color); color: var(--text-color); - line-height: 1.6; /* 改善可读性 */ + background: var(--app-bg-gradient); + background-attachment: fixed; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(var(--grid-line-color) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-line-color) 1px, transparent 1px); + background-size: 28px 28px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.36), transparent 82%); } -/* 全局链接样式 */ a { - /* color: var(--link-color); */ /* 注释掉全局 a 标签的颜色设置,让 Tailwind 类生效 */ - text-decoration: none; /* 移除下划线 */ + color: inherit; + text-decoration: none; } -/* Removed global a:hover underline rule to avoid conflicts with Tailwind utilities */ - -/* 全局图标样式 */ -i, .fas, .far, .fab { /* 根据你使用的图标库调整选择器 */ - color: var(--icon-color); +i, +.fas, +.far, +.fab { + color: inherit; transition: color 0.2s ease; } -a:hover i, a:hover .fas, a:hover .far, a:hover .fab, /* 链接内的图标 */ -button:hover i, button:hover .fas, button:hover .far, button:hover .fab, /* 按钮内的图标 */ -.icon-interactive:hover i, .icon-interactive:hover .fas, .icon-interactive:hover .far, .icon-interactive:hover .fab { /* 可交互图标容器 */ - color: var(--icon-hover-color); + +button, +input, +textarea, +select { + font: inherit; } -/* 全局分割线样式 */ + +button:hover { + cursor: pointer; +} + +input:focus, +textarea:focus, +select:focus { + border-color: var(--input-focus-border-color) !important; + outline: 0; + box-shadow: 0 0 0 3px rgba(var(--input-focus-glow-rgb), 0.18) !important; +} + +button:focus, +button:focus-visible { + outline: none !important; +} + hr { border: none; - border-top: 1px solid var(--divider-color); + border-top: 1px solid rgba(103, 124, 155, 0.18); margin: var(--base-margin) 0; } - -/* 可以添加更多全局样式规则 */ - -/* 为 xterm 终端添加内边距 */ - -.xterm{ - padding: 10px 10px 10px 10px; - +.xterm { + padding: 10px; +} + +.control-panel { + border: 1px solid rgba(103, 124, 155, 0.18); + border-radius: 24px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(246, 249, 253, 0.8)); + box-shadow: var(--shadow-card); + backdrop-filter: blur(20px); +} + +.control-panel--muted { + background: linear-gradient(180deg, rgba(243, 247, 252, 0.82), rgba(236, 242, 248, 0.74)); +} + +.control-toolbar { + border: 1px solid rgba(103, 124, 155, 0.14); + border-radius: 18px; + background: rgba(247, 250, 253, 0.78); + box-shadow: var(--shadow-inset); +} + +.control-stat-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); +} + +.control-stat-card { + position: relative; + overflow: hidden; + border: 1px solid rgba(103, 124, 155, 0.14); + border-radius: 20px; + background: linear-gradient(180deg, rgba(250, 252, 255, 0.9), rgba(241, 246, 252, 0.78)); + box-shadow: var(--shadow-inset); + padding: 1rem 1.1rem; +} + +.control-stat-card::after { + content: ""; + position: absolute; + inset: 0 auto auto 0; + width: 100%; + height: 3px; + background: linear-gradient(90deg, rgba(60, 105, 231, 0.72), rgba(16, 185, 129, 0.48)); +} + +.control-stat-card__label { + display: block; + color: var(--text-color-tertiary); + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.control-stat-card__value { + margin-top: 0.65rem; + display: block; + color: var(--text-color); + font-family: var(--font-family-display); + font-size: 1.6rem; + font-weight: 700; + line-height: 1.1; +} + +.control-stat-card__meta { + margin-top: 0.45rem; + color: var(--text-color-secondary); + font-size: 0.85rem; +} + +.control-empty { + padding: 2.6rem 1.4rem; + border: 1px dashed rgba(103, 124, 155, 0.3); + border-radius: 20px; + background: rgba(246, 249, 253, 0.8); } -/* 为历史记录和快捷命令列表设置字体 */ -/* 注意:这里的选择器可能需要根据实际组件结构调整 */ .command-history-item, -.quick-command-item { /* 假设这些是列表项的类名 */ - font-family: var(--font-family-sans-serif); -} - -/* 如果是 Element Plus 的 Table 组件 */ +.quick-command-item, .el-table .cell { font-family: var(--font-family-sans-serif); } -/* Override splitpanes default theme pane background */ +.el-card { + border-color: rgba(103, 124, 155, 0.18); + box-shadow: var(--shadow-card); +} + +.el-card__body { + padding: 1.15rem 1.25rem; +} + +.el-input__wrapper, +.el-select__wrapper, +.el-textarea__inner { + background: rgba(245, 248, 252, 0.92); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65); +} + +.el-button { + font-weight: 600; +} + +.el-button--primary { + box-shadow: 0 12px 24px rgba(60, 105, 231, 0.2); +} + +.el-button.is-plain { + background: rgba(255, 255, 255, 0.6); +} + +.el-tabs__nav-wrap::after { + background-color: rgba(103, 124, 155, 0.14); +} + +.el-tabs__item { + font-weight: 600; +} + +.el-table { + --el-table-border-color: rgba(103, 124, 155, 0.14); + --el-table-header-bg-color: rgba(243, 247, 252, 0.88); + --el-table-tr-bg-color: transparent; + --el-table-row-hover-bg-color: rgba(60, 105, 231, 0.05); + border-radius: 18px; + overflow: hidden; +} + .splitpanes.default-theme .splitpanes__pane { - background-color: var(--app-bg-color) !important; + background-color: transparent !important; } -/* Style the splitpane splitter */ .splitpanes.default-theme .splitpanes__splitter { - background-color: var(--app-bg-color) !important; /* Use important to ensure override */ - border-left: 1px solid var(--border-color); /* Add a subtle border */ - border-right: 1px solid var(--border-color); + background-color: transparent !important; + border-left: 1px solid rgba(103, 124, 155, 0.18); + border-right: 1px solid rgba(103, 124, 155, 0.18); box-sizing: border-box; - transition: background-color 0.2s ease; /* Add transition for hover effect */ -} -.splitpanes.default-theme .splitpanes__splitter:hover { - background-color: var(--link-active-color) !important; /* Highlight on hover, keep important */ -} -.splitpanes--vertical > .splitpanes__splitter { - width: 7px; /* Adjust width as needed */ - border-top: none; - border-bottom: none; -} -.splitpanes--horizontal > .splitpanes__splitter { - height: 7px; /* Adjust height as needed */ - border-left: none; - border-right: none; - border-top: 1px solid var(--border-color); - border-bottom: 1px solid var(--border-color); + transition: background-color 0.2s ease; +} + +.splitpanes.default-theme .splitpanes__splitter:hover { + background-color: rgba(60, 105, 231, 0.16) !important; +} + +.splitpanes--vertical > .splitpanes__splitter { + width: 8px; +} + +.splitpanes--horizontal > .splitpanes__splitter { + height: 8px; + border-top: 1px solid rgba(103, 124, 155, 0.18); + border-bottom: 1px solid rgba(103, 124, 155, 0.18); } -/* Style scrollbars */ ::-webkit-scrollbar { - width: 8px; /* Width of vertical scrollbar */ - height: 8px; /* Height of horizontal scrollbar */ + width: 10px; + height: 10px; } ::-webkit-scrollbar-track { - background: var(--app-bg-color); /* Scrollbar track background */ - border-radius: 4px; + background: rgba(255, 255, 255, 0.28); + border-radius: 999px; } ::-webkit-scrollbar-thumb { - background-color: var(--border-color); /* Scrollbar handle color */ - border-radius: 4px; - border: 2px solid var(--app-bg-color); /* Creates padding around thumb */ + background-color: rgba(104, 123, 152, 0.5); + border-radius: 999px; + border: 2px solid transparent; + background-clip: padding-box; } ::-webkit-scrollbar-thumb:hover { - background-color: var(--text-color-secondary); /* Scrollbar handle hover color */ + background-color: rgba(61, 84, 118, 0.66); } -/* Input focus styles */ -input:focus, textarea:focus, select:focus { - border-color: var(--input-focus-border-color) !important; /* Use new variable, !important might be needed depending on specificity */ - outline: 0; - box-shadow: 0 0 0 3px rgba(var(--input-focus-glow-rgb), 0.2) !important; /* Use new variable, !important might be needed */ +::v-deep(.el-progress-bar__outer) { + background-color: rgba(226, 233, 244, 0.86) !important; } - -/* Ensure icons inside primary buttons are white */ -button.bg-primary i, -button.bg-primary .fas, -button.bg-primary .far, -button.bg-primary .fab { - color: white !important; /* Force white color */ -} - -/* Optional: Keep icon white even on hover for primary buttons */ -button.bg-primary:hover i, -button.bg-primary:hover .fas, -button.bg-primary:hover .far, -button.bg-primary:hover .fab { - color: white !important; /* Keep white on hover */ -} - -/* 移除按钮的聚焦光圈 */ -button:focus { - outline: none !important; - box-shadow: none !important; /* 同时移除 box-shadow 以防其被用于聚焦指示 */ -} - -/* 针对使用 :focus-visible 的浏览器 */ -button:focus-visible { - outline: none !important; - box-shadow: none !important; -} -/* 当鼠标悬停在按钮上时,鼠标指针变为手型 */ -button:hover { - cursor: pointer; -} \ No newline at end of file diff --git a/packages/frontend/src/views/AuditLogView.vue b/packages/frontend/src/views/AuditLogView.vue index 2c9dd40..91b6c9d 100644 --- a/packages/frontend/src/views/AuditLogView.vue +++ b/packages/frontend/src/views/AuditLogView.vue @@ -1,228 +1,203 @@ - - - + diff --git a/packages/frontend/src/views/ConnectionsView.vue b/packages/frontend/src/views/ConnectionsView.vue index 566ee4f..119da4d 100644 --- a/packages/frontend/src/views/ConnectionsView.vue +++ b/packages/frontend/src/views/ConnectionsView.vue @@ -746,4 +746,4 @@ const handleConnectAllFilteredConnections = async () => { @saved="handleBatchEditSaved" /> - \ No newline at end of file + diff --git a/packages/frontend/src/views/DashboardView.vue b/packages/frontend/src/views/DashboardView.vue index dc83446..ce8e938 100644 --- a/packages/frontend/src/views/DashboardView.vue +++ b/packages/frontend/src/views/DashboardView.vue @@ -1,57 +1,51 @@ diff --git a/packages/frontend/src/views/LoginView.vue b/packages/frontend/src/views/LoginView.vue index 845e817..326b177 100644 --- a/packages/frontend/src/views/LoginView.vue +++ b/packages/frontend/src/views/LoginView.vue @@ -1,115 +1,86 @@ + diff --git a/packages/frontend/src/views/NotificationsView.vue b/packages/frontend/src/views/NotificationsView.vue index 8276723..9489482 100644 --- a/packages/frontend/src/views/NotificationsView.vue +++ b/packages/frontend/src/views/NotificationsView.vue @@ -1,13 +1,15 @@ - - - + diff --git a/packages/frontend/src/views/ProxiesView.vue b/packages/frontend/src/views/ProxiesView.vue index f3205ab..eea4c89 100644 --- a/packages/frontend/src/views/ProxiesView.vue +++ b/packages/frontend/src/views/ProxiesView.vue @@ -2,8 +2,9 @@ import { ref, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; import { useProxiesStore, ProxyInfo } from '../stores/proxies.store'; +import PageShell from '../components/PageShell.vue'; import ProxyList from '../components/ProxyList.vue'; -import AddProxyForm from '../components/AddProxyForm.vue'; +import AddProxyForm from '../components/AddProxyForm.vue'; const { t } = useI18n(); const proxiesStore = useProxiesStore(); @@ -11,7 +12,6 @@ const proxiesStore = useProxiesStore(); const showForm = ref(false); const editingProxy = ref(null); -// 组件挂载时获取代理列表 onMounted(() => { proxiesStore.fetchProxies(); }); @@ -42,21 +42,18 @@ const closeForm = () => { - - diff --git a/packages/frontend/src/views/SettingsView.vue b/packages/frontend/src/views/SettingsView.vue index dc221e2..251c378 100644 --- a/packages/frontend/src/views/SettingsView.vue +++ b/packages/frontend/src/views/SettingsView.vue @@ -1,98 +1,10 @@ - - - + + + diff --git a/packages/frontend/src/views/SetupView.vue b/packages/frontend/src/views/SetupView.vue index 22a5d99..3999f47 100644 --- a/packages/frontend/src/views/SetupView.vue +++ b/packages/frontend/src/views/SetupView.vue @@ -1,98 +1,14 @@ - - - \ No newline at end of file +