This commit is contained in:
Baobhan Sith
2025-04-19 20:55:21 +08:00
parent 890a4284d6
commit 283fa02a18
5 changed files with 197 additions and 54 deletions
+94 -22
View File
@@ -90,6 +90,9 @@ const handleGlobalKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Alt' && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
event.preventDefault(); // 阻止 Alt 键的默认行为 (例如激活菜单栏)
// +++ Log: 打印当前的配置序列 +++
console.log('[App] Current configured sequence in store:', JSON.stringify(focusSwitcherStore.configuredSequence));
const activeElement = document.activeElement as HTMLElement;
let currentFocusId: string | null = null;
@@ -100,29 +103,98 @@ const handleGlobalKeyDown = (event: KeyboardEvent) => {
console.log(`[App] Alt pressed. Current focus ID: ${currentFocusId}`);
// 从 Store 获取下一个目标 ID
const nextFocusId = focusSwitcherStore.getNextFocusTargetId(currentFocusId);
console.log(`[App] Next focus target ID from store: ${nextFocusId}`);
// --- 新的查找逻辑 ---
const sequence = focusSwitcherStore.configuredSequence; // 获取完整的配置顺序
if (sequence.length === 0) {
console.log('[App] No focus sequence configured.');
return; // 没有配置,直接返回
}
if (nextFocusId) {
// 尝试查找下一个目标元素
// 使用 requestAnimationFrame 确保在 DOM 更新后查找
requestAnimationFrame(() => {
const nextElement = document.querySelector(`[data-focus-id="${nextFocusId}"]`) as HTMLElement | null;
let startIndex = 0;
if (currentFocusId) {
const currentIndex = sequence.indexOf(currentFocusId);
if (currentIndex !== -1) {
startIndex = (currentIndex + 1) % sequence.length; // 从当前焦点的下一个开始查找
} else {
console.log(`[App] Current focus ID ${currentFocusId} not found in sequence, starting search from beginning.`);
}
} else {
console.log('[App] No current focus ID found, starting search from beginning.');
}
if (nextElement && isElementVisibleAndFocusable(nextElement)) {
console.log(`[App] Focusing next element:`, nextElement);
nextElement.focus();
// 如果是输入框,可能需要选中内容
if (nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement) {
nextElement.select();
}
} else {
console.log(`[App] Next element with ID ${nextFocusId} not found or not focusable.`);
// 可选:如果找不到或不可聚焦,可以尝试查找配置列表中的再下一个,或者直接忽略
// 这里暂时忽略,避免无限循环
// 循环查找下一个可聚焦的目标 (最多循环一次完整的序列)
let foundFocusable = false;
for (let i = 0; i < sequence.length; i++) {
const nextIndex = (startIndex + i) % sequence.length;
const nextFocusId = sequence[nextIndex];
console.log(`[App] Trying to find element with ID: ${nextFocusId}`);
const nextElement = document.querySelector(`[data-focus-id="${nextFocusId}"]`) as HTMLElement | null;
if (nextElement && isElementVisibleAndFocusable(nextElement)) {
// --- 目标元素找到且可聚焦 ---
console.log(`[App] Found focusable element:`, nextElement);
nextElement.focus();
if (nextElement instanceof HTMLInputElement || nextElement instanceof HTMLTextAreaElement) {
nextElement.select();
}
});
foundFocusable = true;
break; // 找到并聚焦,跳出循环
} else if (nextFocusId === 'fileManagerSearch' || nextFocusId === 'terminalSearch') {
// --- 特殊处理:目标是文件管理器或终端搜索框 ---
const targetElement = document.querySelector(`[data-focus-id="${nextFocusId}"]`) as HTMLElement | null; // 先尝试查找
if (!targetElement || !isElementVisibleAndFocusable(targetElement)) {
// --- 如果元素不存在或不可聚焦,尝试激活 ---
console.log(`[App] Target ${nextFocusId} not found or not focusable. Triggering activation via store...`);
if (nextFocusId === 'fileManagerSearch') {
focusSwitcherStore.triggerFileManagerSearchActivation();
} else { // terminalSearch
focusSwitcherStore.triggerTerminalSearchActivation();
}
// --- 关键:触发激活后,不设置 foundFocusable,也不 break,让循环继续查找下一个 ---
console.log(`[App] Activation triggered for ${nextFocusId}. Continuing search...`);
} else {
// --- 如果元素存在且可聚焦 (理论上不应该进入这里,因为前面的 if 会处理,但作为防御性代码保留) ---
console.log(`[App] Found focusable element after all:`, targetElement);
targetElement.focus();
if (targetElement instanceof HTMLInputElement || targetElement instanceof HTMLTextAreaElement) {
targetElement.select();
}
foundFocusable = true;
break;
}
// --- 旧的逻辑移除 ---
/*
// 使用 setTimeout 等待 DOM 更新后再尝试聚焦
setTimeout(() => {
const targetElement = document.querySelector(`[data-focus-id="${nextFocusId}"]`) as HTMLElement | null;
if (targetElement && isElementVisibleAndFocusable(targetElement)) {
console.log(`[App] Focusing ${nextFocusId} after activation attempt.`);
targetElement.focus();
if (targetElement instanceof HTMLInputElement || targetElement instanceof HTMLTextAreaElement) {
targetElement.select();
}
} else {
console.warn(`[App] Failed to focus ${nextFocusId} even after activation attempt.`);
}
}, 150); // 稍微增加延迟,确保组件有足够时间响应和渲染
foundFocusable = true; // 无论是否成功聚焦,都认为这个目标已被尝试处理
break; // 处理完文件管理器,跳出循环
*/
} else {
// --- 其他元素未找到或不可聚焦 ---
console.log(`[App] Element with ID ${nextFocusId} not found or not focusable. Skipping.`);
}
}
if (!foundFocusable) {
console.log('[App] Cycled through sequence, no focusable element found.');
}
}
};
@@ -183,9 +255,9 @@ const isElementVisibleAndFocusable = (element: HTMLElement): boolean => {
<!-- 条件渲染样式自定义器使用 store 的状态和方法 -->
<StyleCustomizer v-if="isStyleCustomizerVisible" @close="closeStyleCustomizer" />
<!-- +++ 条件渲染焦点切换配置器 +++ -->
<!-- +++ 条件渲染焦点切换配置器 (使用 v-show 保持实例) +++ -->
<FocusSwitcherConfigurator
v-if="isFocusSwitcherVisible"
v-show="isFocusSwitcherVisible"
:isVisible="isFocusSwitcherVisible"
@close="focusSwitcherStore.toggleConfigurator(false)"
/>
@@ -76,6 +76,14 @@ const handleCommandInputKeydown = (event: KeyboardEvent) => {
}
};
// +++ 监听 Store 中的触发器以激活终端搜索 +++
watch(() => focusSwitcherStore.activateTerminalSearchTrigger, () => {
if (focusSwitcherStore.activateTerminalSearchTrigger > 0 && !isSearching.value) {
console.log('[CommandInputBar] Received terminal search activation trigger from store.');
toggleSearch(); // 调用组件内部的切换搜索方法来激活
}
});
</script>
<template>
@@ -8,8 +8,9 @@ import { createSftpActionsManager, type WebSocketDependencies } from '../composa
import { useFileUploader } from '../composables/useFileUploader';
// import { useFileEditor } from '../composables/useFileEditor'; // 移除旧的 composable 导入
import { useFileEditorStore, type FileInfo } from '../stores/fileEditor.store'; // 导入新的 Store 和 FileInfo 类型
import { useSessionStore } from '../stores/session.store'; // 导入 Session Store
import { useSettingsStore } from '../stores/settings.store'; // 导入 Settings Store
import { useSessionStore } from '../stores/session.store';
import { useSettingsStore } from '../stores/settings.store';
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
// WebSocket composable 不再直接使用
import FileUploadPopup from './FileUploadPopup.vue';
// import FileEditorOverlay from './FileEditorOverlay.vue'; // 不再在此处渲染
@@ -88,8 +89,9 @@ const {
// 实例化 Stores
const fileEditorStore = useFileEditorStore(); // 用于共享模式
const sessionStore = useSessionStore(); // 用于独立模式
const settingsStore = useSettingsStore(); // 用于获取设置
const sessionStore = useSessionStore();
const settingsStore = useSettingsStore();
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
// 从 Settings Store 获取共享设置
const { shareFileEditorTabsBoolean } = storeToRefs(settingsStore); // 使用 storeToRefs 保持响应性
@@ -980,6 +982,15 @@ watchEffect((onCleanup) => {
}
});
// +++ 监听 Store 中的触发器以激活搜索 +++
watch(() => focusSwitcherStore.activateFileManagerSearchTrigger, () => {
// 确保只在触发器值大于 0 时执行(避免初始加载时触发)
if (focusSwitcherStore.activateFileManagerSearchTrigger > 0) {
console.log('[FileManager] Received search activation trigger from store.');
activateSearch(); // 调用组件内部的激活搜索方法
}
});
onBeforeUnmount(() => {
console.log(`[FileManager ${props.sessionId}] 组件即将卸载。`);
@@ -34,15 +34,22 @@ const dialogStyle = reactive({
const hasChanges = ref(false);
// 本地副本,用于在弹窗内编辑而不直接修改 store
const localSequence: Ref<FocusableInput[]> = ref([]);
// +++ 存储原始序列 ID,用于比较 +++
const originalSequenceIds: Ref<string[]> = ref([]);
// --- Watchers ---
watch(() => props.isVisible, (newValue) => {
if (newValue) {
// 从 Store 加载当前配置到本地副本
// 使用深拷贝确保 localSequence 是独立的
localSequence.value = JSON.parse(JSON.stringify(focusSwitcherStore.getConfiguredInputs));
const loadedSequence = focusSwitcherStore.getConfiguredInputs; // 直接获取 getter 的值
console.log('[FocusSwitcherConfigurator] Loading sequence from store getter...'); // +++ Log: Start loading +++
localSequence.value = JSON.parse(JSON.stringify(loadedSequence));
// +++ 存储原始 ID 序列 +++
originalSequenceIds.value = loadedSequence.map(item => item.id);
hasChanges.value = false;
console.log('[FocusSwitcherConfigurator] 弹窗打开, 已加载配置到本地副本:', localSequence.value);
console.log('[FocusSwitcherConfigurator] Dialog opened. Loaded sequence to local copy:', localSequence.value); // +++ Log: Loaded local +++
console.log('[FocusSwitcherConfigurator] Original sequence IDs stored:', originalSequenceIds.value); // +++ Log: Stored original +++
// 重置/计算初始位置和大小
requestAnimationFrame(() => {
if (dialogRef.value) {
@@ -62,16 +69,20 @@ watch(() => props.isVisible, (newValue) => {
});
// 监听本地序列变化,标记未保存更改
watch(localSequence, (newValue, oldValue) => {
// 确保不是初始化加载触发的 watch
if (oldValue.length > 0 || (oldValue.length === 0 && newValue.length > 0)) {
// 比较 ID 序列是否真的改变了
const oldIds = oldValue.map(item => item.id);
const newIds = newValue.map(item => item.id);
if (JSON.stringify(oldIds) !== JSON.stringify(newIds)) {
hasChanges.value = true;
console.log('[FocusSwitcherConfigurator] 本地序列已更改。');
}
watch(localSequence, (currentLocalSequence) => {
// 直接比较当前本地序列的 ID 和原始 ID 序列
const currentIds = currentLocalSequence.map(item => item.id);
const originalIds = originalSequenceIds.value;
// 比较 JSON 字符串看是否有变化
const hasChanged = JSON.stringify(currentIds) !== JSON.stringify(originalIds); // +++ Calculate change status +++
if (hasChanged) {
// console.log('[FocusSwitcherConfigurator] Local sequence changed.'); // +++ Log: Changed +++
hasChanges.value = true;
} else {
// console.log('[FocusSwitcherConfigurator] Local sequence reverted to original.'); // +++ Log: Reverted +++
// 如果序列变回和原来一样,则标记为无更改
hasChanges.value = false;
}
}, { deep: true });
@@ -90,9 +101,10 @@ const closeDialog = () => {
const saveConfiguration = () => {
// 从本地副本提取 ID 序列
const newSequenceIds = localSequence.value.map(item => item.id);
console.log('[FocusSwitcherConfigurator] Saving configuration. Sequence IDs to save:', newSequenceIds); // +++ Log: Saving IDs +++
focusSwitcherStore.updateSequence(newSequenceIds); // 更新 Store 中的序列
focusSwitcherStore.saveConfiguration(); // 持久化保存
console.log('[FocusSwitcherConfigurator] 配置已保存:', newSequenceIds);
console.log('[FocusSwitcherConfigurator] Configuration save process completed.'); // +++ Log: Save completed +++
hasChanges.value = false;
emit('close'); // 保存后关闭
};
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { ref, computed, nextTick } from 'vue'; // +++ 将 nextTick 导入移到这里 +++
import { useI18n } from 'vue-i18n';
// 定义输入框的接口
@@ -15,7 +15,9 @@ export interface FocusableInput {
interface FocusSwitcherState {
availableInputs: FocusableInput[];
configuredSequence: string[]; // 只存储 ID 序列
isConfiguratorVisible: boolean; // 新增:控制配置器可见性
isConfiguratorVisible: boolean;
activateFileManagerSearchTrigger: number;
activateTerminalSearchTrigger: number; // +++ 新增:终端搜索触发器 +++
}
const LOCAL_STORAGE_KEY = 'focusSwitcherSequence';
@@ -38,10 +40,26 @@ export const useFocusSwitcherStore = defineStore('focusSwitcher', () => {
// 用户配置的切换顺序 (存储 ID)
const configuredSequence = ref<string[]>([]);
// 控制配置弹窗可见性
// 控制配置弹窗可见性
const isConfiguratorVisible = ref(false);
// +++ 触发器状态 +++
const activateFileManagerSearchTrigger = ref(0);
const activateTerminalSearchTrigger = ref(0); // +++ 终端搜索触发器状态 +++
// --- Actions ---
// +++ 新增:触发终端搜索激活的 Action +++
function triggerTerminalSearchActivation() {
activateTerminalSearchTrigger.value++;
console.log('[FocusSwitcherStore] Triggering Terminal search activation.');
}
// +++ 新增:触发文件管理器搜索激活的 Action +++
function triggerFileManagerSearchActivation() {
activateFileManagerSearchTrigger.value++; // 简单地增加计数器来触发监听
console.log('[FocusSwitcherStore] Triggering FileManager search activation.');
}
// 控制配置器显示/隐藏
function toggleConfigurator(visible?: boolean) {
isConfiguratorVisible.value = visible === undefined ? !isConfiguratorVisible.value : visible;
@@ -50,15 +68,24 @@ export const useFocusSwitcherStore = defineStore('focusSwitcher', () => {
// 从 localStorage 加载配置
function loadConfiguration() {
console.log('[FocusSwitcherStore] Attempting to load configuration...'); // +++ Log: Start loading +++
const savedSequence = localStorage.getItem(LOCAL_STORAGE_KEY);
console.log(`[FocusSwitcherStore] Raw data from localStorage ('${LOCAL_STORAGE_KEY}'):`, savedSequence); // +++ Log: Raw data +++
if (savedSequence) {
try {
const parsedSequence = JSON.parse(savedSequence);
console.log('[FocusSwitcherStore] Parsed sequence from localStorage:', parsedSequence); // +++ Log: Parsed data +++
// 验证加载的 ID 是否仍然存在于 availableInputs 中
configuredSequence.value = parsedSequence.filter((id: string) =>
availableInputs.value.some(input => input.id === id)
);
console.log('[FocusSwitcherStore] Configuration loaded:', configuredSequence.value);
const availableIds = new Set(availableInputs.value.map(input => input.id)); // +++ Get available IDs +++
const filteredSequence = parsedSequence.filter((id: string) => { // +++ Filter and log +++
const isValid = availableIds.has(id);
if (!isValid) {
console.warn(`[FocusSwitcherStore] Filtered out invalid ID during load: ${id}`);
}
return isValid;
});
configuredSequence.value = filteredSequence;
console.log('[FocusSwitcherStore] Final loaded & filtered configuration:', configuredSequence.value); // +++ Log: Final loaded value +++
} catch (error) {
console.error('[FocusSwitcherStore] Failed to parse saved configuration:', error);
configuredSequence.value = []; // 解析失败则重置
@@ -75,8 +102,12 @@ export const useFocusSwitcherStore = defineStore('focusSwitcher', () => {
// 保存配置到 localStorage
function saveConfiguration() {
try {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(configuredSequence.value));
console.log('[FocusSwitcherStore] Configuration saved:', configuredSequence.value);
const sequenceToSave = configuredSequence.value; // +++ Get value to save +++
const jsonStringToSave = JSON.stringify(sequenceToSave); // +++ Stringify +++
console.log(`[FocusSwitcherStore] Attempting to save sequence to localStorage ('${LOCAL_STORAGE_KEY}'):`, sequenceToSave); // +++ Log: Value being saved +++
console.log(`[FocusSwitcherStore] JSON string being saved:`, jsonStringToSave); // +++ Log: String being saved +++
localStorage.setItem(LOCAL_STORAGE_KEY, jsonStringToSave);
console.log('[FocusSwitcherStore] Configuration successfully saved.'); // +++ Log: Success +++
} catch (error) {
console.error('[FocusSwitcherStore] Failed to save configuration:', error);
}
@@ -84,10 +115,12 @@ export const useFocusSwitcherStore = defineStore('focusSwitcher', () => {
// 更新切换顺序
function updateSequence(newSequence: string[]) {
console.log('[FocusSwitcherStore] updateSequence called with:', newSequence); // +++ Log: Action called +++
// 确保新序列中的 ID 都是有效的
configuredSequence.value = newSequence.filter(id =>
availableInputs.value.some(input => input.id === id)
);
const availableIds = new Set(availableInputs.value.map(input => input.id)); // +++ Get available IDs +++
const filteredSequence = newSequence.filter(id => availableIds.has(id)); // +++ Filter +++
configuredSequence.value = filteredSequence;
console.log('[FocusSwitcherStore] configuredSequence updated to:', configuredSequence.value); // +++ Log: State updated +++
// 可以在这里直接保存,或者让用户点击保存按钮再调用 saveConfiguration
// saveConfiguration(); // 取决于设计
}
@@ -129,16 +162,23 @@ export const useFocusSwitcherStore = defineStore('focusSwitcher', () => {
// --- Initialization ---
// Store 创建时自动加载配置
loadConfiguration();
// Store 创建时自动加载配置 (使用 nextTick 确保响应式系统就绪)
// --- import { nextTick } from 'vue'; // --- 移除这里的导入 ---
nextTick(() => {
loadConfiguration();
});
return {
// State
availableInputs,
configuredSequence,
isConfiguratorVisible, // 导出状态
isConfiguratorVisible,
activateFileManagerSearchTrigger,
activateTerminalSearchTrigger, // +++ 导出终端搜索触发器状态 +++
// Actions
toggleConfigurator, // 导出 action
toggleConfigurator,
triggerFileManagerSearchActivation,
triggerTerminalSearchActivation, // +++ 确保导出终端搜索触发器 Action +++
loadConfiguration,
saveConfiguration,
updateSequence,