update
This commit is contained in:
@@ -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>© 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', '关闭')">×</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', '移除')">×</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"
|
||||
|
||||
Reference in New Issue
Block a user