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
@@ -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>