This commit is contained in:
Baobhan Sith
2025-04-19 19:58:23 +08:00
parent ce96861eb6
commit 24d9360076
7 changed files with 562 additions and 8 deletions
+87 -4
View File
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { RouterLink, RouterView, useRoute } from 'vue-router';
import { ref, onMounted, watch, nextTick, computed } from 'vue'; // *** 导入 computed ***
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'; // +++ 添加 onUnmounted +++
import { useI18n } from 'vue-i18n';
import { useAuthStore } from './stores/auth.store';
import { useSettingsStore } from './stores/settings.store';
import { useAppearanceStore } from './stores/appearance.store';
import { useLayoutStore } from './stores/layout.store'; // *** 导入布局 Store ***
import { useLayoutStore } from './stores/layout.store';
import { useFocusSwitcherStore } from './stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
import { storeToRefs } from 'pinia';
// 导入通知显示组件
import UINotificationDisplay from './components/UINotificationDisplay.vue';
@@ -13,16 +14,20 @@ import UINotificationDisplay from './components/UINotificationDisplay.vue';
import FileEditorOverlay from './components/FileEditorOverlay.vue';
// 导入样式自定义器组件
import StyleCustomizer from './components/StyleCustomizer.vue';
// +++ 导入焦点切换配置器组件 +++
import FocusSwitcherConfigurator from './components/FocusSwitcherConfigurator.vue';
const { t } = useI18n();
const authStore = useAuthStore();
const settingsStore = useSettingsStore();
const appearanceStore = useAppearanceStore();
const layoutStore = useLayoutStore(); // *** 实例化布局 Store ***
const layoutStore = useLayoutStore();
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
const { isAuthenticated } = storeToRefs(authStore);
const { showPopupFileEditorBoolean } = storeToRefs(settingsStore);
const { isStyleCustomizerVisible } = storeToRefs(appearanceStore);
const { isLayoutVisible } = storeToRefs(layoutStore); // *** 获取布局可见性状态 ***
const { isLayoutVisible } = storeToRefs(layoutStore);
const { isConfiguratorVisible: isFocusSwitcherVisible } = storeToRefs(focusSwitcherStore);
const route = useRoute();
const navRef = ref<HTMLElement | null>(null);
@@ -46,8 +51,17 @@ onMounted(() => {
// Initial position update
// Use setTimeout to ensure styles are applied and elements have dimensions
setTimeout(updateUnderline, 100);
// +++ 添加全局 Alt 键监听器 +++
window.addEventListener('keydown', handleGlobalKeyDown);
});
// +++ 添加卸载钩子以移除监听器 +++
onUnmounted(() => {
window.removeEventListener('keydown', handleGlobalKeyDown);
});
// *** 新增:计算属性,判断是否在 workspace 路由 ***
const isWorkspaceRoute = computed(() => route.path === '/workspace');
@@ -69,6 +83,68 @@ const openStyleCustomizer = () => {
const closeStyleCustomizer = () => {
appearanceStore.toggleStyleCustomizer(false);
};
// +++ 全局键盘事件处理函数 +++
const handleGlobalKeyDown = (event: KeyboardEvent) => {
// 仅当 Alt 键被按下且没有其他修饰键 (如 Ctrl, Shift, Meta) 时触发
if (event.key === 'Alt' && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
event.preventDefault(); // 阻止 Alt 键的默认行为 (例如激活菜单栏)
const activeElement = document.activeElement as HTMLElement;
let currentFocusId: string | null = null;
// 检查当前焦点元素是否有我们设置的 data-focus-id
if (activeElement && activeElement.hasAttribute('data-focus-id')) {
currentFocusId = activeElement.getAttribute('data-focus-id');
}
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}`);
if (nextFocusId) {
// 尝试查找下一个目标元素
// 使用 requestAnimationFrame 确保在 DOM 更新后查找
requestAnimationFrame(() => {
const nextElement = document.querySelector(`[data-focus-id="${nextFocusId}"]`) as HTMLElement | null;
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.`);
// 可选:如果找不到或不可聚焦,可以尝试查找配置列表中的再下一个,或者直接忽略
// 这里暂时忽略,避免无限循环
}
});
}
}
};
// +++ 辅助函数:检查元素是否可见且可聚焦 +++
const isElementVisibleAndFocusable = (element: HTMLElement): boolean => {
if (!element) return false;
// 检查元素是否在 DOM 中,并且没有 display: none
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden') return false;
// 检查元素或其父元素是否被禁用
if ((element as HTMLInputElement).disabled) return false;
let parent = element.parentElement;
while (parent) {
if ((parent as HTMLFieldSetElement).disabled) return false;
parent = parent.parentElement;
}
// 检查元素是否足够在视口内(粗略检查)
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
</script>
<template>
@@ -107,6 +183,13 @@ const closeStyleCustomizer = () => {
<!-- 条件渲染样式自定义器使用 store 的状态和方法 -->
<StyleCustomizer v-if="isStyleCustomizerVisible" @close="closeStyleCustomizer" />
<!-- +++ 条件渲染焦点切换配置器 +++ -->
<FocusSwitcherConfigurator
v-if="isFocusSwitcherVisible"
:isVisible="isFocusSwitcherVisible"
@close="focusSwitcherStore.toggleConfigurator(false)"
/>
<footer v-if="!isWorkspaceRoute || isLayoutVisible"> <!-- *** 添加 v-if *** -->
<!-- 使用 t 函数获取应用名称 -->
<p>&copy; 2025 {{ t('appName') }}</p>
@@ -1,14 +1,16 @@
<script setup lang="ts">
import { ref, watch } from 'vue'; // Remove computed
import { ref, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // 导入 Store
// 假设你有一个图标库,例如 unplugin-icons 或类似库
// import SearchIcon from '~icons/mdi/magnify';
// import ArrowUpIcon from '~icons/mdi/arrow-up';
// import ArrowDownIcon from '~icons/mdi/arrow-down';
// import CloseIcon from '~icons/mdi/close';
const emit = defineEmits(['send-command', 'search', 'find-next', 'find-previous', 'close-search']);
const emit = defineEmits(['send-command', 'search', 'find-next', 'find-previous', 'close-search']); // 移除 open-focus-switcher-config 事件
const { t } = useI18n();
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化 Store +++
// Props definition is now empty as search results are no longer handled here
const props = defineProps<{
@@ -60,22 +62,38 @@ watch(searchTerm, (newValue) => {
});
// 可以在这里添加一个 ref 用于聚焦搜索框
// const searchInputRef = ref<HTMLInputElement | null>(null);
const searchInputRef = ref<HTMLInputElement | null>(null);
// Removed debug computed property
const handleCommandInputKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === 'f') {
event.preventDefault(); // 阻止浏览器默认的查找行为
isSearching.value = true;
nextTick(() => {
searchInputRef.value?.focus();
});
}
};
</script>
<template>
<div class="command-input-bar">
<div class="input-wrapper" :class="{ 'searching': isSearching }">
<!-- 新增焦点切换配置按钮 -->
<button @click="focusSwitcherStore.toggleConfigurator(true)" class="icon-button focus-switcher-button" :title="t('commandInputBar.configureFocusSwitch', '配置焦点切换')">
<i class="fas fa-keyboard"></i> <!-- 或者其他合适的图标 -->
</button>
<!-- 命令输入框 (始终渲染) -->
<input
type="text"
v-model="commandInput"
:placeholder="t('commandInputBar.placeholder')"
class="command-input"
data-focus-id="commandInput"
@keydown.enter="sendCommand"
@keydown="handleCommandInputKeydown"
/>
<!-- 搜索输入框 (始终渲染, 通过 CSS 控制显示/隐藏和宽度) -->
@@ -84,6 +102,7 @@ watch(searchTerm, (newValue) => {
v-model="searchTerm"
:placeholder="t('commandInputBar.searchPlaceholder')"
class="search-input"
data-focus-id="terminalSearch"
@keydown.enter.prevent="findNext"
@keydown.shift.enter.prevent="findPrevious"
@keydown.up.prevent="findPrevious"
@@ -120,7 +139,7 @@ watch(searchTerm, (newValue) => {
padding: 5px 10px; /* 增加左右 padding */
background-color: var(--app-bg-color);
min-height: 30px;
gap: 10px; /* 在输入框和控件之间添加间隙 */
gap: 5px; /* 减小整体间隙 */
}
.input-wrapper {
@@ -129,8 +148,16 @@ watch(searchTerm, (newValue) => {
align-items: center; /* 垂直居中对齐 */
background-color: transparent;
position: relative; /* 为了按钮定位 */
gap: 5px; /* 在按钮和输入框之间添加间隙 */
}
/* 焦点切换按钮样式 (复用 icon-button) */
.focus-switcher-button {
/* 可以添加特定样式,如果需要的话 */
flex-shrink: 0; /* 防止按钮被压缩 */
}
.command-input {
padding: 6px 10px;
border: 1px solid var(--border-color);
@@ -1155,6 +1155,7 @@ const handleWheel = (event: WheelEvent) => {
v-model="searchQuery"
:placeholder="t('fileManager.searchPlaceholder')"
class="search-input"
data-focus-id="fileManagerSearch"
@blur="deactivateSearch"
@keyup.esc="cancelSearch"
@keydown.up.prevent="handleKeydown"
@@ -0,0 +1,291 @@
<script setup lang="ts">
import { ref, computed, watch, reactive, type Ref } from 'vue'; // 添加 Ref
import { useI18n } from 'vue-i18n';
import draggable from 'vuedraggable'; // 导入 draggable
import { useFocusSwitcherStore, type FocusableInput } from '../stores/focusSwitcher.store'; // 导入 Store 和类型
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
// --- Props ---
const props = defineProps({
isVisible: {
type: Boolean,
required: true,
},
});
// --- Emits ---
const emit = defineEmits(['close']);
// --- Setup ---
const { t } = useI18n();
const focusSwitcherStore = useFocusSwitcherStore(); // 实例化 Store
// --- State ---
const dialogRef = ref<HTMLElement | null>(null);
const initialDialogState = { width: 900, height: 600 }; // *** 增加初始尺寸 ***
const dialogStyle = reactive({
width: `${initialDialogState.width}px`,
height: `${initialDialogState.height}px`,
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
position: 'absolute' as 'absolute',
});
const hasChanges = ref(false);
// 本地副本,用于在弹窗内编辑而不直接修改 store
const localSequence: Ref<FocusableInput[]> = ref([]);
// --- Watchers ---
watch(() => props.isVisible, (newValue) => {
if (newValue) {
// 从 Store 加载当前配置到本地副本
// 使用深拷贝确保 localSequence 是独立的
localSequence.value = JSON.parse(JSON.stringify(focusSwitcherStore.getConfiguredInputs));
hasChanges.value = false;
console.log('[FocusSwitcherConfigurator] 弹窗打开, 已加载配置到本地副本:', localSequence.value);
// 重置/计算初始位置和大小
requestAnimationFrame(() => {
if (dialogRef.value) {
const initialWidth = initialDialogState.width;
const initialHeight = initialDialogState.height;
dialogStyle.width = `${initialWidth}px`;
dialogStyle.height = `${initialHeight}px`;
dialogStyle.left = `${(window.innerWidth - initialWidth) / 2}px`;
dialogStyle.top = `${(window.innerHeight - initialHeight) / 2}px`;
dialogStyle.transform = 'none';
dialogStyle.position = 'absolute';
}
});
} else {
// 清理工作(如果需要)
}
});
// 监听本地序列变化,标记未保存更改
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] 本地序列已更改。');
}
}
}, { deep: true });
// --- Methods ---
const closeDialog = () => {
if (hasChanges.value) {
if (confirm(t('focusSwitcher.confirmClose', '有未保存的更改,确定要关闭吗?'))) {
emit('close');
}
} else {
emit('close');
}
};
const saveConfiguration = () => {
// 从本地副本提取 ID 序列
const newSequenceIds = localSequence.value.map(item => item.id);
focusSwitcherStore.updateSequence(newSequenceIds); // 更新 Store 中的序列
focusSwitcherStore.saveConfiguration(); // 持久化保存
console.log('[FocusSwitcherConfigurator] 配置已保存:', newSequenceIds);
hasChanges.value = false;
emit('close'); // 保存后关闭
};
// --- Computed ---
// 新的计算属性:基于本地已配置列表动态计算可用输入框
const localAvailableInputs = computed(() => {
// 获取本地已配置项的 ID 集合
const configuredIds = new Set(localSequence.value.map(item => item.id));
// 从 store 的 availableInputs state 中过滤掉已在本地配置的项
// 注意:直接访问 store 的 state ref
return focusSwitcherStore.availableInputs.filter(input => !configuredIds.has(input.id));
});
// 注意:已配置的列表直接使用 localSequence ref
// 原先的 availableInputsForConfigurator getter 在 store 中仍然存在,但我们现在使用本地计算的版本以实现实时更新
</script>
<template>
<div v-if="isVisible" class="focus-switcher-overlay" @click.self="closeDialog">
<div ref="dialogRef" class="focus-switcher-dialog" :style="dialogStyle">
<header class="dialog-header">
<h2>{{ t('focusSwitcher.configTitle', '配置 Alt 焦点切换') }}</h2>
<button class="close-button" @click="closeDialog" :title="t('common.close', '关闭')">&times;</button>
</header>
<main class="dialog-content">
<section class="available-inputs-section">
<h3>{{ t('focusSwitcher.availableInputs', '可用输入框') }}</h3>
<draggable
:list="localAvailableInputs" <!-- 改为使用本地计算属性 -->
tag="ul"
class="draggable-list available-list"
item-key="id"
:group="{ name: 'focus-inputs', pull: true, put: false }"
:sort="false"
>
<template #item="{ element }: { element: FocusableInput }">
<li class="draggable-item">
<i class="fas fa-grip-vertical drag-handle"></i>
{{ element.label }}
</li>
</template>
<template #footer>
<li v-if="localAvailableInputs.length === 0" class="no-items-placeholder"> <!-- 判断条件也更新 -->
{{ t('focusSwitcher.allInputsConfigured', '所有输入框都已配置') }}
</li>
</template>
</draggable>
</section>
<section class="configured-sequence-section">
<h3>{{ t('focusSwitcher.configuredSequence', '切换顺序 (拖拽排序)') }}</h3>
<draggable
:list="localSequence"
tag="ul"
class="draggable-list configured-list"
item-key="id"
:group="{ name: 'focus-inputs', put: true }" <!-- 明确允许放入 -->
handle=".drag-handle"
>
<template #item="{ element, index }: { element: FocusableInput, index: number }">
<li class="draggable-item">
<i class="fas fa-grip-vertical drag-handle"></i>
{{ element.label }}
<!-- 添加移除按钮 -->
<button @click="localSequence.splice(index, 1)" class="remove-button" :title="t('common.remove', '移除')">&times;</button>
</li>
</template>
<template #footer>
<li v-if="localSequence.length === 0" class="no-items-placeholder">
{{ t('focusSwitcher.dragHere', '从左侧拖拽输入框到此处') }}
</li>
</template>
</draggable>
</section>
</main>
<footer class="dialog-footer">
<button @click="closeDialog" class="button-secondary">{{ t('common.cancel', '取消') }}</button>
<button @click="saveConfiguration" class="button-primary" :disabled="!hasChanges">
{{ t('common.save', '保存') }} {{ hasChanges ? '*' : '' }}
</button>
</footer>
</div>
</div>
</template>
<style scoped>
/* 样式很大程度上复用 LayoutConfigurator,但使用不同的类名以避免冲突 */
.focus-switcher-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.6); display: flex;
justify-content: center; align-items: flex-start; /* 改为 flex-start */
z-index: 1000; pointer-events: none;
}
.focus-switcher-dialog {
background-color: var(--dialog-bg-color, #fff); border-radius: 8px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
display: flex; flex-direction: column; overflow: hidden;
position: absolute; pointer-events: auto; cursor: default;
color: var(--text-color);
}
.dialog-header {
display: flex; justify-content: space-between; align-items: center;
padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-color);
background-color: var(--header-bg-color);
}
.dialog-header h2 { margin: 0; font-size: 1.2rem; font-weight: 600; }
.close-button {
background: none; border: none; font-size: 1.8rem; cursor: pointer;
color: var(--text-color-secondary); line-height: 1; padding: 0;
}
.close-button:hover { color: var(--text-color); }
.dialog-content {
flex-grow: 1; padding: 1.5rem; overflow-y: auto;
display: flex; gap: 1.5rem;
background-color: var(--app-bg-color); /* 内容区背景 */
}
.available-inputs-section, .configured-sequence-section {
flex: 1; padding: 1rem; border: 1px solid var(--border-color);
border-radius: 4px; background-color: var(--input-bg-color); /* 区域背景 */
}
h3 {
margin-top: 0; margin-bottom: 1rem; font-size: 1rem;
font-weight: 600; color: var(--text-color-secondary);
border-bottom: 1px solid var(--border-color-light); padding-bottom: 0.5rem;
}
.draggable-list {
list-style: none; padding: 0; margin: 0;
min-height: 100px; /* 给拖放区域一个最小高度 */
border: 1px dashed var(--border-color-light); /* 虚线边框 */
border-radius: 4px;
padding: 0.5rem;
background-color: rgba(0,0,0,0.02); /* 轻微背景 */
}
.draggable-item {
padding: 0.6rem 0.8rem; margin-bottom: 0.5rem;
background-color: var(--app-bg-color); border: 1px solid var(--border-color);
border-radius: 4px; cursor: grab;
display: flex; /* 使用 flex 布局 */
align-items: center; /* 垂直居中 */
justify-content: space-between; /* 两端对齐 */
transition: background-color 0.2s ease;
}
.draggable-item:hover {
background-color: var(--header-bg-color); /* 悬停效果 */
}
.draggable-item.sortable-ghost { /* 拖拽时的占位符样式 */
opacity: 0.4;
background: #c8ebfb;
}
.drag-handle {
margin-right: 0.5rem;
color: var(--text-color-secondary);
cursor: grab;
}
.draggable-item:active .drag-handle {
cursor: grabbing;
}
.remove-button {
background: none; border: none; color: var(--text-color-secondary);
font-size: 1.2rem; cursor: pointer; padding: 0 0.3rem; line-height: 1;
margin-left: auto; /* 推到最右边 */
}
.remove-button:hover { color: var(--danger-color, red); }
.no-items-placeholder {
text-align: center; color: var(--text-color-secondary); font-style: italic;
padding: 1rem; border: none; background: none; cursor: default;
}
.dialog-footer {
padding: 1rem 1.5rem; border-top: 1px solid var(--border-color);
display: flex; justify-content: flex-end; gap: 0.8rem;
background-color: var(--header-bg-color);
}
/* 通用按钮样式 (复用 LayoutConfigurator 或全局样式) */
.button-primary, .button-secondary {
padding: 0.5rem 1rem; border: none; border-radius: 4px;
cursor: pointer; font-size: 0.9rem;
transition: background-color 0.2s ease, opacity 0.2s ease;
}
.button-primary {
background-color: var(--button-bg-color); color: var(--button-text-color);
}
.button-primary:hover:not(:disabled) { background-color: var(--button-hover-bg-color); }
.button-primary:disabled { background-color: #6c757d; opacity: 0.7; cursor: not-allowed; }
.button-secondary {
background-color: var(--secondary-button-bg-color, #e9ecef);
color: var(--secondary-button-text-color, #343a40);
border: 1px solid var(--border-color);
}
.button-secondary:hover { background-color: var(--secondary-button-hover-bg-color, #dee2e6); }
</style>
@@ -0,0 +1,150 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
// 定义输入框的接口
export interface FocusableInput {
id: string; // 唯一标识符
label: string; // 用户友好的名称
// 可以添加其他元数据,例如组件路径或选择器,以便将来查找元素
componentPath?: string;
selector?: string;
}
// 定义 Store 的 State 接口 (可选但推荐)
interface FocusSwitcherState {
availableInputs: FocusableInput[];
configuredSequence: string[]; // 只存储 ID 序列
isConfiguratorVisible: boolean; // 新增:控制配置器可见性
}
const LOCAL_STORAGE_KEY = 'focusSwitcherSequence';
export const useFocusSwitcherStore = defineStore('focusSwitcher', () => {
const { t } = useI18n(); // 在 store setup 中获取 t 函数
// --- State ---
// 所有可供配置的输入框列表
// 注意:label 使用 t() 函数获取初始值
const availableInputs = ref<FocusableInput[]>([
{ id: 'commandHistorySearch', label: t('focusSwitcher.input.commandHistorySearch', '命令历史搜索'), componentPath: 'CommandHistoryView.vue', selector: 'input[placeholder*="搜索历史记录"]' },
{ id: 'quickCommandsSearch', label: t('focusSwitcher.input.quickCommandsSearch', '快捷指令搜索'), componentPath: 'QuickCommandsView.vue', selector: 'input[placeholder*="搜索名称或指令"]' },
{ id: 'fileManagerSearch', label: t('focusSwitcher.input.fileManagerSearch', '文件管理器搜索'), componentPath: 'FileManager.vue', selector: '.search-input' }, // FileManager 的搜索框 class 是 search-input
{ id: 'commandInput', label: t('focusSwitcher.input.commandInput', '命令输入'), componentPath: 'CommandInputBar.vue', selector: '.command-input' },
{ id: 'terminalSearch', label: t('focusSwitcher.input.terminalSearch', '终端内搜索'), componentPath: 'CommandInputBar.vue', selector: '.search-input' }, // CommandInputBar 的搜索框 class 也是 search-input
// 注意:CommandInputBar 和 FileManager 都有 .search-input,需要更精确的选择器或逻辑来区分,暂时先这样
]);
// 用户配置的切换顺序 (存储 ID)
const configuredSequence = ref<string[]>([]);
// 控制配置弹窗可见性
const isConfiguratorVisible = ref(false);
// --- Actions ---
// 控制配置器显示/隐藏
function toggleConfigurator(visible?: boolean) {
isConfiguratorVisible.value = visible === undefined ? !isConfiguratorVisible.value : visible;
console.log(`[FocusSwitcherStore] Configurator visibility set to: ${isConfiguratorVisible.value}`);
}
// 从 localStorage 加载配置
function loadConfiguration() {
const savedSequence = localStorage.getItem(LOCAL_STORAGE_KEY);
if (savedSequence) {
try {
const parsedSequence = JSON.parse(savedSequence);
// 验证加载的 ID 是否仍然存在于 availableInputs 中
configuredSequence.value = parsedSequence.filter((id: string) =>
availableInputs.value.some(input => input.id === id)
);
console.log('[FocusSwitcherStore] Configuration loaded:', configuredSequence.value);
} catch (error) {
console.error('[FocusSwitcherStore] Failed to parse saved configuration:', error);
configuredSequence.value = []; // 解析失败则重置
localStorage.removeItem(LOCAL_STORAGE_KEY); // 移除损坏的数据
}
} else {
// 如果没有保存的配置,可以设置一个默认顺序
// configuredSequence.value = ['commandInput', 'terminalSearch']; // 例如
configuredSequence.value = []; // 或者默认为空
console.log('[FocusSwitcherStore] No saved configuration found, using default (empty).');
}
}
// 保存配置到 localStorage
function saveConfiguration() {
try {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(configuredSequence.value));
console.log('[FocusSwitcherStore] Configuration saved:', configuredSequence.value);
} catch (error) {
console.error('[FocusSwitcherStore] Failed to save configuration:', error);
}
}
// 更新切换顺序
function updateSequence(newSequence: string[]) {
// 确保新序列中的 ID 都是有效的
configuredSequence.value = newSequence.filter(id =>
availableInputs.value.some(input => input.id === id)
);
// 可以在这里直接保存,或者让用户点击保存按钮再调用 saveConfiguration
// saveConfiguration(); // 取决于设计
}
// --- Getters ---
// 获取已配置的输入框完整信息 (按顺序)
const getConfiguredInputs = computed((): FocusableInput[] => {
return configuredSequence.value
.map(id => availableInputs.value.find(input => input.id === id))
.filter((input): input is FocusableInput => input !== undefined); // 类型守卫确保过滤掉 undefined
});
// 获取在配置器中可用的输入框 (未被配置的)
const getAvailableInputsForConfigurator = computed((): FocusableInput[] => {
const configuredIds = new Set(configuredSequence.value);
return availableInputs.value.filter(input => !configuredIds.has(input.id));
});
// 获取下一个要聚焦的输入框 ID (用于实际切换逻辑)
function getNextFocusTargetId(currentFocusedId: string | null): string | null {
if (configuredSequence.value.length === 0) {
return null; // 没有配置顺序
}
if (currentFocusedId === null) {
// 如果当前没有焦点在配置列表内,返回第一个
return configuredSequence.value[0];
}
const currentIndex = configuredSequence.value.indexOf(currentFocusedId);
if (currentIndex === -1) {
// 如果当前焦点不在配置列表内,返回第一个
return configuredSequence.value[0];
}
// 返回下一个,如果到末尾则循环回第一个
const nextIndex = (currentIndex + 1) % configuredSequence.value.length;
return configuredSequence.value[nextIndex];
}
// --- Initialization ---
// Store 创建时自动加载配置
loadConfiguration();
return {
// State
availableInputs,
configuredSequence,
isConfiguratorVisible, // 导出状态
// Actions
toggleConfigurator, // 导出 action
loadConfiguration,
saveConfiguration,
updateSequence,
// Getters
getConfiguredInputs,
getAvailableInputsForConfigurator,
getNextFocusTargetId,
};
});
@@ -9,6 +9,7 @@
type="text"
:placeholder="$t('commandHistory.searchPlaceholder', '搜索历史记录...')"
:value="searchTerm"
data-focus-id="commandHistorySearch"
@input="updateSearchTerm($event)"
@keydown="handleKeydown"
class="search-input"
@@ -8,6 +8,7 @@
type="text"
:placeholder="$t('quickCommands.searchPlaceholder', '搜索名称或指令...')"
:value="searchTerm"
data-focus-id="quickCommandsSearch"
@input="updateSearchTerm($event)"
@keydown="handleKeydown"
class="search-input"