update
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, onMounted, onBeforeUnmount, defineExpose } from 'vue';
|
||||
import { ref, watch, nextTick, onMounted, onBeforeUnmount, defineExpose, computed } from 'vue'; // Import computed
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia'; // Import storeToRefs
|
||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // 导入 Store
|
||||
import { useSettingsStore } from '../stores/settings.store'; // NEW: Import settings store
|
||||
import { useQuickCommandsStore } from '../stores/quickCommands.store'; // NEW: Import quick commands store
|
||||
import { useCommandHistoryStore } from '../stores/commandHistory.store'; // NEW: Import command history store
|
||||
// 假设你有一个图标库,例如 unplugin-icons 或类似库
|
||||
// import SearchIcon from '~icons/mdi/magnify';
|
||||
// import ArrowUpIcon from '~icons/mdi/arrow-up';
|
||||
@@ -11,6 +15,18 @@ import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // 导入
|
||||
const emit = defineEmits(['send-command', 'search', 'find-next', 'find-previous', 'close-search']); // 移除 open-focus-switcher-config 事件
|
||||
const { t } = useI18n();
|
||||
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化 Store +++
|
||||
const settingsStore = useSettingsStore(); // NEW: Instantiate settings store
|
||||
const quickCommandsStore = useQuickCommandsStore(); // NEW: Instantiate quick commands store
|
||||
const commandHistoryStore = useCommandHistoryStore(); // NEW: Instantiate command history store
|
||||
|
||||
// Get reactive setting from store
|
||||
const { commandInputSyncTarget } = storeToRefs(settingsStore);
|
||||
// Get reactive state and actions from quick commands store
|
||||
const { selectedIndex: quickCommandsSelectedIndex, filteredAndSortedCommands: quickCommandsFiltered } = storeToRefs(quickCommandsStore);
|
||||
const { resetSelection: resetQuickCommandsSelection } = quickCommandsStore;
|
||||
// Get reactive state and actions from command history store
|
||||
const { selectedIndex: historySelectedIndex, filteredHistory: historyFiltered } = storeToRefs(commandHistoryStore);
|
||||
const { resetSelection: resetHistorySelection } = commandHistoryStore;
|
||||
|
||||
// Props definition is now empty as search results are no longer handled here
|
||||
const props = defineProps<{
|
||||
@@ -61,6 +77,17 @@ watch(searchTerm, (newValue) => {
|
||||
}
|
||||
});
|
||||
|
||||
// NEW: Watch commandInput and sync searchTerm based on settings
|
||||
watch(commandInput, (newValue) => {
|
||||
const target = commandInputSyncTarget.value;
|
||||
if (target === 'quickCommands') {
|
||||
quickCommandsStore.setSearchTerm(newValue);
|
||||
} else if (target === 'commandHistory') {
|
||||
commandHistoryStore.setSearchTerm(newValue);
|
||||
}
|
||||
// If target is 'none', do nothing
|
||||
});
|
||||
|
||||
// 可以在这里添加一个 ref 用于聚焦搜索框
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null);
|
||||
const commandInputRef = ref<HTMLInputElement | null>(null); // Ref for command input
|
||||
@@ -74,9 +101,68 @@ const handleCommandInputKeydown = (event: KeyboardEvent) => {
|
||||
nextTick(() => {
|
||||
searchInputRef.value?.focus();
|
||||
});
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
const target = commandInputSyncTarget.value;
|
||||
if (target === 'quickCommands') {
|
||||
event.preventDefault();
|
||||
quickCommandsStore.selectPreviousCommand();
|
||||
} else if (target === 'commandHistory') {
|
||||
event.preventDefault();
|
||||
commandHistoryStore.selectPreviousCommand();
|
||||
}
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
const target = commandInputSyncTarget.value;
|
||||
if (target === 'quickCommands') {
|
||||
event.preventDefault();
|
||||
quickCommandsStore.selectNextCommand();
|
||||
} else if (target === 'commandHistory') {
|
||||
event.preventDefault();
|
||||
commandHistoryStore.selectNextCommand();
|
||||
}
|
||||
} else if (event.altKey && event.key === 'Enter') {
|
||||
const target = commandInputSyncTarget.value;
|
||||
let selectedCommand: string | undefined;
|
||||
|
||||
if (target === 'quickCommands') {
|
||||
const index = quickCommandsSelectedIndex.value;
|
||||
const commands = quickCommandsFiltered.value;
|
||||
if (index >= 0 && index < commands.length) {
|
||||
selectedCommand = commands[index].command;
|
||||
resetQuickCommandsSelection(); // Reset selection after execution
|
||||
}
|
||||
} else if (target === 'commandHistory') {
|
||||
const index = historySelectedIndex.value;
|
||||
const history = historyFiltered.value;
|
||||
if (index >= 0 && index < history.length) {
|
||||
selectedCommand = history[index].command;
|
||||
resetHistorySelection(); // Reset selection after execution
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedCommand !== undefined) {
|
||||
event.preventDefault();
|
||||
console.log(`[CommandInputBar] Alt+Enter detected. Sending selected command: ${selectedCommand}`);
|
||||
emit('send-command', selectedCommand + '\n');
|
||||
commandInput.value = ''; // Clear input after sending selected command
|
||||
}
|
||||
} else if (!event.altKey && event.key === 'Enter') {
|
||||
// Handle regular Enter key press - send current input
|
||||
event.preventDefault(); // Prevent default if needed, e.g., form submission
|
||||
sendCommand(); // Call the existing sendCommand function
|
||||
}
|
||||
};
|
||||
|
||||
// NEW: Handle blur event on command input
|
||||
const handleCommandInputBlur = () => {
|
||||
// Reset selection in the target store when input loses focus
|
||||
const target = commandInputSyncTarget.value;
|
||||
if (target === 'quickCommands') {
|
||||
resetQuickCommandsSelection();
|
||||
} else if (target === 'commandHistory') {
|
||||
resetHistorySelection();
|
||||
}
|
||||
};
|
||||
|
||||
// +++ 监听 Store 中的触发器以激活终端搜索 +++
|
||||
watch(() => focusSwitcherStore.activateTerminalSearchTrigger, () => {
|
||||
if (focusSwitcherStore.activateTerminalSearchTrigger > 0 && !isSearching.value) {
|
||||
@@ -151,11 +237,12 @@ onBeforeUnmount(() => {
|
||||
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-basis-3/4': isSearching, 'flex-basis-full': !isSearching }"
|
||||
:class="{ 'basis-3/4': isSearching, 'basis-full': !isSearching }"
|
||||
ref="commandInputRef"
|
||||
data-focus-id="commandInput"
|
||||
@keydown.enter="sendCommand"
|
||||
|
||||
@keydown="handleCommandInputKeydown"
|
||||
@blur="handleCommandInputBlur"
|
||||
/>
|
||||
|
||||
<!-- Search Input (Conditional rendering with v-show for transition) -->
|
||||
@@ -164,7 +251,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 flex-basis-1/4 ml-1.5"
|
||||
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"
|
||||
data-focus-id="terminalSearch"
|
||||
@keydown.enter.prevent="findNext"
|
||||
@keydown.shift.enter.prevent="findPrevious"
|
||||
|
||||
@@ -21,6 +21,7 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => {
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
const selectedIndex = ref<number>(-1); // NEW: Index of the selected command in the filtered list
|
||||
|
||||
// --- Getters ---
|
||||
|
||||
@@ -37,6 +38,26 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => {
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
// NEW: Action to select the next command in the filtered list
|
||||
const selectNextCommand = () => {
|
||||
const history = filteredHistory.value;
|
||||
if (history.length === 0) {
|
||||
selectedIndex.value = -1;
|
||||
return;
|
||||
}
|
||||
selectedIndex.value = (selectedIndex.value + 1) % history.length;
|
||||
};
|
||||
|
||||
// NEW: Action to select the previous command in the filtered list
|
||||
const selectPreviousCommand = () => {
|
||||
const history = filteredHistory.value;
|
||||
if (history.length === 0) {
|
||||
selectedIndex.value = -1;
|
||||
return;
|
||||
}
|
||||
selectedIndex.value = (selectedIndex.value - 1 + history.length) % history.length;
|
||||
};
|
||||
|
||||
// 从后端获取历史记录
|
||||
const fetchHistory = async () => {
|
||||
isLoading.value = true;
|
||||
@@ -108,6 +129,12 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => {
|
||||
// 设置搜索词
|
||||
const setSearchTerm = (term: string) => {
|
||||
searchTerm.value = term;
|
||||
selectedIndex.value = -1; // Reset selection when search term changes
|
||||
};
|
||||
|
||||
// NEW: Action to reset the selection (Moved before return)
|
||||
const resetSelection = () => {
|
||||
selectedIndex.value = -1;
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -116,10 +143,18 @@ export const useCommandHistoryStore = defineStore('commandHistory', () => {
|
||||
isLoading,
|
||||
error,
|
||||
filteredHistory,
|
||||
selectedIndex, // NEW: Expose selected index
|
||||
fetchHistory,
|
||||
addCommand, // 导出 addCommand
|
||||
deleteCommand,
|
||||
clearAllHistory,
|
||||
setSearchTerm,
|
||||
selectNextCommand, // NEW: Expose action
|
||||
selectPreviousCommand, // NEW: Expose action
|
||||
resetSelection, // Ensure resetSelection is exported
|
||||
};
|
||||
|
||||
// REMOVED resetSelection definition from here
|
||||
|
||||
// REMOVED duplicate return block
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
const selectedIndex = ref<number>(-1); // NEW: Index of the selected command in the filtered list
|
||||
|
||||
// --- Getters ---
|
||||
|
||||
@@ -53,6 +54,26 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
// NEW: Action to select the next command in the filtered list
|
||||
const selectNextCommand = () => {
|
||||
const commands = filteredAndSortedCommands.value;
|
||||
if (commands.length === 0) {
|
||||
selectedIndex.value = -1;
|
||||
return;
|
||||
}
|
||||
selectedIndex.value = (selectedIndex.value + 1) % commands.length;
|
||||
};
|
||||
|
||||
// NEW: Action to select the previous command in the filtered list
|
||||
const selectPreviousCommand = () => {
|
||||
const commands = filteredAndSortedCommands.value;
|
||||
if (commands.length === 0) {
|
||||
selectedIndex.value = -1;
|
||||
return;
|
||||
}
|
||||
selectedIndex.value = (selectedIndex.value - 1 + commands.length) % commands.length;
|
||||
};
|
||||
|
||||
// 从后端获取快捷指令 (带排序)
|
||||
const fetchQuickCommands = async () => {
|
||||
isLoading.value = true;
|
||||
@@ -141,6 +162,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
// 设置搜索词
|
||||
const setSearchTerm = (term: string) => {
|
||||
searchTerm.value = term;
|
||||
selectedIndex.value = -1; // Reset selection when search term changes
|
||||
};
|
||||
|
||||
// 设置排序方式并重新获取数据
|
||||
@@ -151,6 +173,13 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// NEW: Action to reset the selection
|
||||
const resetSelection = () => {
|
||||
selectedIndex.value = -1;
|
||||
};
|
||||
|
||||
// Removed duplicate resetSelection definition
|
||||
|
||||
return {
|
||||
quickCommandsList,
|
||||
searchTerm,
|
||||
@@ -158,6 +187,7 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
isLoading,
|
||||
error,
|
||||
filteredAndSortedCommands, // 使用计算属性
|
||||
selectedIndex, // NEW: Expose selected index
|
||||
fetchQuickCommands,
|
||||
addQuickCommand,
|
||||
updateQuickCommand,
|
||||
@@ -165,5 +195,8 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
incrementUsage,
|
||||
setSearchTerm,
|
||||
setSortBy,
|
||||
selectNextCommand, // NEW: Expose action
|
||||
selectPreviousCommand, // NEW: Expose action
|
||||
resetSelection, // Ensure resetSelection is exported
|
||||
};
|
||||
});
|
||||
|
||||
@@ -42,6 +42,8 @@ interface SettingsState {
|
||||
sidebarPaneWidths?: string; // NEW: 存储各侧边栏组件宽度的 JSON 字符串
|
||||
fileManagerRowSizeMultiplier?: string; // NEW: 文件管理器行大小乘数 (e.g., '1.0')
|
||||
fileManagerColWidths?: string; // NEW: 文件管理器列宽 JSON 字符串 (e.g., '{"name": 300, "size": 100}')
|
||||
fileManagerColWidths?: string; // NEW: 文件管理器列宽 JSON 字符串 (e.g., '{"name": 300, "size": 100}')
|
||||
commandInputSyncTarget?: 'quickCommands' | 'commandHistory' | 'none'; // NEW: 命令输入同步目标
|
||||
// Add other general settings keys here as needed
|
||||
[key: string]: string | undefined; // Allow other string settings
|
||||
}
|
||||
@@ -198,6 +200,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
// await updateSetting('fileManagerColWidths', finalFmWidthsString);
|
||||
// }
|
||||
|
||||
// NEW: Command Input Sync Target default
|
||||
if (settings.value.commandInputSyncTarget === undefined) {
|
||||
settings.value.commandInputSyncTarget = 'none'; // 默认不同步
|
||||
}
|
||||
|
||||
// --- 语言设置 ---
|
||||
const langFromSettings = settings.value.language;
|
||||
console.log(`[SettingsStore] Language from fetched settings: ${langFromSettings}`); // <-- 添加日志
|
||||
@@ -251,7 +258,8 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
||||
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
|
||||
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
|
||||
'fileManagerColWidths' // +++ 添加文件管理器列宽键 +++
|
||||
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
|
||||
'commandInputSyncTarget' // +++ 添加命令输入同步目标键 +++
|
||||
];
|
||||
if (!allowedKeys.includes(key)) {
|
||||
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
|
||||
@@ -289,7 +297,8 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
||||
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
|
||||
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
|
||||
'fileManagerColWidths' // +++ 添加文件管理器列宽键 +++
|
||||
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
|
||||
'commandInputSyncTarget' // +++ 添加命令输入同步目标键 +++
|
||||
];
|
||||
const filteredUpdates: Partial<SettingsState> = {};
|
||||
let languageUpdate: 'en' | 'zh' | undefined = undefined;
|
||||
@@ -491,6 +500,15 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
return parsedFileManagerColWidths.value;
|
||||
});
|
||||
|
||||
// NEW: Getter for command input sync target
|
||||
const commandInputSyncTarget = computed(() => {
|
||||
const target = settings.value.commandInputSyncTarget;
|
||||
if (target === 'quickCommands' || target === 'commandHistory') {
|
||||
return target;
|
||||
}
|
||||
return 'none'; // Default to 'none' if invalid or not set
|
||||
});
|
||||
|
||||
// --- CAPTCHA Getters (Public Only) ---
|
||||
const isCaptchaEnabled = computed(() => captchaSettings.value?.enabled ?? false);
|
||||
const captchaProvider = computed(() => captchaSettings.value?.provider ?? 'none');
|
||||
@@ -527,5 +545,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
updateMultipleSettings,
|
||||
updateSidebarPaneWidth, // +++ 暴露更新特定面板宽度的 action +++
|
||||
updateFileManagerLayoutSettings, // +++ 暴露更新文件管理器布局的 action +++
|
||||
commandInputSyncTarget, // +++ 暴露命令输入同步目标 getter +++
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:value="searchTerm"
|
||||
data-focus-id="commandHistorySearch"
|
||||
@input="updateSearchTerm($event)"
|
||||
@keydown="handleKeydown"
|
||||
@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"
|
||||
/>
|
||||
@@ -25,17 +25,17 @@
|
||||
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 === selectedIndex }"
|
||||
@mouseover="hoveredItemId = entry.id; selectedIndex = index"
|
||||
@mouseleave="hoveredItemId = null; selectedIndex = -1"
|
||||
:class="{ 'bg-primary/10 text-primary': index === storeSelectedIndex }"
|
||||
|
||||
|
||||
@click="executeCommand(entry.command)"
|
||||
>
|
||||
<span class="truncate mr-2 flex-grow font-mono text-sm text-foreground" :class="{'text-primary': index === selectedIndex}">{{ entry.command }}</span>
|
||||
<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 === selectedIndex}" :title="$t('commandHistory.copy', '复制')">
|
||||
<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>
|
||||
</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 === selectedIndex}" :title="$t('commandHistory.delete', '删除')">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -53,7 +53,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, computed, nextTick, defineExpose } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, computed, nextTick, defineExpose, watch } from 'vue'; // Import watch
|
||||
import { storeToRefs } from 'pinia'; // Import storeToRefs
|
||||
import { useCommandHistoryStore, CommandHistoryEntryFE } from '../stores/commandHistory.store';
|
||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -65,7 +66,7 @@ const uiNotificationsStore = useUiNotificationsStore();
|
||||
const { t } = useI18n();
|
||||
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
||||
const hoveredItemId = ref<number | null>(null);
|
||||
const selectedIndex = ref<number>(-1); // -1 表示没有选中
|
||||
// const selectedIndex = ref<number>(-1); // REMOVED: Use store's selectedIndex
|
||||
const historyListRef = ref<HTMLUListElement | null>(null); // Ref for the history list UL
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null); // +++ Ref for the search input +++
|
||||
let unregisterFocus: (() => void) | null = null; // +++ 保存注销函数 +++
|
||||
@@ -75,6 +76,7 @@ const searchTerm = computed(() => commandHistoryStore.searchTerm);
|
||||
// 使用 store 的 filteredHistory getter
|
||||
const filteredHistory = computed(() => commandHistoryStore.filteredHistory);
|
||||
const isLoading = computed(() => commandHistoryStore.isLoading);
|
||||
const { selectedIndex: storeSelectedIndex } = storeToRefs(commandHistoryStore); // Get selectedIndex reactively
|
||||
|
||||
// --- 事件定义 ---
|
||||
// 定义组件发出的事件
|
||||
@@ -108,16 +110,16 @@ onBeforeUnmount(() => {
|
||||
const updateSearchTerm = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
commandHistoryStore.setSearchTerm(target.value);
|
||||
selectedIndex.value = -1; // Reset selection when search term changes
|
||||
// selectedIndex.value = -1; // REMOVED: Store handles resetting index
|
||||
};
|
||||
|
||||
// 滚动到选中的项目
|
||||
const scrollToSelected = async () => {
|
||||
const scrollToSelected = async (index: number) => { // Accept index as argument
|
||||
await nextTick(); // 等待 DOM 更新
|
||||
if (selectedIndex.value < 0 || !historyListRef.value) return;
|
||||
if (index < 0 || !historyListRef.value) return;
|
||||
|
||||
const listElement = historyListRef.value;
|
||||
const selectedItem = listElement.children[selectedIndex.value] as HTMLLIElement;
|
||||
const selectedItem = listElement.children[index] as HTMLLIElement;
|
||||
|
||||
if (selectedItem) {
|
||||
const listRect = listElement.getBoundingClientRect();
|
||||
@@ -133,32 +135,36 @@ const scrollToSelected = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
// Watch for changes in the store's selectedIndex and scroll
|
||||
watch(storeSelectedIndex, (newIndex) => {
|
||||
scrollToSelected(newIndex);
|
||||
});
|
||||
|
||||
// Renamed function to avoid conflict if needed, and added logic
|
||||
const handleSearchInputKeydown = (event: KeyboardEvent) => {
|
||||
const history = filteredHistory.value;
|
||||
if (!history.length) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
selectedIndex.value = (selectedIndex.value + 1) % history.length;
|
||||
scrollToSelected();
|
||||
commandHistoryStore.selectNextCommand(); // Use store action
|
||||
// scrollToSelected is handled by watcher
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
selectedIndex.value = (selectedIndex.value - 1 + history.length) % history.length;
|
||||
scrollToSelected();
|
||||
commandHistoryStore.selectPreviousCommand(); // Use store action
|
||||
// scrollToSelected is handled by watcher
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
if (selectedIndex.value >= 0 && selectedIndex.value < history.length) {
|
||||
executeCommand(history[selectedIndex.value].command);
|
||||
if (storeSelectedIndex.value >= 0 && storeSelectedIndex.value < history.length) {
|
||||
executeCommand(history[storeSelectedIndex.value].command);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 确认清空所有历史记录
|
||||
const confirmClearAll = () => {
|
||||
if (window.confirm(t('commandHistory.confirmClear', '确定要清空所有历史记录吗?'))) {
|
||||
@@ -186,7 +192,7 @@ const deleteSingleCommand = (id: number) => {
|
||||
const executeCommand = (command: string) => {
|
||||
emit('execute-command', command);
|
||||
// Optionally reset selection after execution
|
||||
// selectedIndex.value = -1;
|
||||
// selectedIndex.value = -1; // REMOVED: Store handles index
|
||||
};
|
||||
|
||||
// +++ 新增:聚焦搜索框的方法 +++
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:value="searchTerm"
|
||||
data-focus-id="quickCommandsSearch"
|
||||
@input="updateSearchTerm($event)"
|
||||
@keydown="handleKeydown"
|
||||
@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"
|
||||
/>
|
||||
@@ -28,9 +28,9 @@
|
||||
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 === selectedIndex }"
|
||||
@mouseover="hoveredItemId = cmd.id; selectedIndex = index"
|
||||
@mouseleave="hoveredItemId = null; selectedIndex = -1"
|
||||
:class="{ 'bg-primary/10 text-primary': index === storeSelectedIndex }"
|
||||
|
||||
|
||||
@click="executeCommand(cmd)"
|
||||
>
|
||||
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
||||
@@ -38,11 +38,11 @@
|
||||
<span class="text-xs text-text-secondary truncate font-mono" :class="{ 'text-sm text-foreground': !cmd.name }">{{ 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 === selectedIndex}" :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 === selectedIndex}" :title="$t('common.edit', '编辑')">
|
||||
<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>
|
||||
</button>
|
||||
<button @click.stop="confirmDelete(cmd)" class="p-1 text-text-secondary hover:text-error transition-colors duration-150" :class="{'text-primary': index === selectedIndex}" :title="$t('common.delete', '删除')">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -67,7 +67,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, computed, nextTick, defineExpose } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, computed, nextTick, defineExpose, watch } from 'vue'; // Import watch
|
||||
import { storeToRefs } from 'pinia'; // Import storeToRefs
|
||||
import { useQuickCommandsStore, type QuickCommandFE, type QuickCommandSortByType } from '../stores/quickCommands.store';
|
||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -82,7 +83,7 @@ const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换
|
||||
const hoveredItemId = ref<number | null>(null);
|
||||
const isFormVisible = ref(false);
|
||||
const commandToEdit = ref<QuickCommandFE | null>(null);
|
||||
const selectedIndex = ref<number>(-1); // -1 表示没有选中
|
||||
// const selectedIndex = ref<number>(-1); // REMOVED: Use store's selectedIndex
|
||||
const commandListRef = ref<HTMLUListElement | null>(null); // Ref for the command list UL
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null); // +++ Ref for the search input +++
|
||||
let unregisterFocus: (() => void) | null = null; // +++ 保存注销函数 +++
|
||||
@@ -92,6 +93,7 @@ const searchTerm = computed(() => quickCommandsStore.searchTerm);
|
||||
const sortBy = computed(() => quickCommandsStore.sortBy);
|
||||
const filteredAndSortedCommands = computed(() => quickCommandsStore.filteredAndSortedCommands);
|
||||
const isLoading = computed(() => quickCommandsStore.isLoading);
|
||||
const { selectedIndex: storeSelectedIndex } = storeToRefs(quickCommandsStore); // Get selectedIndex reactively
|
||||
|
||||
// --- 事件定义 ---
|
||||
const emit = defineEmits<{
|
||||
@@ -120,16 +122,16 @@ onBeforeUnmount(() => {
|
||||
const updateSearchTerm = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
quickCommandsStore.setSearchTerm(target.value);
|
||||
selectedIndex.value = -1; // Reset selection when search term changes
|
||||
// selectedIndex.value = -1; // REMOVED: Store handles resetting index
|
||||
};
|
||||
|
||||
// 滚动到选中的项目
|
||||
const scrollToSelected = async () => {
|
||||
const scrollToSelected = async (index: number) => { // Accept index as argument
|
||||
await nextTick(); // 等待 DOM 更新
|
||||
if (selectedIndex.value < 0 || !commandListRef.value) return;
|
||||
if (index < 0 || !commandListRef.value) return;
|
||||
|
||||
const listElement = commandListRef.value;
|
||||
const selectedItem = listElement.children[selectedIndex.value] as HTMLLIElement;
|
||||
const selectedItem = listElement.children[index] as HTMLLIElement;
|
||||
|
||||
if (selectedItem) {
|
||||
const listRect = listElement.getBoundingClientRect();
|
||||
@@ -147,33 +149,36 @@ const scrollToSelected = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes in the store's selectedIndex and scroll
|
||||
watch(storeSelectedIndex, (newIndex) => {
|
||||
scrollToSelected(newIndex);
|
||||
});
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
// Renamed function to avoid conflict if needed, and added logic
|
||||
const handleSearchInputKeydown = (event: KeyboardEvent) => {
|
||||
const commands = filteredAndSortedCommands.value;
|
||||
if (!commands.length) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
selectedIndex.value = (selectedIndex.value + 1) % commands.length;
|
||||
scrollToSelected();
|
||||
quickCommandsStore.selectNextCommand(); // Use store action
|
||||
// scrollToSelected is handled by watcher
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
selectedIndex.value = (selectedIndex.value - 1 + commands.length) % commands.length;
|
||||
scrollToSelected();
|
||||
quickCommandsStore.selectPreviousCommand(); // Use store action
|
||||
// scrollToSelected is handled by watcher
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
if (selectedIndex.value >= 0 && selectedIndex.value < commands.length) {
|
||||
executeCommand(commands[selectedIndex.value]);
|
||||
if (storeSelectedIndex.value >= 0 && storeSelectedIndex.value < commands.length) {
|
||||
executeCommand(commands[storeSelectedIndex.value]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 切换排序方式
|
||||
const toggleSortBy = () => {
|
||||
const newSortBy = sortBy.value === 'name' ? 'usage_count' : 'name';
|
||||
@@ -221,7 +226,7 @@ const executeCommand = (command: QuickCommandFE) => {
|
||||
// 2. 发出执行事件给父组件
|
||||
emit('execute-command', command.command);
|
||||
// Optionally reset selection after execution
|
||||
// selectedIndex.value = -1;
|
||||
// selectedIndex.value = -1; // REMOVED: Store handles index
|
||||
};
|
||||
|
||||
// +++ 新增:聚焦搜索框的方法 +++
|
||||
|
||||
@@ -404,6 +404,31 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<hr class="border-border/50"> <!-- NEW: Separator -->
|
||||
<!-- Command Input Sync Target -->
|
||||
<div class="settings-section-content">
|
||||
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.commandInputSync.title', '命令输入同步') }}</h3>
|
||||
<form @submit.prevent="handleUpdateCommandInputSyncTarget" class="space-y-4">
|
||||
<div>
|
||||
<label for="commandInputSyncTargetSelect" class="block text-sm font-medium text-text-secondary mb-1">{{ $t('settings.commandInputSync.selectLabel', '同步目标') }}</label>
|
||||
<select id="commandInputSyncTargetSelect" v-model="commandInputSyncTargetLocal"
|
||||
class="w-full px-3 py-2 border border-border rounded-md shadow-sm bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary appearance-none bg-no-repeat bg-right pr-8"
|
||||
style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%236c757d\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'M2 5l6 6 6-6\'/%3e%3c/svg%3e'); background-position: right 0.75rem center; background-size: 16px 12px;">
|
||||
<option value="none">{{ $t('settings.commandInputSync.targetNone', '无') }}</option>
|
||||
<option value="quickCommands">{{ $t('settings.commandInputSync.targetQuickCommands', '快捷指令') }}</option>
|
||||
<option value="commandHistory">{{ $t('settings.commandInputSync.targetCommandHistory', '历史命令') }}</option>
|
||||
</select>
|
||||
<p class="text-xs text-text-secondary mt-1">{{ $t('settings.commandInputSync.description', '将命令输入框的内容实时同步到所选面板的搜索框。') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="submit" :disabled="commandInputSyncLoading"
|
||||
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed transition duration-150 ease-in-out text-sm font-medium">
|
||||
{{ commandInputSyncLoading ? $t('common.saving') : $t('common.save') }}
|
||||
</button>
|
||||
<p v-if="commandInputSyncMessage" :class="['text-sm', commandInputSyncSuccess ? 'text-success' : 'text-error']">{{ commandInputSyncMessage }}</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -505,6 +530,7 @@ const {
|
||||
language: storeLanguage,
|
||||
workspaceSidebarPersistentBoolean,
|
||||
captchaSettings, // <-- Import CAPTCHA settings state
|
||||
commandInputSyncTarget, // NEW: Import command input sync target getter
|
||||
} = storeToRefs(settingsStore);
|
||||
|
||||
// --- Local state for forms ---
|
||||
@@ -517,6 +543,7 @@ const blacklistSettingsForm = reactive({ // Renamed to avoid conflict with store
|
||||
});
|
||||
const popupEditorEnabled = ref(true); // 本地状态,用于 v-model
|
||||
const workspaceSidebarPersistentEnabled = ref(false); // 新增:侧边栏固定设置的本地状态
|
||||
const commandInputSyncTargetLocal = ref<'none' | 'quickCommands' | 'commandHistory'>('none'); // NEW: Local state for command input sync target
|
||||
|
||||
// --- Local UI feedback state ---
|
||||
const ipWhitelistLoading = ref(false);
|
||||
@@ -551,6 +578,9 @@ const statusMonitorSuccess = ref(false);
|
||||
const workspaceSidebarPersistentLoading = ref(false); // 新增
|
||||
const workspaceSidebarPersistentMessage = ref(''); // 新增
|
||||
const workspaceSidebarPersistentSuccess = ref(false); // 新增
|
||||
const commandInputSyncLoading = ref(false); // NEW
|
||||
const commandInputSyncMessage = ref(''); // NEW
|
||||
const commandInputSyncSuccess = ref(false); // NEW
|
||||
|
||||
// CAPTCHA Form State
|
||||
const captchaForm = reactive<UpdateCaptchaSettingsDto>({ // Use reactive for the form object
|
||||
@@ -584,6 +614,7 @@ watch(settings, (newSettings, oldSettings) => {
|
||||
dockerExpandDefault.value = dockerDefaultExpandBoolean.value; // 同步 Docker 默认展开状态
|
||||
statusMonitorIntervalLocal.value = statusMonitorIntervalSecondsNumber.value; // 同步状态监控间隔
|
||||
workspaceSidebarPersistentEnabled.value = workspaceSidebarPersistentBoolean.value; // 新增:同步侧边栏固定设置
|
||||
commandInputSyncTargetLocal.value = commandInputSyncTarget.value; // NEW: Sync command input sync target
|
||||
|
||||
}, { deep: true, immediate: true }); // immediate: true to run on initial load
|
||||
|
||||
@@ -736,6 +767,24 @@ const handleUpdateWorkspaceSidebarSetting = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- Command Input Sync Target setting method ---
|
||||
const handleUpdateCommandInputSyncTarget = async () => {
|
||||
commandInputSyncLoading.value = true;
|
||||
commandInputSyncMessage.value = '';
|
||||
commandInputSyncSuccess.value = false;
|
||||
try {
|
||||
await settingsStore.updateSetting('commandInputSyncTarget', commandInputSyncTargetLocal.value);
|
||||
commandInputSyncMessage.value = t('settings.commandInputSync.success.saved', '同步目标已保存'); // 需要添加翻译
|
||||
commandInputSyncSuccess.value = true;
|
||||
} catch (error: any) {
|
||||
console.error('更新命令输入同步目标失败:', error);
|
||||
commandInputSyncMessage.value = error.message || t('settings.commandInputSync.error.saveFailed', '保存同步目标失败'); // 需要添加翻译
|
||||
commandInputSyncSuccess.value = false;
|
||||
} finally {
|
||||
commandInputSyncLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 外观设置 ---
|
||||
const openStyleCustomizer = () => {
|
||||
appearanceStore.toggleStyleCustomizer(true);
|
||||
|
||||
Reference in New Issue
Block a user