fix(terminal): 右下角快捷键与右键菜单中文化并放大
快捷键区改为中文大按钮;工具栏字号/回滚/全屏中文化;右键菜单加大并在命令栏拦截英文系统菜单。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,11 @@
|
||||
- 前端:重新 `vite build` 并部署 `web/app/`。
|
||||
- 后端:拉取代码后 `supervisorctl restart nexus`(reorder 端点)。
|
||||
|
||||
## 后续(同日)
|
||||
|
||||
- 终端右下角「快捷键」区改为中文大按钮(中断/EOF/补全);工具栏字号/回滚/全屏中文化。
|
||||
- 右键菜单加大(comfortable 密度)、靠近底部时向上弹出;命令栏右键同样显示中文菜单。
|
||||
|
||||
## 验证
|
||||
|
||||
1. 打开 `https://api.synaglobal.vip/app/#/terminal`:左侧仍为全局运维菜单,无第二列服务器列表。
|
||||
|
||||
@@ -4,25 +4,37 @@
|
||||
class="pa-0 d-flex flex-column terminal-root"
|
||||
:class="theme.global.current.value.dark ? 'terminal-root--dark' : 'terminal-root--light'"
|
||||
>
|
||||
<!-- ── Context Menu ── -->
|
||||
<v-menu v-model="showTermMenu" :target="[menuX, menuY]" location="bottom start">
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item prepend-icon="mdi-content-copy" @click="termCopy">
|
||||
<!-- ── 终端/命令栏右键菜单(中文、加大,避免浏览器英文菜单) ── -->
|
||||
<v-menu
|
||||
v-model="showTermMenu"
|
||||
:target="[menuX, menuY]"
|
||||
:location="termMenuLocation"
|
||||
content-class="terminal-ctx-menu"
|
||||
>
|
||||
<v-list density="comfortable" nav class="py-1">
|
||||
<v-list-item prepend-icon="mdi-content-copy" rounded="lg" @click="termCopy">
|
||||
<v-list-item-title>复制</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item prepend-icon="mdi-content-paste" @click="termPaste">
|
||||
<v-list-item prepend-icon="mdi-content-paste" rounded="lg" @click="termPaste">
|
||||
<v-list-item-title>粘贴</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item prepend-icon="mdi-select-all" @click="termSelectAll">
|
||||
<v-list-item prepend-icon="mdi-select-all" rounded="lg" @click="termSelectAll">
|
||||
<v-list-item-title>全选</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item prepend-icon="mdi-delete-sweep" @click="termClear">
|
||||
<v-list-item-title>清屏</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item prepend-icon="mdi-logout" @click="termDisconnect" :disabled="!activeSession">
|
||||
<v-list-item-title class="text-error">断开连接</v-list-item-title>
|
||||
</v-list-item>
|
||||
<template v-if="termMenuContext === 'terminal'">
|
||||
<v-divider class="my-1" />
|
||||
<v-list-item prepend-icon="mdi-delete-sweep" rounded="lg" @click="termClear">
|
||||
<v-list-item-title>清屏</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-logout"
|
||||
rounded="lg"
|
||||
:disabled="!activeSession"
|
||||
@click="termDisconnect"
|
||||
>
|
||||
<v-list-item-title class="text-error">断开连接</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
@@ -77,23 +89,27 @@
|
||||
<span v-if="activeSession?.uptime" class="text-caption text-medium-emphasis">{{ activeSession.uptime }}</span>
|
||||
<span v-if="activeSession?.pingMs !== null && activeSession?.pingMs !== undefined" class="text-caption text-medium-emphasis">{{ activeSession.pingMs }}ms</span>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-btn variant="text" size="x-small" density="compact" @click="changeFontSize(-1)">A−</v-btn>
|
||||
<span class="text-caption">{{ fontSize }}</span>
|
||||
<v-btn variant="text" size="x-small" density="compact" @click="changeFontSize(1)">A+</v-btn>
|
||||
<div class="d-flex align-center ga-2 terminal-toolbar-actions">
|
||||
<span class="text-body-2 text-medium-emphasis shrink-0">字号</span>
|
||||
<v-btn variant="tonal" size="small" @click="changeFontSize(-1)">缩小</v-btn>
|
||||
<span class="text-body-2 font-weight-medium" style="min-width: 1.5rem; text-align: center">{{ fontSize }}</span>
|
||||
<v-btn variant="tonal" size="small" @click="changeFontSize(1)">放大</v-btn>
|
||||
<v-select
|
||||
v-model="scrollback"
|
||||
:items="[1000, 5000, 10000, 50000]"
|
||||
density="compact"
|
||||
:items="scrollbackOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="回滚行数"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
style="max-width: 90px"
|
||||
class="terminal-toolbar-scrollback"
|
||||
@update:model-value="changeScrollback"
|
||||
/>
|
||||
<v-btn variant="text" size="x-small" density="compact" @click="toggleFullscreen">
|
||||
<v-icon size="14">mdi-fullscreen</v-icon>
|
||||
<v-btn variant="tonal" size="small" prepend-icon="mdi-fullscreen" @click="toggleFullscreen">
|
||||
全屏
|
||||
</v-btn>
|
||||
<v-btn variant="text" size="x-small" density="compact" color="error" @click="disconnect">
|
||||
<v-btn variant="tonal" size="small" color="error" prepend-icon="mdi-link-off" @click="disconnect">
|
||||
断开
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -204,6 +220,7 @@
|
||||
<div
|
||||
class="d-flex align-start ga-2 px-3 py-2 shrink-0 cmd-bar cmd-bar--tall"
|
||||
:class="{ 'opacity-50 pointer-events-none': !activeSession?.ws }"
|
||||
@contextmenu="onCmdContext"
|
||||
>
|
||||
<span class="cmd-bar__prompt font-mono text-primary shrink-0">❯</span>
|
||||
<div class="flex-grow-1 cmd-bar__input-wrap">
|
||||
@@ -217,11 +234,14 @@
|
||||
@keydown="onCmdKeydown"
|
||||
/>
|
||||
</div>
|
||||
<v-btn size="small" variant="tonal" color="primary" class="mt-1" @click="sendCmd" :disabled="!activeSession?.ws">发送</v-btn>
|
||||
<div class="d-flex ga-1 pl-2" style="border-left: 1px solid rgb(var(--v-theme-surface-variant))">
|
||||
<v-btn size="x-small" variant="outlined" density="compact" @click="sendCtrl('c')" title="Ctrl+C">^C</v-btn>
|
||||
<v-btn size="x-small" variant="outlined" density="compact" @click="sendCtrl('d')" title="Ctrl+D">^D</v-btn>
|
||||
<v-btn size="x-small" variant="outlined" density="compact" @click="sendKey('\t')" title="Tab补全">Tab</v-btn>
|
||||
<v-btn size="default" variant="tonal" color="primary" class="mt-1" @click="sendCmd" :disabled="!activeSession?.ws">发送</v-btn>
|
||||
<div class="terminal-hotkeys shrink-0 pl-3">
|
||||
<div class="text-body-2 font-weight-medium text-medium-emphasis mb-2">快捷键</div>
|
||||
<div class="d-flex flex-column ga-2">
|
||||
<v-btn size="default" variant="outlined" block @click="sendCtrl('c')">中断 (Ctrl+C)</v-btn>
|
||||
<v-btn size="default" variant="outlined" block @click="sendCtrl('d')">EOF (Ctrl+D)</v-btn>
|
||||
<v-btn size="default" variant="outlined" block @click="sendKey('\t')">补全 (Tab)</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,6 +376,15 @@ const servers = ref<{ id: number; name: string; domain: string; is_online: boole
|
||||
const showTermMenu = ref(false)
|
||||
const menuX = ref(0)
|
||||
const menuY = ref(0)
|
||||
const termMenuContext = ref<'terminal' | 'command'>('terminal')
|
||||
const termMenuLocation = computed(() => (menuY.value > window.innerHeight * 0.55 ? 'top end' : 'bottom end'))
|
||||
|
||||
const scrollbackOptions = [
|
||||
{ title: '1 千行', value: 1000 },
|
||||
{ title: '5 千行', value: 5000 },
|
||||
{ title: '1 万行', value: 10000 },
|
||||
{ title: '5 万行', value: 50000 },
|
||||
]
|
||||
|
||||
// ── Quick commands(只读展示,管理在系统设置) ──
|
||||
const quickCmds = ref<QuickCmdItem[]>([])
|
||||
@@ -779,18 +808,43 @@ function disconnect() {
|
||||
}
|
||||
|
||||
// ── Context menu handlers ──
|
||||
function onTermContext(e: MouseEvent) {
|
||||
function openTermMenu(e: MouseEvent, context: 'terminal' | 'command') {
|
||||
e.preventDefault()
|
||||
termMenuContext.value = context
|
||||
menuX.value = e.clientX
|
||||
menuY.value = e.clientY
|
||||
showTermMenu.value = true
|
||||
}
|
||||
|
||||
function onTermContext(e: MouseEvent) {
|
||||
openTermMenu(e, 'terminal')
|
||||
}
|
||||
|
||||
function onCmdContext(e: MouseEvent) {
|
||||
openTermMenu(e, 'command')
|
||||
}
|
||||
|
||||
function getCmdInputEl(): HTMLTextAreaElement | null {
|
||||
const root = document.querySelector('.cmd-bar--tall .sh-field__input--multi')
|
||||
return root instanceof HTMLTextAreaElement ? root : null
|
||||
}
|
||||
|
||||
function termCopy() {
|
||||
showTermMenu.value = false
|
||||
if (termMenuContext.value === 'command') {
|
||||
const el = getCmdInputEl()
|
||||
const text = el?.value.substring(el.selectionStart ?? 0, el.selectionEnd ?? 0) || ''
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(text).catch(() => snackbar('复制失败', 'error'))
|
||||
return
|
||||
}
|
||||
}
|
||||
const sel = window.getSelection()?.toString()
|
||||
if (sel) {
|
||||
navigator.clipboard.writeText(sel).catch(() => snackbar('复制失败', 'error'))
|
||||
} else if (activeSession.value?.term) {
|
||||
const buffer = activeSession.value.term.getSelection()
|
||||
if (buffer) navigator.clipboard.writeText(buffer).catch(() => snackbar('复制失败', 'error'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,7 +852,22 @@ async function termPaste() {
|
||||
showTermMenu.value = false
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (activeSession.value?.term && text) {
|
||||
if (!text) return
|
||||
if (termMenuContext.value === 'command') {
|
||||
const el = getCmdInputEl()
|
||||
if (el) {
|
||||
const start = el.selectionStart ?? el.value.length
|
||||
const end = el.selectionEnd ?? start
|
||||
const next = el.value.slice(0, start) + text + el.value.slice(end)
|
||||
cmdInput.value = next
|
||||
await nextTick()
|
||||
el.focus()
|
||||
const pos = start + text.length
|
||||
el.setSelectionRange(pos, pos)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (activeSession.value?.term) {
|
||||
activeSession.value.term.paste(text)
|
||||
}
|
||||
} catch {
|
||||
@@ -808,6 +877,14 @@ async function termPaste() {
|
||||
|
||||
function termSelectAll() {
|
||||
showTermMenu.value = false
|
||||
if (termMenuContext.value === 'command') {
|
||||
const el = getCmdInputEl()
|
||||
if (el) {
|
||||
el.focus()
|
||||
el.select()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (activeSession.value?.term) {
|
||||
activeSession.value.term.selectAll()
|
||||
}
|
||||
@@ -1111,6 +1188,29 @@ onBeforeUnmount(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.terminal-hotkeys {
|
||||
border-left: 1px solid rgb(var(--v-theme-surface-variant));
|
||||
min-width: 9.5rem;
|
||||
}
|
||||
|
||||
.terminal-toolbar-scrollback {
|
||||
min-width: 8.5rem;
|
||||
max-width: 10rem;
|
||||
}
|
||||
|
||||
:deep(.terminal-ctx-menu) {
|
||||
min-width: 11.5rem;
|
||||
}
|
||||
|
||||
:deep(.terminal-ctx-menu .v-list-item-title) {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.terminal-ctx-menu .v-icon) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
/* Shell 语法高亮(命令栏 + 快捷命令编辑) */
|
||||
.terminal-root--dark :deep(.sh-keyword) { color: #60a5fa; font-weight: 500; }
|
||||
.terminal-root--dark :deep(.sh-builtin) { color: #38bdf8; }
|
||||
|
||||
Reference in New Issue
Block a user