feat(workspace): add cpu core status display and safer quick command actions
Expose `cpuCores` in backend status collection with multi-command fallback and surface it in the status panel as a localized CPU core badge under the CPU model. Adjust terminal group UX by adding a server-level close-all control in the SSH tab group header. Reduce accidental quick command execution by switching list interaction to single-click select + double-click execute, while preserving keyboard Enter and context-menu execution paths.
This commit is contained in:
@@ -36,7 +36,10 @@
|
||||
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.cpuModelLabel') }}</label>
|
||||
<span class="cpu-model-value truncate text-left" :title="displayCpuModel">{{ displayCpuModel }}</span>
|
||||
<div class="cpu-spec-block text-left">
|
||||
<span class="cpu-model-value truncate" :title="displayCpuModel">{{ displayCpuModel }}</span>
|
||||
<span class="cpu-core-badge">{{ displayCpuCores }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||
@@ -245,6 +248,7 @@ const displaySwapPercent = computed(() => currentServerStatus.value?.swapPercent
|
||||
const currentStatusError = computed<string | null>(() => currentSessionState.value?.statusMonitorManager?.statusError?.value ?? null);
|
||||
|
||||
const cachedCpuModel = ref<string | null>(null);
|
||||
const cachedCpuCores = ref<number | null>(null);
|
||||
const cachedOsName = ref<string | null>(null);
|
||||
|
||||
watch(currentServerStatus, newData => {
|
||||
@@ -252,6 +256,9 @@ watch(currentServerStatus, newData => {
|
||||
if (newData.cpuModel) {
|
||||
cachedCpuModel.value = newData.cpuModel;
|
||||
}
|
||||
if (typeof newData.cpuCores === 'number' && Number.isFinite(newData.cpuCores)) {
|
||||
cachedCpuCores.value = newData.cpuCores;
|
||||
}
|
||||
if (newData.osName) {
|
||||
cachedOsName.value = newData.osName;
|
||||
}
|
||||
@@ -266,6 +273,14 @@ watch(() => props.activeSessionId, async (newId, oldId) => {
|
||||
});
|
||||
|
||||
const displayCpuModel = computed(() => (currentServerStatus.value?.cpuModel ?? cachedCpuModel.value) || t('statusMonitor.notAvailable'));
|
||||
const displayCpuCores = computed(() => {
|
||||
const cpuCores = currentServerStatus.value?.cpuCores ?? cachedCpuCores.value;
|
||||
if (typeof cpuCores !== 'number' || !Number.isFinite(cpuCores)) {
|
||||
return t('statusMonitor.notAvailable');
|
||||
}
|
||||
|
||||
return t('statusMonitor.cpuCoresValue', { count: Math.round(cpuCores) });
|
||||
});
|
||||
const displayOsName = computed(() => (currentServerStatus.value?.osName ?? cachedOsName.value) || t('statusMonitor.notAvailable'));
|
||||
|
||||
const formatBytesPerSecond = (bytes?: number): string => {
|
||||
@@ -434,6 +449,36 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.cpu-spec-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cpu-model-value {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cpu-core-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(96, 165, 250, 0.26);
|
||||
background: rgba(37, 99, 235, 0.14);
|
||||
color: #dbeafe;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-card__header {
|
||||
@@ -530,6 +575,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.memory-stat__label,
|
||||
@@ -537,6 +583,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary-color, #9ca3af);
|
||||
}
|
||||
@@ -550,6 +597,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
line-height: 1.15;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.memory-stat__dot {
|
||||
@@ -654,6 +702,11 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.disk-table__header > span,
|
||||
.disk-table__row > span {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.disk-table__header {
|
||||
color: var(--text-secondary-color, #9ca3af);
|
||||
font-size: 12px;
|
||||
@@ -688,6 +741,67 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
border: 1px solid rgba(34, 197, 94, 0.18);
|
||||
}
|
||||
|
||||
@container (max-width: 320px) {
|
||||
.status-card__header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.memory-card__content,
|
||||
.disk-card__body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.memory-ring,
|
||||
.disk-usage-tube {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.memory-stats-grid,
|
||||
.disk-rate-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.memory-stat__value,
|
||||
.disk-rate-card__value {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.disk-meta-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.disk-table__header,
|
||||
.disk-table__row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 250px) {
|
||||
.status-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.status-card__badge {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.memory-stat__value,
|
||||
.disk-rate-card__value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.disk-table__header,
|
||||
.disk-table__row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.disk-mount-pill,
|
||||
.disk-percent-pill {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.memory-card__content,
|
||||
.disk-card__body {
|
||||
|
||||
@@ -42,7 +42,7 @@ let lastResizeObserverWidth = 0;
|
||||
let lastResizeObserverHeight = 0;
|
||||
const RESIZE_THRESHOLD = 0.5; // px
|
||||
const BOTTOM_STICK_THRESHOLD = 2;
|
||||
let lastKnownViewportLine = 0;
|
||||
let lastKnownDistanceFromBottom = 0;
|
||||
let lastKnownShouldStickToBottom = true;
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ const debounce = (func: Function, delay: number) => {
|
||||
};
|
||||
|
||||
type TerminalViewportSnapshot = {
|
||||
viewportLine: number;
|
||||
distanceFromBottom: number;
|
||||
shouldStickToBottom: boolean;
|
||||
};
|
||||
|
||||
@@ -103,30 +103,38 @@ const getViewportSnapshot = (term: Terminal): TerminalViewportSnapshot => {
|
||||
const buffer = term.buffer.active;
|
||||
const maxScrollLine = Math.max(0, buffer.baseY);
|
||||
const viewportLine = Math.max(0, Math.min(buffer.viewportY, maxScrollLine));
|
||||
// Keep a relative offset from the live bottom so hidden terminals can recover
|
||||
// the same reading position even if logs keep appending while inactive.
|
||||
const distanceFromBottom = Math.max(0, maxScrollLine - viewportLine);
|
||||
|
||||
return {
|
||||
viewportLine,
|
||||
shouldStickToBottom: maxScrollLine - viewportLine <= BOTTOM_STICK_THRESHOLD,
|
||||
distanceFromBottom,
|
||||
shouldStickToBottom: distanceFromBottom <= BOTTOM_STICK_THRESHOLD,
|
||||
};
|
||||
};
|
||||
|
||||
const syncViewportTracking = (term: Terminal): TerminalViewportSnapshot => {
|
||||
const snapshot = getViewportSnapshot(term);
|
||||
lastKnownViewportLine = snapshot.viewportLine;
|
||||
lastKnownDistanceFromBottom = snapshot.distanceFromBottom;
|
||||
lastKnownShouldStickToBottom = snapshot.shouldStickToBottom;
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
const restoreViewportSnapshot = (term: Terminal, snapshot?: TerminalViewportSnapshot) => {
|
||||
const effectiveSnapshot = snapshot ?? {
|
||||
viewportLine: lastKnownViewportLine,
|
||||
distanceFromBottom: lastKnownDistanceFromBottom,
|
||||
shouldStickToBottom: lastKnownShouldStickToBottom,
|
||||
};
|
||||
const maxScrollLine = Math.max(0, term.buffer.active.baseY);
|
||||
|
||||
if (effectiveSnapshot.shouldStickToBottom) {
|
||||
term.scrollToBottom();
|
||||
} else {
|
||||
const targetLine = Math.min(effectiveSnapshot.viewportLine, Math.max(0, term.buffer.active.baseY));
|
||||
const targetDistanceFromBottom = Math.min(
|
||||
Math.max(0, effectiveSnapshot.distanceFromBottom),
|
||||
maxScrollLine,
|
||||
);
|
||||
const targetLine = Math.max(0, maxScrollLine - targetDistanceFromBottom);
|
||||
term.scrollToLine(targetLine);
|
||||
}
|
||||
|
||||
@@ -418,7 +426,7 @@ onMounted(() => {
|
||||
// --- Become Active ---
|
||||
console.log(`[Terminal ${props.sessionId}] Becoming active. Observing element and fitting.`);
|
||||
const activationViewportSnapshot = {
|
||||
viewportLine: lastKnownViewportLine,
|
||||
distanceFromBottom: lastKnownDistanceFromBottom,
|
||||
shouldStickToBottom: lastKnownShouldStickToBottom,
|
||||
};
|
||||
// Start observing
|
||||
|
||||
@@ -58,6 +58,16 @@ const closeSession = (event: MouseEvent, sessionId: string) => {
|
||||
emitWorkspaceEvent('session:close', { sessionId });
|
||||
};
|
||||
|
||||
const closeConnectionGroup = (event: MouseEvent, connectionId: string) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const sessionIds = getConnectionSessions(connectionId).map((session) => session.sessionId);
|
||||
sessionIds.forEach((sessionId) => {
|
||||
emitWorkspaceEvent('session:close', { sessionId });
|
||||
});
|
||||
};
|
||||
|
||||
// --- 本地状态 ---
|
||||
const sessionStore = useSessionStore(); // Session store 保持不变
|
||||
const showConnectionListPopup = ref(false); // 连接列表弹出状态
|
||||
@@ -534,34 +544,49 @@ onBeforeUnmount(() => {
|
||||
:class="['flex h-full flex-shrink-0 items-stretch py-1', isGroupStart(index) ? 'pl-1' : 'pl-0']"
|
||||
@dragstart="handleDragStart"
|
||||
>
|
||||
<button
|
||||
<div
|
||||
v-if="isSshConnection(session.connectionId)"
|
||||
type="button"
|
||||
:class="[
|
||||
'group flex h-full items-center gap-2 rounded-md border px-3 text-left transition-all duration-150',
|
||||
'group flex h-full items-center rounded-md border px-3 transition-all duration-150',
|
||||
session.connectionId === activeConnectionId
|
||||
? 'border-primary/60 bg-primary/10 text-foreground shadow-[0_0_0_1px_rgba(34,197,94,0.18)]'
|
||||
: 'border-border/70 bg-header/80 text-text-secondary shadow-[0_0_0_1px_rgba(0,0,0,0.08)] hover:bg-border hover:text-foreground',
|
||||
]"
|
||||
:title="getTopLevelItemTitle(session)"
|
||||
@click="activateTopLevelItem(session)"
|
||||
@contextmenu.prevent="showTopLevelContextMenu($event, session)"
|
||||
>
|
||||
<i class="fas fa-server text-[11px] text-primary/80"></i>
|
||||
<span class="max-w-[180px] truncate text-xs font-semibold tracking-wide">
|
||||
{{ session.connectionName }}
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
'rounded-full px-1.5 py-0.5 text-[10px] font-semibold',
|
||||
session.connectionId === activeConnectionId
|
||||
? 'bg-primary/15 text-foreground/90'
|
||||
: 'bg-black/20 text-text-secondary group-hover:text-foreground',
|
||||
]"
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 flex-1 items-center gap-2 text-left"
|
||||
@click="activateTopLevelItem(session)"
|
||||
>
|
||||
{{ getConnectionSessionCount(session.connectionId) }}
|
||||
</span>
|
||||
</button>
|
||||
<i class="fas fa-server text-[11px] text-primary/80"></i>
|
||||
<span class="max-w-[180px] truncate text-xs font-semibold tracking-wide">
|
||||
{{ session.connectionName }}
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
'rounded-full px-1.5 py-0.5 text-[10px] font-semibold',
|
||||
session.connectionId === activeConnectionId
|
||||
? 'bg-primary/15 text-foreground/90'
|
||||
: 'bg-black/20 text-text-secondary group-hover:text-foreground',
|
||||
]"
|
||||
>
|
||||
{{ getConnectionSessionCount(session.connectionId) }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded-full p-0.5 text-text-secondary opacity-0 transition-opacity duration-150 hover:bg-header hover:text-foreground group-hover:opacity-100"
|
||||
:class="{ 'text-foreground': session.connectionId === activeConnectionId }"
|
||||
:title="t('terminalTabBar.closeConnectionGroupTooltip', { name: session.connectionName, count: getConnectionSessionCount(session.connectionId) })"
|
||||
@click="closeConnectionGroup($event, session.connectionId)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="[
|
||||
|
||||
@@ -278,7 +278,18 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
||||
|
||||
// Helper function to parse a single script line using minimist
|
||||
|
||||
|
||||
const stripWrappedQuotes = (value: string): string => {
|
||||
if (value.length >= 2) {
|
||||
const firstChar = value[0];
|
||||
const lastChar = value[value.length - 1];
|
||||
if ((firstChar === '"' || firstChar === '\'') && firstChar === lastChar) {
|
||||
return value.slice(1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const parseScriptLine = (line: string): { type: 'SSH' | 'RDP' | 'VNC', userHostPort: string, name: string, password: string | null, keyName: string | null, proxyName: string | null, tags: string[], note: string | null, error?: string } => {
|
||||
line = line.trim();
|
||||
if (!line) {
|
||||
@@ -309,8 +320,8 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
||||
let note: string | null = null;
|
||||
|
||||
// 4. Parse optionsString
|
||||
// Regex to split by space, respecting quotes
|
||||
const args = optionsString.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
||||
// Regex to split by space, respecting both single and double quotes
|
||||
const args = optionsString.match(/"[^"]*"|'[^']*'|[^\s]+/g) || [];
|
||||
let i = 0;
|
||||
while (i < args.length) {
|
||||
const arg = args[i];
|
||||
@@ -322,7 +333,7 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
||||
// Handle -tags, which can be followed by zero or more tags
|
||||
tags = [];
|
||||
while (i < args.length && !args[i].startsWith('-')) {
|
||||
tags.push(args[i].replace(/^"|"$/g, '')); // Remove surrounding quotes
|
||||
tags.push(stripWrappedQuotes(args[i]));
|
||||
i++;
|
||||
}
|
||||
// No need to i++ here, the next loop iteration or outer loop handles it
|
||||
@@ -333,14 +344,14 @@ export function useAddConnectionForm(props: AddConnectionFormProps, emit: AddCon
|
||||
noteParts.push(args[i]);
|
||||
i++;
|
||||
}
|
||||
note = noteParts.join(' ').replace(/^"|"$/g, ''); // Join parts and remove quotes
|
||||
note = stripWrappedQuotes(noteParts.join(' '));
|
||||
break; // Exit the outer loop as note consumes the rest
|
||||
} else if (i >= args.length) {
|
||||
// All other options require a value
|
||||
return { type, userHostPort: userHostPortPart, name, password, keyName, proxyName, tags, note, error: t('connections.form.scriptErrorMissingValueForKey', { key: arg }) };
|
||||
} else {
|
||||
// Handle options that require a single value
|
||||
const value = args[i].replace(/^"|"$/g, ''); // Remove surrounding quotes
|
||||
const value = stripWrappedQuotes(args[i]);
|
||||
switch (key) {
|
||||
case 'type':
|
||||
const typeValue = value.toUpperCase();
|
||||
|
||||
@@ -626,6 +626,7 @@
|
||||
"errorPrefix": "Error:",
|
||||
"loading": "Waiting for data...",
|
||||
"cpuModelLabel": "CPU Model:",
|
||||
"cpuCoresValue": "{count} cores",
|
||||
"osLabel": "OS:",
|
||||
"cpuLabel": "CPU:",
|
||||
"memoryLabel": "Memory:",
|
||||
@@ -1627,6 +1628,7 @@
|
||||
"selectServerTitle": "Select server to connect",
|
||||
"showTransferProgressTooltip": "Show/Hide Transfer Progress",
|
||||
"newTerminalTooltip": "Open another terminal for the current server",
|
||||
"closeConnectionGroupTooltip": "Close all terminals for {name} ({count})",
|
||||
"openConnectionPickerTooltip": "Choose another server",
|
||||
"terminalBadge": "Terminal {index}",
|
||||
"serverEntryTitle": "{name} · {count} terminals",
|
||||
|
||||
@@ -1365,6 +1365,7 @@
|
||||
"bytesPerSecond": "B/秒",
|
||||
"cpuLabel": "CPU:",
|
||||
"cpuModelLabel": "CPU モデル:",
|
||||
"cpuCoresValue": "{count} コア",
|
||||
"diskLabel": "ディスク:",
|
||||
"errorPrefix": "エラー:",
|
||||
"gigaBytes": "GB",
|
||||
@@ -1587,6 +1588,7 @@
|
||||
"selectServerTitle": "接続するサーバーを選択",
|
||||
"showTransferProgressTooltip": "転送進捗の表示/非表示",
|
||||
"newTerminalTooltip": "現在のサーバーに新しいターミナルを追加",
|
||||
"closeConnectionGroupTooltip": "{name} の端末をすべて閉じる ({count} 件)",
|
||||
"openConnectionPickerTooltip": "別のサーバーを選択",
|
||||
"terminalBadge": "端末 {index}"
|
||||
},
|
||||
|
||||
@@ -626,6 +626,7 @@
|
||||
"errorPrefix": "错误:",
|
||||
"loading": "等待数据...",
|
||||
"cpuModelLabel": "CPU 型号:",
|
||||
"cpuCoresValue": "{count} 核",
|
||||
"osLabel": "系统:",
|
||||
"cpuLabel": "CPU:",
|
||||
"memoryLabel": "内存:",
|
||||
@@ -1631,6 +1632,7 @@
|
||||
"selectServerTitle": "选择要连接的服务器",
|
||||
"showTransferProgressTooltip": "显示/隐藏传输进度",
|
||||
"newTerminalTooltip": "为当前服务器新增终端",
|
||||
"closeConnectionGroupTooltip": "关闭 {name} 的全部终端({count} 个)",
|
||||
"openConnectionPickerTooltip": "选择其他服务器",
|
||||
"terminalBadge": "终端 {index}",
|
||||
"serverEntryTitle": "{name} · {count} 个终端",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 类型定义:用于服务器状态监控数据 (从 useStatusMonitor 迁移)
|
||||
export interface ServerStatus {
|
||||
cpuPercent?: number;
|
||||
cpuCores?: number;
|
||||
memPercent?: number;
|
||||
memUsed?: number; // MB
|
||||
memTotal?: number; // MB
|
||||
|
||||
@@ -111,10 +111,12 @@
|
||||
v-for="(cmd) in groupData.commands"
|
||||
:key="cmd.id"
|
||||
:data-command-id="cmd.id"
|
||||
:title="cmd.command"
|
||||
class="group flex justify-between items-center mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||
:style="{ padding: isCompactMode ? `calc(0.1rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.625rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }"
|
||||
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
||||
@click="executeCommand(cmd)"
|
||||
@click="selectCommand(cmd.id)"
|
||||
@dblclick="executeCommand(cmd)"
|
||||
@contextmenu.prevent="showQuickCommandContextMenu($event, cmd)"
|
||||
>
|
||||
<!-- Command Info -->
|
||||
@@ -157,10 +159,12 @@
|
||||
v-for="(cmd) in flatFilteredCommands"
|
||||
:key="cmd.id"
|
||||
:data-command-id="cmd.id"
|
||||
:title="cmd.command"
|
||||
class="group flex justify-between items-center mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||
:style="{ padding: isCompactMode ? `calc(0.1rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.625rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }"
|
||||
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
||||
@click="executeCommand(cmd)"
|
||||
@click="selectCommand(cmd.id)"
|
||||
@dblclick="executeCommand(cmd)"
|
||||
@contextmenu.prevent="showQuickCommandContextMenu($event, cmd)"
|
||||
>
|
||||
<!-- Command Info -->
|
||||
@@ -392,6 +396,10 @@ const isCommandSelected = (commandId: number): boolean => {
|
||||
return flatVisibleCommands.value[storeSelectedIndex.value].id === commandId;
|
||||
};
|
||||
|
||||
const selectCommand = (commandId: number) => {
|
||||
storeSelectedIndex.value = flatVisibleCommands.value.findIndex((cmd) => cmd.id === commandId);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
|
||||
Reference in New Issue
Block a user