This commit is contained in:
Baobhan Sith
2025-04-25 15:54:43 +08:00
parent 13763035d5
commit b1fbcb254a
9 changed files with 204 additions and 154 deletions
@@ -387,7 +387,7 @@ const testButtonText = computed(() => {
</form> <!-- End Form -->
<!-- Form Actions -->
<div class="flex justify-between items-center pt-5 mt-6 border-t border-border flex-shrink-0">
<div class="flex justify-between items-center pt-5 mt-6 flex-shrink-0">
<div class="flex flex-col items-start gap-1"> <!-- Test Area -->
<div class="flex items-center gap-2"> <!-- Button and Icon -->
<button type="button" @click="handleTestConnection" :disabled="isLoading || testStatus === 'testing'"
@@ -1,33 +1,35 @@
<template>
<div class="fixed inset-0 bg-overlay flex justify-center items-center z-[1050]" @click.self="closeForm">
<div class="bg-background text-foreground p-6 rounded-lg border border-border shadow-xl w-[90%] max-w-lg"> <!-- Changed bg-dialog to bg-background, text-dialog-text to text-foreground -->
<h2 class="m-0 mb-6 text-center text-xl font-medium">{{ isEditing ? t('quickCommands.form.titleEdit', '编辑快捷指令') : t('quickCommands.form.titleAdd', '添加快捷指令') }}</h2>
<form @submit.prevent="handleSubmit">
<div class="mb-4">
<label for="qc-name" class="block mb-2 font-bold text-text-secondary text-sm">{{ t('quickCommands.form.name', '名称:') }}</label>
<div class="bg-background text-foreground p-6 rounded-xl border border-border/50 shadow-2xl w-[90%] max-w-lg">
<h2 class="m-0 mb-6 text-center text-xl font-semibold">{{ isEditing ? t('quickCommands.form.titleEdit', '编辑快捷指令') : t('quickCommands.form.titleAdd', '添加快捷指令') }}</h2>
<form @submit.prevent="handleSubmit" class="space-y-5">
<div>
<label for="qc-name" class="block mb-1.5 text-sm font-medium text-text-secondary">{{ t('quickCommands.form.name', '名称:') }}</label>
<input
id="qc-name"
type="text"
v-model="formData.name"
:placeholder="t('quickCommands.form.namePlaceholder', '可选,用于快速识别')"
class="w-full px-3 py-2 border border-border rounded bg-input text-foreground text-base focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors duration-150"
class="w-full px-4 py-2 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out"
/>
</div>
<div class="mb-4">
<label for="qc-command" class="block mb-2 font-bold text-text-secondary text-sm">{{ t('quickCommands.form.command', '指令:') }} <span class="text-error">*</span></label>
<div>
<label for="qc-command" class="block mb-1.5 text-sm font-medium text-text-secondary">{{ t('quickCommands.form.command', '指令:') }} <span class="text-error">*</span></label>
<textarea
id="qc-command"
v-model="formData.command"
required
rows="3"
rows="4"
:placeholder="t('quickCommands.form.commandPlaceholder', '例如:ls -alh /home/user')"
class="w-full px-3 py-2 border border-border rounded bg-input text-foreground text-base resize-vertical min-h-[80px] focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors duration-150"
class="w-full px-4 py-2 border border-border/50 rounded-lg bg-input text-foreground text-sm resize-y min-h-[100px] shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out"
></textarea>
<small v-if="commandError" class="text-error text-xs mt-1 block">{{ commandError }}</small>
</div>
<div class="flex justify-end mt-6 pt-2 border-t border-border">
<button type="button" @click="closeForm" class="py-2 px-4 rounded text-sm transition-colors duration-150 bg-button text-button-text hover:bg-button-hover border border-border mr-2">{{ t('common.cancel', '取消') }}</button>
<button type="submit" :disabled="isSubmitting || !!commandError" class="py-2 px-4 rounded text-sm transition-colors duration-150 bg-primary text-white hover:bg-primary-dark disabled:bg-gray-400 disabled:opacity-70 disabled:cursor-not-allowed">
<div class="flex justify-end mt-8 pt-4 border-t border-border/50">
<!-- Secondary/Cancel Button -->
<button type="button" @click="closeForm" class="py-2 px-5 rounded-lg text-sm font-medium transition-colors duration-150 bg-background border border-border/50 text-text-secondary hover:bg-border hover:text-foreground mr-3">{{ t('common.cancel', '取消') }}</button>
<!-- Primary/Submit Button -->
<button type="submit" :disabled="isSubmitting || !!commandError" class="py-2 px-5 rounded-lg text-sm font-semibold transition-colors duration-150 bg-primary text-white border-none shadow-md hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:bg-gray-400 disabled:opacity-70 disabled:cursor-not-allowed">
{{ isSubmitting ? t('common.saving', '保存中...') : (isEditing ? t('common.save', '保存') : t('quickCommands.form.add', '添加')) }}
</button>
</div>
@@ -221,26 +221,25 @@ onBeforeUnmount(() => {
</script>
<template>
<div class="flex items-center px-2.5 py-1.5 bg-background min-h-[30px] gap-1.5">
<div class="flex-grow flex items-center bg-transparent relative gap-1.5">
<div class="flex items-center px-2 py-1.5 bg-background gap-2"> <!-- Removed border-t and border-border/50 -->
<div class="flex-grow flex items-center bg-transparent relative gap-2"> <!-- Adjusted gap -->
<!-- Focus Switcher Config Button -->
<button
@click="focusSwitcherStore.toggleConfigurator(true)"
class="flex-shrink-0 flex items-center justify-center w-7 h-7 text-text-secondary rounded transition-colors duration-200 hover:bg-black/10 hover:text-foreground"
class="flex-shrink-0 flex items-center justify-center w-8 h-8 border border-border/50 rounded-lg text-text-secondary transition-colors duration-200 hover:bg-border hover:text-foreground"
:title="t('commandInputBar.configureFocusSwitch', '配置焦点切换')"
>
<i class="fas fa-keyboard text-base text-primary transition-colors duration-200"></i>
<i class="fas fa-keyboard text-base"></i> <!-- Removed text-primary -->
</button>
<!-- Command Input -->
<input
type="text"
v-model="commandInput"
:placeholder="t('commandInputBar.placeholder')"
class="flex-grow px-2.5 py-1.5 border border-border rounded text-sm bg-input text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all duration-300 ease-in-out"
class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-300 ease-in-out"
:class="{ 'basis-3/4': isSearching, 'basis-full': !isSearching }"
ref="commandInputRef"
data-focus-id="commandInput"
@keydown="handleCommandInputKeydown"
@blur="handleCommandInputBlur"
/>
@@ -251,7 +250,7 @@ onBeforeUnmount(() => {
type="text"
v-model="searchTerm"
:placeholder="t('commandInputBar.searchPlaceholder')"
class="flex-grow px-2.5 py-1.5 border border-border rounded text-sm bg-input text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all duration-300 ease-in-out basis-1/4 ml-1.5"
class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-300 ease-in-out basis-1/4"
data-focus-id="terminalSearch"
@keydown.enter.prevent="findNext"
@keydown.shift.enter.prevent="findPrevious"
@@ -261,30 +260,30 @@ onBeforeUnmount(() => {
/>
<!-- Search Controls -->
<div class="flex items-center gap-1 flex-shrink-0">
<div class="flex items-center gap-1 flex-shrink-0"> <!-- Adjusted gap -->
<button
@click="toggleSearch"
class="flex items-center justify-center w-7 h-7 text-text-secondary rounded transition-colors duration-200 hover:bg-black/10 hover:text-foreground"
class="flex items-center justify-center w-8 h-8 border border-border/50 rounded-lg text-text-secondary transition-colors duration-200 hover:bg-border hover:text-foreground"
:title="isSearching ? t('commandInputBar.closeSearch') : t('commandInputBar.openSearch')"
>
<i v-if="!isSearching" class="fas fa-search text-base text-primary transition-colors duration-200"></i>
<i v-else class="fas fa-times text-base text-primary transition-colors duration-200"></i>
<i v-if="!isSearching" class="fas fa-search text-base"></i>
<i v-else class="fas fa-times text-base"></i>
</button>
<template v-if="isSearching">
<button
@click="findPrevious"
class="flex items-center justify-center w-7 h-7 text-text-secondary rounded transition-colors duration-200 hover:bg-black/10 hover:text-foreground"
class="flex items-center justify-center w-8 h-8 border border-border/50 rounded-lg text-text-secondary transition-colors duration-200 hover:bg-border hover:text-foreground"
:title="t('commandInputBar.findPrevious')"
>
<i class="fas fa-arrow-up text-base text-primary transition-colors duration-200"></i>
<i class="fas fa-arrow-up text-base"></i>
</button>
<button
@click="findNext"
class="flex items-center justify-center w-7 h-7 text-text-secondary rounded transition-colors duration-200 hover:bg-black/10 hover:text-foreground"
class="flex items-center justify-center w-8 h-8 border border-border/50 rounded-lg text-text-secondary transition-colors duration-200 hover:bg-border hover:text-foreground"
:title="t('commandInputBar.findNext')"
>
<i class="fas fa-arrow-down text-base text-primary transition-colors duration-200"></i>
<i class="fas fa-arrow-down text-base"></i>
</button>
</template>
</div>
@@ -910,7 +910,7 @@ defineExpose({ focusSearchInput, startPathEdit });
<template>
<div class="flex flex-col h-full overflow-hidden bg-background text-foreground text-sm font-sans">
<div class="flex items-center justify-between flex-wrap gap-2 p-2 bg-header border-b border-border flex-shrink-0">
<div class="flex items-center justify-between flex-wrap gap-2 p-2 bg-header flex-shrink-0">
<!-- Path Bar -->
<div class="flex items-center bg-background border border-border rounded px-1.5 py-0.5 overflow-hidden min-w-[100px] flex-shrink">
<span v-show="!isEditingPath" class="text-text-secondary whitespace-nowrap overflow-x-auto pr-2">
@@ -1045,7 +1045,7 @@ defineExpose({ focusSearchInput, startPathEdit });
</div>
<!-- File Table -->
<table ref="tableRef" class="w-full border-collapse table-fixed border border-border rounded" @contextmenu.prevent>
<table ref="tableRef" class="w-full border-collapse table-fixed border-border rounded" @contextmenu.prevent>
<colgroup>
<col :style="{ width: `${colWidths.type}px` }">
<col :style="{ width: `${colWidths.name}px` }">
@@ -1,6 +1,6 @@
<template>
<!-- Root element with padding, background, border, and text styles -->
<div class="status-monitor p-4 border-l border-border bg-background text-foreground h-full overflow-y-auto text-sm">
<div class="status-monitor p-4 bg-background text-foreground h-full overflow-y-auto text-sm">
<!-- Title with margin, border, padding, font size, and color -->
<h4 class="mt-0 mb-4 border-b border-border pb-2 text-base font-medium">
{{ t('statusMonitor.title') }}
@@ -301,96 +301,109 @@ const scrollToHighlighted = async () => {
</script>
<template>
<div class="h-full flex flex-col overflow-hidden bg-background text-sm text-foreground">
<div v-if="connectionsLoading || tagsLoading" class="p-4 text-center text-text-secondary">
{{ t('common.loading') }}
<div class="h-full flex flex-col overflow-hidden bg-background text-foreground">
<!-- Loading / Error State -->
<div v-if="connectionsLoading || tagsLoading" class="flex items-center justify-center h-full text-text-secondary">
<i class="fas fa-spinner fa-spin mr-2"></i> {{ t('common.loading') }}
</div>
<div v-else-if="connectionsError || tagsError" class="p-4 text-center text-red-600">
{{ connectionsError || tagsError }}
<div v-else-if="connectionsError || tagsError" class="flex items-center justify-center h-full text-error px-4 text-center">
<i class="fas fa-exclamation-triangle mr-2"></i> {{ connectionsError || tagsError }}
</div>
<!-- 搜索和添加栏 -->
<div class="flex p-2 border-b border-border bg-header">
<input
type="text"
v-model="searchTerm"
:placeholder="t('workspaceConnectionList.searchPlaceholder')"
ref="searchInputRef"
class="flex-grow min-w-0 px-3 py-1.5 border border-border rounded-l-md text-sm outline-none bg-background text-foreground focus:border-primary focus:ring-2 focus:ring-primary focus:ring-opacity-50 transition-colors duration-150"
data-focus-id="connectionListSearch"
@keydown="handleKeyDown"
@blur="handleBlur"
/>
<button
class="px-3 py-1.5 border border-border border-l-0 bg-background cursor-pointer rounded-r-md text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150"
@click="handleMenuAction('add')"
:title="t('connections.addConnection')"
>
<i class="fas fa-plus"></i>
</button>
</div>
<!-- Main Content Area -->
<div v-else class="flex flex-col h-full">
<!-- Search and Add Bar -->
<div class="flex p-2 border-b border-border/50"> <!-- Reduced padding p-3 to p-2 -->
<input
type="text"
v-model="searchTerm"
:placeholder="t('workspaceConnectionList.searchPlaceholder')"
ref="searchInputRef"
class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out"
data-focus-id="connectionListSearch"
@keydown="handleKeyDown"
@blur="handleBlur"
/>
<button
class="ml-2 w-8 h-8 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:bg-gray-400 disabled:cursor-not-allowed disabled:opacity-70 flex-shrink-0 flex items-center justify-center"
@click="handleMenuAction('add')"
:title="t('connections.addConnection')"
>
<i class="fas fa-plus text-white"></i>
</button>
</div>
<!-- 连接列表区域 -->
<div class="flex-grow overflow-y-auto" ref="listAreaRef">
<div v-if="connectionsLoading || tagsLoading" class="p-4 text-center text-text-secondary">
{{ t('common.loading') }}
</div>
<div v-else-if="connectionsError || tagsError" class="p-4 text-center text-red-600">
{{ connectionsError || tagsError }}
</div>
<div v-else-if="filteredAndGroupedConnections.length === 0 && connections.length > 0" class="p-4 text-center text-text-secondary">
{{ t('workspaceConnectionList.noResults') }} "{{ searchTerm }}"
</div>
<div v-else-if="connections.length === 0" class="p-4 text-center text-text-secondary">
{{ t('connections.noConnections') }}
</div>
<div v-else>
<!-- 循环分组 -->
<div v-for="groupData in filteredAndGroupedConnections" :key="groupData.groupName" class="mb-0 last:mb-0">
<div class="group px-3 py-2 font-semibold cursor-pointer bg-header border-t border-b border-border flex items-center text-foreground hover:bg-border transition-colors duration-150" @click="toggleGroup(groupData.groupName)">
<i :class="['fas', expandedGroups[groupData.groupName] ? 'fa-chevron-down' : 'fa-chevron-right', 'mr-2 w-4 text-center text-text-secondary group-hover:text-foreground transition-transform duration-200 ease-in-out']"></i>
<span>{{ groupData.groupName }}</span>
</div>
<!-- 连接项列表 -->
<ul v-show="expandedGroups[groupData.groupName]" class="list-none p-0 m-0">
<li
v-for="conn in groupData.connections"
:key="conn.id"
class="group py-2 pr-4 pl-6 cursor-pointer flex items-center border-b border-border whitespace-nowrap overflow-hidden text-ellipsis text-foreground hover:bg-header/50 transition-colors duration-150"
:class="{ 'bg-primary/10 text-primary': conn.id === highlightedConnectionId }"
:data-conn-id="conn.id"
@click.left="handleConnect(conn.id)"
@contextmenu.prevent="showContextMenu($event, conn)"
<!-- Connection List Area -->
<div class="flex-grow overflow-y-auto p-2" ref="listAreaRef">
<!-- No Results / No Connections State -->
<div v-if="filteredAndGroupedConnections.length === 0 && connections.length > 0" class="p-6 text-center text-text-secondary">
<i class="fas fa-search text-xl mb-2"></i>
<p>{{ t('workspaceConnectionList.noResults') }} "{{ searchTerm }}"</p>
</div>
<div v-else-if="connections.length === 0" class="p-6 text-center text-text-secondary">
<i class="fas fa-plug text-xl mb-2"></i>
<p>{{ t('connections.noConnections') }}</p>
<button
class="mt-4 px-4 py-2 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
@click="handleMenuAction('add')"
>
{{ t('connections.addFirstConnection') }} <!-- Need translation -->
</button>
</div>
<!-- Groups and Connections -->
<div v-else>
<div v-for="groupData in filteredAndGroupedConnections" :key="groupData.groupName" class="mb-1 last:mb-0">
<!-- Group Header -->
<div
class="group px-3 py-2 font-semibold cursor-pointer flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
@click="toggleGroup(groupData.groupName)"
>
<i class="fas fa-server mr-2.5 w-4 text-center text-text-secondary group-hover:text-foreground" :class="{ 'text-primary': conn.id === highlightedConnectionId }"></i>
<span class="overflow-hidden text-ellipsis whitespace-nowrap flex-grow" :title="conn.name || conn.host">
{{ conn.name || conn.host }}
</span>
</li>
</ul>
<i :class="['fas', expandedGroups[groupData.groupName] ? 'fa-chevron-down' : 'fa-chevron-right', 'mr-2 w-4 text-center text-text-secondary group-hover:text-foreground transition-transform duration-200 ease-in-out', {'transform rotate-0': !expandedGroups[groupData.groupName]}]"></i>
<span class="text-sm">{{ groupData.groupName }}</span>
</div>
<!-- Connection Items List -->
<ul v-show="expandedGroups[groupData.groupName]" class="list-none p-0 m-0 pl-3">
<li
v-for="conn in groupData.connections"
:key="conn.id"
class="group my-0.5 py-2 pr-3 pl-4 cursor-pointer flex items-center rounded-md whitespace-nowrap overflow-hidden text-ellipsis text-foreground hover:bg-primary/10 transition-colors duration-150"
:class="{ 'bg-primary/20 text-white font-medium': conn.id === highlightedConnectionId }"
:data-conn-id="conn.id"
@click.left="handleConnect(conn.id)"
@contextmenu.prevent="showContextMenu($event, conn)"
@mousedown.middle.prevent="handleOpenInNewTab(conn.id)"
@auxclick.prevent="handleOpenInNewTab(conn.id)"
>
<i class="fas fa-server mr-2.5 w-4 text-center text-text-secondary group-hover:text-primary" :class="{ 'text-white': conn.id === highlightedConnectionId }"></i>
<span class="overflow-hidden text-ellipsis whitespace-nowrap flex-grow text-sm" :title="conn.name || conn.host">
{{ conn.name || conn.host }}
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 右键菜单 -->
<!-- Context Menu -->
<div
v-if="contextMenuVisible"
class="fixed bg-background border border-border shadow-lg rounded-md py-1 z-50 min-w-[160px]"
class="fixed bg-background border border-border/50 shadow-xl rounded-lg py-1.5 z-50 min-w-[180px]"
:style="{ top: `${contextMenuPosition.y}px`, left: `${contextMenuPosition.x}px` }"
@click.stop
>
<!-- 防止点击菜单内部关闭菜单 -->
<ul class="list-none p-0 m-0">
<li class="group px-4 py-1.5 cursor-pointer flex items-center text-foreground hover:bg-header text-sm transition-colors duration-150" @click="handleMenuAction('add')">
<i class="fas fa-plus mr-3 w-4 text-center text-text-secondary group-hover:text-foreground"></i>
<li class="group px-4 py-1.5 cursor-pointer flex items-center text-foreground hover:bg-primary/10 hover:text-primary text-sm transition-colors duration-150 rounded-md mx-1" @click="handleMenuAction('add')">
<i class="fas fa-plus mr-3 w-4 text-center text-text-secondary group-hover:text-primary"></i>
<span>{{ t('connections.addConnection') }}</span>
</li>
<li v-if="contextTargetConnection" class="group px-4 py-1.5 cursor-pointer flex items-center text-foreground hover:bg-header text-sm transition-colors duration-150" @click="handleMenuAction('edit')">
<i class="fas fa-edit mr-3 w-4 text-center text-text-secondary group-hover:text-foreground"></i>
<li v-if="contextTargetConnection" class="group px-4 py-1.5 cursor-pointer flex items-center text-foreground hover:bg-primary/10 hover:text-primary text-sm transition-colors duration-150 rounded-md mx-1" @click="handleMenuAction('edit')">
<i class="fas fa-edit mr-3 w-4 text-center text-text-secondary group-hover:text-primary"></i>
<span>{{ t('connections.actions.edit') }}</span>
</li>
<li v-if="contextTargetConnection" class="group px-4 py-1.5 cursor-pointer flex items-center text-red-600 hover:bg-red-500/10 text-sm transition-colors duration-150" @click="handleMenuAction('delete')">
<i class="fas fa-trash-alt mr-3 w-4 text-center text-red-500 group-hover:text-red-600"></i>
<li v-if="contextTargetConnection" class="group px-4 py-1.5 cursor-pointer flex items-center text-error hover:bg-error/10 text-sm transition-colors duration-150 rounded-md mx-1" @click="handleMenuAction('delete')">
<i class="fas fa-trash-alt mr-3 w-4 text-center text-error/80 group-hover:text-error"></i>
<span>{{ t('connections.actions.delete') }}</span>
</li>
</ul>
+16 -1
View File
@@ -107,7 +107,6 @@ 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);
}
/* 全局分割线样式 */
hr {
border: none;
@@ -193,3 +192,19 @@ input:focus, textarea:focus, select:focus {
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 */
}
@@ -2,8 +2,8 @@
<div class="flex flex-col h-full overflow-hidden bg-background">
<!-- Container for controls and list -->
<div class="flex flex-col flex-grow overflow-hidden bg-background">
<!-- Controls embedded within the container -->
<div class="flex items-stretch p-2 border-b border-border flex-shrink-0 gap-1 bg-header">
<!-- Controls Area -->
<div class="flex items-center p-2 flex-shrink-0 gap-2 bg-background"> <!-- Reduced padding p-3 to p-2 -->
<input
type="text"
:placeholder="$t('commandHistory.searchPlaceholder', '搜索历史记录...')"
@@ -12,41 +12,49 @@
@input="updateSearchTerm($event)"
@keydown="handleSearchInputKeydown"
ref="searchInputRef"
class="flex-grow min-w-[8px] px-2 py-1 border border-border rounded-sm bg-background text-foreground text-sm focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out"
/>
<button @click="confirmClearAll" class="w-8 py-1 border border-border rounded-sm text-text-secondary hover:bg-error/10 hover:text-error hover:border-error/50 transition-colors duration-150 flex-shrink-0 flex" :title="$t('commandHistory.clear', '清空')">
<i class="fas fa-trash-alt text-sm m-auto"></i>
<!-- Clear Button -->
<button @click="confirmClearAll" class="w-8 h-8 border border-border/50 rounded-lg text-text-secondary hover:bg-error/10 hover:text-error hover:border-error/50 transition-colors duration-150 flex-shrink-0 flex items-center justify-center" :title="$t('commandHistory.clear', '清空')"> <!-- Use w-8 h-8 -->
<i class="fas fa-trash-alt text-base"></i>
</button>
</div>
<!-- List Area -->
<div class="flex-grow overflow-y-auto">
<ul ref="historyListRef" v-if="filteredHistory.length > 0" class="list-none p-0 m-0">
<div class="flex-grow overflow-y-auto p-2">
<!-- Loading State -->
<div v-if="isLoading" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
<i class="fas fa-spinner fa-spin text-xl mb-2"></i>
<p>{{ $t('commandHistory.loading', '加载中...') }}</p>
</div>
<!-- Empty State -->
<div v-else-if="filteredHistory.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
<i class="fas fa-history text-xl mb-2"></i>
<p>{{ $t('commandHistory.empty', '没有历史记录') }}</p>
</div>
<!-- History List -->
<ul ref="historyListRef" v-else class="list-none p-0 m-0">
<li
v-for="(entry, index) in filteredHistory"
:key="entry.id"
class="group flex justify-between items-center px-3 py-2 cursor-pointer border-b border-border last:border-b-0 hover:bg-header/50 transition-colors duration-150"
:class="{ 'bg-primary/10 text-primary': index === storeSelectedIndex }"
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
:class="{ 'bg-primary/20 text-white font-medium': index === storeSelectedIndex }"
@click="executeCommand(entry.command)"
>
<span class="truncate mr-2 flex-grow font-mono text-sm text-foreground" :class="{'text-primary': index === storeSelectedIndex}">{{ entry.command }}</span>
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity duration-150">
<button @click.stop="copyCommand(entry.command)" class="p-1 text-text-secondary hover:text-primary transition-colors duration-150" :class="{'text-primary': index === storeSelectedIndex}" :title="$t('commandHistory.copy', '复制')">
<i class="fas fa-copy text-xs"></i>
<!-- Command Text -->
<span class="truncate mr-2 flex-grow font-mono text-sm" :class="{'text-white': index === storeSelectedIndex, 'text-foreground': index !== storeSelectedIndex}">{{ entry.command }}</span>
<!-- Actions (Show on Hover) -->
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
<!-- Copy Button -->
<button @click.stop="copyCommand(entry.command)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150" :class="{'text-white hover:bg-white/20': index === storeSelectedIndex, 'text-text-secondary hover:text-primary': index !== storeSelectedIndex}" :title="$t('commandHistory.copy', '复制')">
<i class="fas fa-copy text-sm"></i>
</button>
<button @click.stop="deleteSingleCommand(entry.id)" class="ml-1 p-1 text-text-secondary hover:text-error transition-colors duration-150" :class="{'text-primary': index === storeSelectedIndex}" :title="$t('commandHistory.delete', '删除')">
<i class="fas fa-times text-xs"></i>
<!-- Delete Button -->
<button @click.stop="deleteSingleCommand(entry.id)" class="ml-1 p-1.5 rounded hover:bg-black/10 transition-colors duration-150" :class="{'text-white hover:bg-white/20': index === storeSelectedIndex, 'text-text-secondary hover:text-error': index !== storeSelectedIndex}" :title="$t('commandHistory.delete', '删除')">
<i class="fas fa-times text-sm"></i>
</button>
</div>
</li>
</ul>
<div v-else-if="isLoading" class="p-6 text-center text-text-secondary text-sm">
{{ $t('commandHistory.loading', '加载中...') }}
</div>
<div v-else class="p-6 text-center text-text-secondary text-sm italic">
{{ $t('commandHistory.empty', '没有历史记录') }}
</div>
</div>
</div>
</div>
@@ -2,8 +2,8 @@
<div class="flex flex-col h-full overflow-hidden bg-background">
<!-- Container for controls and list -->
<div class="flex flex-col flex-grow overflow-hidden bg-background">
<!-- Controls embedded within the container -->
<div class="flex items-stretch p-2 border-b border-border flex-shrink-0 gap-1 bg-header">
<!-- Controls Area -->
<div class="flex items-center p-2 flex-shrink-0 gap-2 bg-background"> <!-- Reduced padding p-3 to p-2 -->
<input
type="text"
:placeholder="$t('quickCommands.searchPlaceholder', '搜索名称或指令...')"
@@ -12,48 +12,61 @@
@input="updateSearchTerm($event)"
@keydown="handleSearchInputKeydown"
ref="searchInputRef"
class="flex-grow min-w-[8px] px-2 py-1 border border-border rounded-sm bg-background text-foreground text-sm focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
class="flex-grow min-w-0 px-4 py-1.5 border border-border/50 rounded-lg bg-input text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition duration-150 ease-in-out"
/>
<button @click="toggleSortBy" class="w-8 py-1 border border-border rounded-sm text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150 flex-shrink-0 flex" :title="sortButtonTitle">
<i :class="[sortButtonIcon, 'text-sm', 'm-auto']"></i>
<!-- Sort Button -->
<button @click="toggleSortBy" class="w-8 h-8 border border-border/50 rounded-lg text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150 flex-shrink-0 flex items-center justify-center" :title="sortButtonTitle"> <!-- Use w-8 h-8 -->
<i :class="[sortButtonIcon, 'text-base']"></i>
</button>
<button @click="openAddForm" class="w-8 py-1 border border-border rounded-sm text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150 flex-shrink-0 flex" :title="$t('quickCommands.add', '添加快捷指令')">
<i class="fas fa-plus text-sm m-auto"></i>
<!-- Add Button -->
<button @click="openAddForm" class="w-8 h-8 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary flex-shrink-0 flex items-center justify-center" :title="$t('quickCommands.add', '添加快捷指令')"> <!-- Use w-8 h-8 -->
<i class="fas fa-plus text-base"></i>
</button>
</div>
<!-- List Area -->
<div class="flex-grow overflow-y-auto">
<ul v-if="filteredAndSortedCommands.length > 0" class="list-none p-0 m-0" ref="commandListRef">
<div class="flex-grow overflow-y-auto p-2">
<!-- Loading State -->
<div v-if="isLoading" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
<i class="fas fa-spinner fa-spin text-xl mb-2"></i>
<p>{{ t('common.loading', '加载中...') }}</p>
</div>
<!-- Empty State -->
<div v-else-if="filteredAndSortedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
<i class="fas fa-bolt text-xl mb-2"></i>
<p class="mb-3">{{ $t('quickCommands.empty', '没有快捷指令。') }}</p>
<button @click="openAddForm" class="px-4 py-2 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
{{ $t('quickCommands.addFirst', '创建第一个快捷指令') }}
</button>
</div>
<!-- Command List -->
<ul v-else class="list-none p-0 m-0" ref="commandListRef">
<li
v-for="(cmd, index) in filteredAndSortedCommands"
:key="cmd.id"
class="group flex justify-between items-center px-3 py-2 cursor-pointer border-b border-border last:border-b-0 hover:bg-header/50 transition-colors duration-150"
:class="{ 'bg-primary/10 text-primary': index === storeSelectedIndex }"
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
:class="{ 'bg-primary/20 text-white font-medium': index === storeSelectedIndex }"
@click="executeCommand(cmd)"
>
<!-- Command Info -->
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
<span v-if="cmd.name" class="font-medium text-foreground text-sm truncate mb-0.5">{{ cmd.name }}</span>
<span class="text-xs text-text-secondary truncate font-mono" :class="{ 'text-sm text-foreground': !cmd.name }">{{ cmd.command }}</span>
<span v-if="cmd.name" class="font-medium text-sm truncate mb-0.5" :class="{'text-white': index === storeSelectedIndex, 'text-foreground': index !== storeSelectedIndex}">{{ cmd.name }}</span>
<span class="text-xs truncate font-mono" :class="{ 'text-sm': !cmd.name, 'text-white/80': index === storeSelectedIndex, 'text-text-secondary': index !== storeSelectedIndex }">{{ cmd.command }}</span>
</div>
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity duration-150">
<span class="text-xs text-text-secondary bg-border px-1.5 py-0.5 rounded mr-1" :class="{'text-primary bg-primary/20': index === storeSelectedIndex}" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
<button @click.stop="openEditForm(cmd)" class="p-1 text-text-secondary hover:text-primary transition-colors duration-150" :class="{'text-primary': index === storeSelectedIndex}" :title="$t('common.edit', '编辑')">
<i class="fas fa-edit text-xs"></i>
<!-- Actions (Show on Hover) -->
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
<!-- Usage Count -->
<span class="text-xs bg-border px-1.5 py-0.5 rounded mr-2" :class="{'text-white/80 bg-white/20': index === storeSelectedIndex, 'text-text-secondary': index !== storeSelectedIndex}" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
<!-- Edit Button -->
<button @click.stop="openEditForm(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150" :class="{'text-white hover:bg-white/20': index === storeSelectedIndex, 'text-text-secondary hover:text-primary': index !== storeSelectedIndex}" :title="$t('common.edit', '编辑')">
<i class="fas fa-edit text-sm"></i>
</button>
<button @click.stop="confirmDelete(cmd)" class="p-1 text-text-secondary hover:text-error transition-colors duration-150" :class="{'text-primary': index === storeSelectedIndex}" :title="$t('common.delete', '删除')">
<i class="fas fa-times text-xs"></i>
<!-- Delete Button -->
<button @click.stop="confirmDelete(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150" :class="{'text-white hover:bg-white/20': index === storeSelectedIndex, 'text-text-secondary hover:text-error': index !== storeSelectedIndex}" :title="$t('common.delete', '删除')">
<i class="fas fa-times text-sm"></i>
</button>
</div>
</li>
</ul>
<div v-else-if="isLoading" class="p-6 text-center text-text-secondary text-sm">
{{ t('common.loading', '加载中...') }}
</div>
<div v-else class="p-6 text-center text-text-secondary text-sm italic">
{{ $t('quickCommands.empty', '没有快捷指令。点击“添加”按钮创建一个吧!') }}
</div>
</div>
</div>