fix(workspace): refine workbench defaults and cursor

Add Quick Commands as the default Workbench tab and update
the default three-column layout proportions to better match
the intended workspace design.

Also fix terminal hover cursor behavior and add locale and
docs updates for the new default interaction.
This commit is contained in:
yinjianm
2026-03-25 04:18:26 +08:00
parent f2f9c754f8
commit 10df92ffa3
9 changed files with 114 additions and 10 deletions
+1
View File
@@ -5,3 +5,4 @@
- 2026-03-25:初始化 `.helloagents/` 知识库骨架与首批模块文档,不代表源码功能变更。 - 2026-03-25:初始化 `.helloagents/` 知识库骨架与首批模块文档,不代表源码功能变更。
- 2026-03-25:新增 GHCR Docker 发布 workflow,并将 `docker-compose.yml` 的三个业务镜像切换到 `ghcr.io/micah123321/*` - 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 参考图,并修复终端区域鼠标悬停时指针异常消失的问题。
@@ -48,6 +48,7 @@
| 2026-03-25 12:20 | 3.1 / 3.2 | 完成 | 后端透出累计上下行字节数,前端状态监控新增“开机累计流量”展示 | | 2026-03-25 12:20 | 3.1 / 3.2 | 完成 | 后端透出累计上下行字节数,前端状态监控新增“开机累计流量”展示 |
| 2026-03-25 12:31 | 4.1 | 完成 | 执行 `npm install` 后,前后端 build 均通过 | | 2026-03-25 12:31 | 4.1 | 完成 | 执行 `npm install` 后,前后端 build 均通过 |
| 2026-03-25 12:33 | 4.2 | 完成 | 更新模块文档、变更日志并准备归档 | | 2026-03-25 12:33 | 4.2 | 完成 | 更新模块文档、变更日志并准备归档 |
| 2026-03-25 12:48 | follow-up | 完成 | Workbench 新增默认快捷指令 tab,微调三栏宽度并修复终端鼠标悬停指针异常 |
--- ---
+2 -2
View File
@@ -36,8 +36,8 @@
### 工作区交互 ### 工作区交互
**条件**: 用户进入 `/workspace` 或相关管理页面。 **条件**: 用户进入 `/workspace` 或相关管理页面。
**行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 以 tab 容器整合命令历史、文件管理和编辑器。 **行为**: 通过组件、Pinia 与 composable 协同管理终端、文件管理、命令历史、布局配置、主题和状态监控;当前 `/workspace` 默认主布局为“左侧 Workbench、中央终端、右侧状态监控”,其中 Workbench 以 tab 容器整合快捷指令、命令历史、文件管理和编辑器,默认激活快捷指令
**结果**: 页面逻辑分散在 `views/``components/``stores/``composables/`,其中布局调优先落在 `layout.store.ts``LayoutRenderer.vue``WorkspaceWorkbench.vue` **结果**: 页面逻辑分散在 `views/``components/``stores/``composables/`,其中布局与交互微调优先落在 `layout.store.ts``LayoutRenderer.vue``WorkspaceWorkbench.vue``Terminal.vue`
## 依赖关系 ## 依赖关系
@@ -739,6 +739,17 @@ watchEffect(() => {
/* z-index 调整或移除,因为背景层不再在此组件内 */ /* z-index 调整或移除,因为背景层不再在此组件内 */
} }
.terminal-inner-container,
.terminal-inner-container :deep(.xterm),
.terminal-inner-container :deep(.xterm-screen),
.terminal-inner-container :deep(.xterm-viewport) {
cursor: default !important;
}
.terminal-inner-container :deep(.xterm .xterm-cursor-pointer) {
cursor: pointer !important;
}
/* 文字描边和阴影样式 */ /* 文字描边和阴影样式 */
.terminal-inner-container.has-text-stroke :deep(.xterm-rows span), .terminal-inner-container.has-text-stroke :deep(.xterm-rows span),
.terminal-inner-container.has-text-stroke :deep(.xterm-rows div > span), /* 更具体地针对嵌套 span */ .terminal-inner-container.has-text-stroke :deep(.xterm-rows div > span), /* 更具体地针对嵌套 span */
@@ -3,13 +3,14 @@ import { computed, ref, watch, type PropType } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import CommandHistoryView from '../views/CommandHistoryView.vue'; import CommandHistoryView from '../views/CommandHistoryView.vue';
import QuickCommandsView from '../views/QuickCommandsView.vue';
import FileManager from './FileManager.vue'; import FileManager from './FileManager.vue';
import FileEditorContainer from './FileEditorContainer.vue'; import FileEditorContainer from './FileEditorContainer.vue';
import { useSessionStore } from '../stores/session.store'; import { useSessionStore } from '../stores/session.store';
import type { FileTab } from '../stores/fileEditor.store'; import type { FileTab } from '../stores/fileEditor.store';
import type { WebSocketDependencies } from '../composables/useSftpActions'; import type { WebSocketDependencies } from '../composables/useSftpActions';
type WorkbenchTab = 'files' | 'history' | 'editor'; type WorkbenchTab = 'quickCommands' | 'files' | 'history' | 'editor';
const props = defineProps({ const props = defineProps({
tabs: { tabs: {
@@ -42,9 +43,14 @@ const { t } = useI18n();
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
const { sessions } = storeToRefs(sessionStore); const { sessions } = storeToRefs(sessionStore);
const activeWorkbenchTab = ref<WorkbenchTab>('files'); const activeWorkbenchTab = ref<WorkbenchTab>('quickCommands');
const workbenchTabs = computed(() => [ const workbenchTabs = computed(() => [
{
id: 'quickCommands' as const,
label: t('workspace.workbench.tabs.quickCommands', '快捷指令'),
icon: 'fas fa-bolt',
},
{ {
id: 'files' as const, id: 'files' as const,
label: t('workspace.workbench.tabs.files', '文件'), label: t('workspace.workbench.tabs.files', '文件'),
@@ -106,7 +112,7 @@ watch(
{{ t('workspace.workbench.label', '工作台') }} {{ t('workspace.workbench.label', '工作台') }}
</span> </span>
</div> </div>
<div class="mt-3 grid grid-cols-3 gap-2"> <div class="mt-3 grid grid-cols-2 gap-2 xl:grid-cols-4">
<button <button
v-for="tab in workbenchTabs" v-for="tab in workbenchTabs"
:key="tab.id" :key="tab.id"
@@ -126,6 +132,10 @@ watch(
</div> </div>
<div class="relative flex-1 min-h-0 overflow-hidden bg-background"> <div class="relative flex-1 min-h-0 overflow-hidden bg-background">
<div v-show="activeWorkbenchTab === 'quickCommands'" class="absolute inset-0 min-h-0 workbench-quick-commands">
<QuickCommandsView />
</div>
<div v-show="activeWorkbenchTab === 'files'" class="absolute inset-0 min-h-0"> <div v-show="activeWorkbenchTab === 'files'" class="absolute inset-0 min-h-0">
<FileManager <FileManager
v-if="hasFileManagerContext" v-if="hasFileManagerContext"
@@ -163,3 +173,58 @@ watch(
</div> </div>
</div> </div>
</template> </template>
<style scoped>
.workbench-quick-commands {
background:
linear-gradient(180deg, rgba(15, 17, 22, 0.98) 0%, rgba(12, 14, 18, 1) 100%);
}
.workbench-quick-commands :deep(> div),
.workbench-quick-commands :deep(> div > div) {
background: transparent;
}
.workbench-quick-commands :deep(input) {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
color: #f5f7fa;
box-shadow: none;
}
.workbench-quick-commands :deep(input::placeholder) {
color: rgba(226, 232, 240, 0.55);
}
.workbench-quick-commands :deep(button) {
box-shadow: none;
}
.workbench-quick-commands :deep([data-command-id]) {
position: relative;
border-radius: 10px;
color: #f8fafc;
}
.workbench-quick-commands :deep([data-command-id]::before) {
content: '';
position: absolute;
left: 0.2rem;
top: 0.2rem;
bottom: 0.2rem;
width: 1px;
background: rgba(255, 255, 255, 0.08);
}
.workbench-quick-commands :deep([data-command-id]:hover) {
background: rgba(139, 92, 246, 0.14);
}
.workbench-quick-commands :deep([data-command-id].bg-primary\/20) {
background: linear-gradient(90deg, rgba(139, 92, 246, 0.3), rgba(139, 92, 246, 0.18));
}
.workbench-quick-commands :deep(.font-semibold.flex.items-center) {
color: #f8fafc;
}
</style>
+1
View File
@@ -1281,6 +1281,7 @@
"noSession": "No active session", "noSession": "No active session",
"fileManagerHint": "Activate an SSH session to browse remote files.", "fileManagerHint": "Activate an SSH session to browse remote files.",
"tabs": { "tabs": {
"quickCommands": "Quick Commands",
"files": "Files", "files": "Files",
"history": "History", "history": "History",
"editor": "Editor" "editor": "Editor"
+1
View File
@@ -573,6 +573,7 @@
"noSession": "アクティブなセッションはありません", "noSession": "アクティブなセッションはありません",
"fileManagerHint": "SSH セッションを有効にするとリモートファイルを参照できます。", "fileManagerHint": "SSH セッションを有効にするとリモートファイルを参照できます。",
"tabs": { "tabs": {
"quickCommands": "クイックコマンド",
"files": "ファイル", "files": "ファイル",
"history": "履歴", "history": "履歴",
"editor": "エディター" "editor": "エディター"
+1
View File
@@ -1285,6 +1285,7 @@
"noSession": "未激活会话", "noSession": "未激活会话",
"fileManagerHint": "激活一个 SSH 会话后即可浏览远程文件。", "fileManagerHint": "激活一个 SSH 会话后即可浏览远程文件。",
"tabs": { "tabs": {
"quickCommands": "快捷指令",
"files": "文件", "files": "文件",
"history": "历史命令", "history": "历史命令",
"editor": "编辑器" "editor": "编辑器"
+28 -5
View File
@@ -56,6 +56,29 @@ function isLegacyDefaultLayout(node: LayoutNode | null): boolean {
); );
} }
function isLegacyWorkbenchLayout(node: LayoutNode | null): boolean {
if (!node || node.type !== 'container' || node.direction !== 'horizontal' || !node.children || node.children.length !== 3) {
return false;
}
const [leftColumn, centerColumn, rightColumn] = node.children;
return Boolean(
isPaneNode(leftColumn, 'workbench') &&
leftColumn?.size === 23 &&
centerColumn?.type === 'container' &&
centerColumn.direction === 'vertical' &&
centerColumn.size === 57 &&
centerColumn.children?.length === 2 &&
isPaneNode(centerColumn.children[0], 'terminal') &&
centerColumn.children[0]?.size === 94 &&
isPaneNode(centerColumn.children[1], 'commandBar') &&
centerColumn.children[1]?.size === 6 &&
isPaneNode(rightColumn, 'statusMonitor') &&
rightColumn?.size === 20
);
}
// 定义默认布局结构 // 定义默认布局结构
const getDefaultLayout = (): LayoutNode => ({ const getDefaultLayout = (): LayoutNode => ({
id: generateId(), // Generate new ID id: generateId(), // Generate new ID
@@ -66,13 +89,13 @@ const getDefaultLayout = (): LayoutNode => ({
id: generateId(), // Generate new ID id: generateId(), // Generate new ID
type: "pane", type: "pane",
component: "workbench", component: "workbench",
size: 23 size: 17
}, },
{ {
id: generateId(), // Generate new ID id: generateId(), // Generate new ID
type: "container", type: "container",
direction: "vertical", direction: "vertical",
size: 57, size: 66,
children: [ children: [
{ {
id: generateId(), // Generate new ID id: generateId(), // Generate new ID
@@ -92,7 +115,7 @@ const getDefaultLayout = (): LayoutNode => ({
id: generateId(), // Generate new ID id: generateId(), // Generate new ID
type: "pane", type: "pane",
component: "statusMonitor", component: "statusMonitor",
size: 20 size: 17
} }
] ]
}); });
@@ -202,8 +225,8 @@ function normalizeLoadedLayout(node: LayoutNode | null): LayoutNode | null {
const layoutWithIds = ensureNodeIds(node); const layoutWithIds = ensureNodeIds(node);
if (!layoutWithIds) return null; if (!layoutWithIds) return null;
if (isLegacyDefaultLayout(layoutWithIds)) { if (isLegacyDefaultLayout(layoutWithIds) || isLegacyWorkbenchLayout(layoutWithIds)) {
console.log('[Layout Store] Detected legacy workspace default layout, migrating to workbench layout.'); console.log('[Layout Store] Detected legacy workspace default layout, migrating to the latest workbench layout.');
return ensureNodeIds(getDefaultLayout()); return ensureNodeIds(getDefaultLayout());
} }