diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index 98a28f4..dd921c0 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -4,6 +4,5 @@ - 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 442cb28..366ce5e 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -45,9 +45,3 @@ 依赖: 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 deleted file mode 100644 index 9aca62b..0000000 --- a/.helloagents/plan/202603250419_frontend-slate-control-center/.status.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index 0d010b9..0000000 --- a/.helloagents/plan/202603250419_frontend-slate-control-center/proposal.md +++ /dev/null @@ -1,159 +0,0 @@ -# 变更提案: 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 deleted file mode 100644 index be89f9b..0000000 --- a/.helloagents/plan/202603250419_frontend-slate-control-center/tasks.md +++ /dev/null @@ -1,60 +0,0 @@ -# 任务清单: 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 379a18c..234a836 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -25,230 +25,294 @@ const authStore = useAuthStore(); const settingsStore = useSettingsStore(); const appearanceStore = useAppearanceStore(); const layoutStore = useLayoutStore(); -const focusSwitcherStore = useFocusSwitcherStore(); -const sessionStore = useSessionStore(); -const dialogStore = useDialogStore(); -const { state: dialogState } = storeToRefs(dialogStore); -const favoritePathsStore = useFavoritePathsStore(); +const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++ +const sessionStore = useSessionStore(); // +++ 实例化 Session Store +++ +const dialogStore = useDialogStore(); // +++ 实例化 DialogStore +++ +const { state: dialogState } = storeToRefs(dialogStore); +const favoritePathsStore = useFavoritePathsStore(); // +++ 实例化 favoritePathsStore +++ const { isAuthenticated } = storeToRefs(authStore); const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); const { isStyleCustomizerVisible } = storeToRefs(appearanceStore); -const { isHeaderVisible } = storeToRefs(layoutStore); +const { isLayoutVisible, isHeaderVisible } = storeToRefs(layoutStore); // 添加 isHeaderVisible const { isConfiguratorVisible: isFocusSwitcherVisible } = storeToRefs(focusSwitcherStore); -const { isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } = storeToRefs(sessionStore); +const { isRdpModalOpen, rdpConnectionInfo, isVncModalOpen, vncConnectionInfo } = storeToRefs(sessionStore); // +++ 获取 RDP 和 VNC 状态 +++ const { isMobile } = useDeviceDetection(); const route = useRoute(); const navRef = ref(null); const underlineRef = ref(null); +// +++ 存储上一次由切换器聚焦的 ID +++ const lastFocusedIdBySwitcher = ref(null); -const isAltPressed = ref(false); +const isAltPressed = ref(false); // 跟踪 Alt 键是否按下 const altShortcutKey = ref(null); +// --- 移除 shortcutTriggeredInKeyDown 标志 --- const updateUnderline = async () => { - await nextTick(); + await nextTick(); // 等待 DOM 更新 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.opacity = '1'; + // underlineRef.value.style.top = `${activeLink.offsetTop + activeLink.offsetHeight + offsetBottom}px`; // 移除 top 设置 + underlineRef.value.style.opacity = '1'; // Make it visible } else { - underlineRef.value.style.opacity = '0'; + underlineRef.value.style.opacity = '0'; // Hide if no active link (e.g., on login page if not a nav link) } } }; onMounted(() => { + // Initial position update + // Use setTimeout to ensure styles are applied and elements have dimensions setTimeout(updateUnderline, 100); - window.addEventListener('keydown', handleAltKeyDown); - window.addEventListener('keyup', handleGlobalKeyUp); - - window.addEventListener('beforeinstallprompt', () => { + // +++ 全局 Alt 键监听器 +++ + window.addEventListener('keydown', handleAltKeyDown); // +++ 监听 keydown 设置状态 +++ + window.addEventListener('keyup', handleGlobalKeyUp); // +++ 监听 keyup 执行切换 +++ + + // PWA Install Prompt + window.addEventListener('beforeinstallprompt', (e) => { 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); - window.removeEventListener('keyup', handleGlobalKeyUp); + window.removeEventListener('keydown', handleAltKeyDown); // +++ 移除 keydown 监听 +++ + window.removeEventListener('keyup', handleGlobalKeyUp); // +++ 移除 keyup 监听 +++ }); + +// *** 计算属性,判断是否在 workspace 路由 *** const isWorkspaceRoute = computed(() => route.path === '/workspace'); -watch( - route, - () => { - updateUnderline(); - }, - { immediate: true } -); +watch(route, () => { + updateUnderline(); +}, { immediate: true }); // *** 确保 immediate: true 存在 *** + const handleLogout = () => { authStore.logout(); }; +// 打开样式自定义器的方法现在直接调用 store action const openStyleCustomizer = () => { appearanceStore.toggleStyleCustomizer(true); }; +// 关闭样式自定义器的方法现在也调用 store action const closeStyleCustomizer = () => { appearanceStore.toggleStyleCustomizer(false); }; -const handleAltKeyDown = async (event: KeyboardEvent) => { - if (!isWorkspaceRoute.value) return; - +// +++ 处理 Alt 键按下的事件处理函数,并记录快捷键 +++ +const handleAltKeyDown = async (event: KeyboardEvent) => { // +++ 改为 async +++ + if (!isWorkspaceRoute.value) return; // 只在 workspace 路由下执行 + // 只在 Alt 键首次按下时设置状态 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}`; - const targetId = focusSwitcherStore.getFocusTargetIdByShortcut(shortcutString); + altShortcutKey.value = key; // 记录按键 + const shortcutString = `Alt+${key}`; + console.log(`[App] KeyDown: Alt+${key} detected. Checking shortcut: ${shortcutString}`); + const targetId = focusSwitcherStore.getFocusTargetIdByShortcut(shortcutString); - if (targetId) { - event.preventDefault(); - const success = await focusSwitcherStore.focusTarget(targetId); - if (success) { - lastFocusedIdBySwitcher.value = targetId; + 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; } - } } else { - isAltPressed.value = false; - altShortcutKey.value = null; + // 按下无效键 (非字母数字),取消 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.'); + } +}; + +// +++ 全局键盘事件处理函数,监听 keyup,优先处理快捷键 +++ +const handleGlobalKeyUp = async (event: KeyboardEvent) => { + if (!isWorkspaceRoute.value) return; // 只在 workspace 路由下执行 + if (event.key === 'Alt') { + const altWasPressed = isAltPressed.value; + const triggeredShortcutKey = altShortcutKey.value; // 记录松开时是否有记录的快捷键 + + // 总是重置状态 isAltPressed.value = false; altShortcutKey.value = null; + // --- 移除重置标志位 --- + + if (altWasPressed && triggeredShortcutKey === null) { + // 如果 Alt 之前是按下的,并且没有记录到有效的快捷键,则执行顺序切换 + console.log('[App] KeyUp: Alt released without a valid shortcut key captured. Attempting sequential focus switch.'); + event.preventDefault(); // 仅在执行顺序切换时阻止默认行为 + + // --- 顺序切换逻辑 (保持不变) --- + let currentFocusId: string | null = lastFocusedIdBySwitcher.value; + console.log(`[App] Sequential switch. Last focused by switcher: ${currentFocusId}`); + + 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.`); + } + } + + const order = focusSwitcherStore.sequenceOrder; // ++ 使用新的 sequenceOrder state ++ + if (order.length === 0) { // ++ 检查新的 state ++ + console.log('[App] No focus sequence configured.'); + return; + } + + 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; + } + + 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.'); + } } }; -const handleGlobalKeyUp = async (event: KeyboardEvent) => { - if (!isWorkspaceRoute.value) return; - if (event.key !== 'Alt') return; - - const altWasPressed = isAltPressed.value; - const triggeredShortcutKey = altShortcutKey.value; - - isAltPressed.value = false; - altShortcutKey.value = null; - - if (altWasPressed && triggeredShortcutKey === null) { - event.preventDefault(); - - 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 success = await focusSwitcherStore.focusTarget(nextFocusId); - if (success) { - lastFocusedIdBySwitcher.value = nextFocusId; - focused = true; - break; - } - - currentFocusId = nextFocusId; - } - - 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; }; + + + @@ -288,197 +371,13 @@ const handleGlobalKeyUp = async (event: KeyboardEvent) => { display: flex; flex-direction: column; min-height: 100vh; - position: relative; - font-family: var(--font-family-sans-serif); + 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%); -} -.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 { +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 deleted file mode 100644 index 1737a38..0000000 --- a/packages/frontend/src/components/AuthPanelLayout.vue +++ /dev/null @@ -1,181 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/PageShell.vue b/packages/frontend/src/components/PageShell.vue deleted file mode 100644 index ecaf1db..0000000 --- a/packages/frontend/src/components/PageShell.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - - - diff --git a/packages/frontend/src/components/StatusMonitor.vue b/packages/frontend/src/components/StatusMonitor.vue index 4367017..4977b89 100644 --- a/packages/frontend/src/components/StatusMonitor.vue +++ b/packages/frontend/src/components/StatusMonitor.vue @@ -1,34 +1,204 @@ + + + - - diff --git a/packages/frontend/src/components/Terminal.vue b/packages/frontend/src/components/Terminal.vue index 4e2f244..c2d0b44 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: text !important; + cursor: default !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 5c7aa8c..abbe199 100644 --- a/packages/frontend/src/components/WorkspaceWorkbench.vue +++ b/packages/frontend/src/components/WorkspaceWorkbench.vue @@ -49,40 +49,31 @@ 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', '文件'), - shortLabel: t('workspace.workbench.tabs.files', '文件'), - icon: 'fas fa-folder-tree', - hint: t('workspace.workbench.filesHint', '浏览远程目录、拖放文件与操作资源。'), + icon: 'fas fa-folder-open', }, { id: 'history' as const, label: t('workspace.workbench.tabs.history', '历史命令'), - shortLabel: t('workspace.workbench.tabs.history', '历史命令'), - icon: 'fas fa-clock-rotate-left', - hint: t('workspace.workbench.historyHint', '回放最近命令并快速重发到当前会话。'), + icon: 'fas fa-history', }, { id: 'editor' as const, label: t('workspace.workbench.tabs.editor', '编辑器'), - shortLabel: t('workspace.workbench.tabs.editor', '编辑器'), - icon: 'fas fa-pen-ruler', - hint: t('workspace.workbench.editorHint', '在工作台里直接查看并编辑当前打开的文件。'), + icon: 'fas fa-pen-to-square', }, ]); const activeSessionName = computed(() => { - if (!props.sessionId) return null; - return sessions.value.get(props.sessionId)?.connectionName ?? props.sessionId; -}); + if (!props.sessionId) { + return null; + } -const activeWorkbenchMeta = computed(() => { - return workbenchTabs.value.find((tab) => tab.id === activeWorkbenchTab.value) ?? workbenchTabs.value[0]; + return sessions.value.get(props.sessionId)?.connectionName ?? props.sessionId; }); const hasFileManagerContext = computed(() => { @@ -106,235 +97,134 @@ watch( diff --git a/packages/frontend/src/style.css b/packages/frontend/src/style.css index 0260d1e..272fd8d 100644 --- a/packages/frontend/src/style.css +++ b/packages/frontend/src/style.css @@ -1,24 +1,23 @@ @import "tailwindcss"; +/* Tailwind Theme Variables Mapping */ @theme inline { - --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); + /* 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-text-secondary: var(--text-color-secondary); - --color-text-alt: var(--text-color-tertiary); - --color-border: var(--border-color); + --color-border: var(--border-color); /* Simplified name */ + --color-border-default: var(--border-color); /* Keep specific if needed */ --color-link: var(--link-color); --color-link-hover: var(--link-hover-color); - --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-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-header: var(--header-bg-color); --color-footer: var(--footer-bg-color); --color-button: var(--button-bg-color); @@ -28,336 +27,200 @@ --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(--success-color); - --color-warning: var(--warning-color); - --color-error: var(--error-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); } +/* 全局样式和 CSS 变量定义 */ :root { - --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%; + /* 基础颜色 */ + --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; /* 基础外边距 */ } +/* 应用基础样式 */ body { - margin: 0; + margin: 0; /* 移除默认 body margin */ font-family: var(--font-family-sans-serif); + background-color: var(--app-bg-color); color: var(--text-color); - 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%); + line-height: 1.6; /* 改善可读性 */ } +/* 全局链接样式 */ a { - color: inherit; - text-decoration: none; + /* color: var(--link-color); */ /* 注释掉全局 a 标签的颜色设置,让 Tailwind 类生效 */ + text-decoration: none; /* 移除下划线 */ } -i, -.fas, -.far, -.fab { - color: inherit; +/* Removed global a:hover underline rule to avoid conflicts with Tailwind utilities */ + +/* 全局图标样式 */ +i, .fas, .far, .fab { /* 根据你使用的图标库调整选择器 */ + color: var(--icon-color); transition: color 0.2s ease; } - -button, -input, -textarea, -select { - font: inherit; +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: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 rgba(103, 124, 155, 0.18); + border-top: 1px solid var(--divider-color); margin: var(--base-margin) 0; } -.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); + +/* 可以添加更多全局样式规则 */ + +/* 为 xterm 终端添加内边距 */ + +.xterm{ + padding: 10px 10px 10px 10px; + } +/* 为历史记录和快捷命令列表设置字体 */ +/* 注意:这里的选择器可能需要根据实际组件结构调整 */ .command-history-item, -.quick-command-item, +.quick-command-item { /* 假设这些是列表项的类名 */ + font-family: var(--font-family-sans-serif); +} + +/* 如果是 Element Plus 的 Table 组件 */ .el-table .cell { font-family: var(--font-family-sans-serif); } -.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; -} - +/* Override splitpanes default theme pane background */ .splitpanes.default-theme .splitpanes__pane { - background-color: transparent !important; + background-color: var(--app-bg-color) !important; } +/* Style the splitpane splitter */ .splitpanes.default-theme .splitpanes__splitter { - background-color: transparent !important; - border-left: 1px solid rgba(103, 124, 155, 0.18); - border-right: 1px solid rgba(103, 124, 155, 0.18); + 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); box-sizing: border-box; - transition: background-color 0.2s ease; + transition: background-color 0.2s ease; /* Add transition for hover effect */ } - .splitpanes.default-theme .splitpanes__splitter:hover { - background-color: rgba(60, 105, 231, 0.16) !important; + background-color: var(--link-active-color) !important; /* Highlight on hover, keep important */ } - .splitpanes--vertical > .splitpanes__splitter { - width: 8px; + width: 7px; /* Adjust width as needed */ + border-top: none; + border-bottom: none; } - .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); + 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); } +/* Style scrollbars */ ::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 8px; /* Width of vertical scrollbar */ + height: 8px; /* Height of horizontal scrollbar */ } ::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.28); - border-radius: 999px; + background: var(--app-bg-color); /* Scrollbar track background */ + border-radius: 4px; } ::-webkit-scrollbar-thumb { - background-color: rgba(104, 123, 152, 0.5); - border-radius: 999px; - border: 2px solid transparent; - background-clip: padding-box; + background-color: var(--border-color); /* Scrollbar handle color */ + border-radius: 4px; + border: 2px solid var(--app-bg-color); /* Creates padding around thumb */ } ::-webkit-scrollbar-thumb:hover { - background-color: rgba(61, 84, 118, 0.66); + background-color: var(--text-color-secondary); /* Scrollbar handle hover color */ } -::v-deep(.el-progress-bar__outer) { - background-color: rgba(226, 233, 244, 0.86) !important; +/* 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 */ } + +/* 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 91b6c9d..2c9dd40 100644 --- a/packages/frontend/src/views/AuditLogView.vue +++ b/packages/frontend/src/views/AuditLogView.vue @@ -1,203 +1,228 @@ + + - + diff --git a/packages/frontend/src/views/ConnectionsView.vue b/packages/frontend/src/views/ConnectionsView.vue index 119da4d..566ee4f 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 ce8e938..dc83446 100644 --- a/packages/frontend/src/views/DashboardView.vue +++ b/packages/frontend/src/views/DashboardView.vue @@ -1,51 +1,57 @@ diff --git a/packages/frontend/src/views/LoginView.vue b/packages/frontend/src/views/LoginView.vue index 326b177..845e817 100644 --- a/packages/frontend/src/views/LoginView.vue +++ b/packages/frontend/src/views/LoginView.vue @@ -1,86 +1,115 @@ - diff --git a/packages/frontend/src/views/NotificationsView.vue b/packages/frontend/src/views/NotificationsView.vue index 9489482..8276723 100644 --- a/packages/frontend/src/views/NotificationsView.vue +++ b/packages/frontend/src/views/NotificationsView.vue @@ -1,15 +1,13 @@ + + - + diff --git a/packages/frontend/src/views/ProxiesView.vue b/packages/frontend/src/views/ProxiesView.vue index eea4c89..f3205ab 100644 --- a/packages/frontend/src/views/ProxiesView.vue +++ b/packages/frontend/src/views/ProxiesView.vue @@ -2,9 +2,8 @@ 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(); @@ -12,6 +11,7 @@ const proxiesStore = useProxiesStore(); const showForm = ref(false); const editingProxy = ref(null); +// 组件挂载时获取代理列表 onMounted(() => { proxiesStore.fetchProxies(); }); @@ -42,18 +42,21 @@ const closeForm = () => { + + diff --git a/packages/frontend/src/views/SettingsView.vue b/packages/frontend/src/views/SettingsView.vue index 251c378..dc221e2 100644 --- a/packages/frontend/src/views/SettingsView.vue +++ b/packages/frontend/src/views/SettingsView.vue @@ -1,10 +1,98 @@ + + - - + diff --git a/packages/frontend/src/views/SetupView.vue b/packages/frontend/src/views/SetupView.vue index 3999f47..22a5d99 100644 --- a/packages/frontend/src/views/SetupView.vue +++ b/packages/frontend/src/views/SetupView.vue @@ -1,14 +1,98 @@ + + - + \ No newline at end of file