@@ -275,6 +275,35 @@ const handleSubmit = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- Tag Creation/Deletion Handling ---
|
||||
const handleCreateTag = async (tagName: string) => {
|
||||
console.log(`[ConnForm] Received create-tag event for: ${tagName}`); // +++ 添加日志 +++
|
||||
if (!tagName || tagName.trim().length === 0) return;
|
||||
console.log(`[ConnForm] Calling tagsStore.addTag...`); // +++ 添加日志 +++
|
||||
const newTag = await tagsStore.addTag(tagName.trim()); // Use the correct store
|
||||
if (newTag && !formData.tag_ids.includes(newTag.id)) {
|
||||
console.log(`[ConnForm] New tag created (ID: ${newTag.id}), adding to selection.`); // +++ 添加日志 +++
|
||||
// Add the new tag's ID to the selected list
|
||||
formData.tag_ids.push(newTag.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTag = async (tagId: number) => {
|
||||
const tagToDelete = tags.value.find(t => t.id === tagId);
|
||||
if (!tagToDelete) return;
|
||||
|
||||
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
|
||||
const success = await tagsStore.deleteTag(tagId); // Use the correct store
|
||||
if (success) {
|
||||
// TagInput's modelValue will update automatically via watch
|
||||
// No need to manually remove from formData.tag_ids here
|
||||
} else {
|
||||
// Optional: Show error notification if deletion fails
|
||||
alert(t('tags.errorDelete', { error: tagsStore.error || '未知错误' }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理测试连接
|
||||
const handleTestConnection = async () => {
|
||||
testStatus.value = 'testing';
|
||||
@@ -487,9 +516,19 @@ const testButtonText = computed(() => {
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-secondary mb-1">{{ t('connections.form.tags') }} ({{ t('connections.form.optional') }})</label>
|
||||
<TagInput v-model="formData.tag_ids" />
|
||||
<TagInput
|
||||
v-model="formData.tag_ids"
|
||||
:available-tags="tags"
|
||||
:allow-create="true"
|
||||
:allow-delete="true"
|
||||
@create-tag="handleCreateTag"
|
||||
@delete-tag="handleDeleteTag"
|
||||
:placeholder="t('tags.inputPlaceholder', '添加或选择标签...')"
|
||||
/>
|
||||
<div v-if="isTagLoading" class="mt-1 text-xs text-text-secondary">{{ t('tags.loading') }}</div>
|
||||
<div v-if="tagStoreError" class="mt-1 text-xs text-error">{{ t('tags.error', { error: tagStoreError }) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<div v-if="formError || storeError" class="text-error bg-error/10 border border-error/30 rounded-md p-3 text-sm text-center font-medium">
|
||||
|
||||
@@ -25,6 +25,23 @@
|
||||
></textarea>
|
||||
<small v-if="commandError" class="text-error text-xs mt-1 block">{{ commandError }}</small>
|
||||
</div>
|
||||
<!-- +++ Tag Input Section +++ -->
|
||||
<div>
|
||||
<label for="qc-tags" class="block mb-1.5 text-sm font-medium text-text-secondary">{{ t('quickCommands.form.tags', '标签:') }}</label>
|
||||
<TagInput
|
||||
id="qc-tags"
|
||||
v-model="formData.tagIds"
|
||||
:available-tags="quickCommandTagsStore.tags"
|
||||
:placeholder="t('quickCommands.form.tagsPlaceholder', '添加或选择标签...')"
|
||||
@create-tag="handleCreateTag"
|
||||
:allow-create="true"
|
||||
:allow-delete="true"
|
||||
@delete-tag="handleDeleteTag"
|
||||
class="w-full"
|
||||
/>
|
||||
<!-- Add styling/classes as needed for TagInput -->
|
||||
</div>
|
||||
<!-- +++ End Tag Input Section +++ -->
|
||||
<div class="flex justify-end mt-8 pt-4 border-t border-border/50">
|
||||
<!-- Secondary/Cancel Button -->
|
||||
<button type="button" @click="closeForm" class="py-2 px-5 rounded-lg text-sm font-medium transition-colors duration-150 bg-background border border-border/50 text-text-secondary hover:bg-border hover:text-foreground mr-3">{{ t('common.cancel', '取消') }}</button>
|
||||
@@ -42,22 +59,26 @@
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store';
|
||||
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; // +++ Import new tag store +++
|
||||
import TagInput from './TagInput.vue'; // +++ Import TagInput component (assuming it exists) +++
|
||||
|
||||
const props = defineProps<{
|
||||
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象
|
||||
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds)
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const quickCommandsStore = useQuickCommandsStore();
|
||||
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate tag store +++
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const isEditing = computed(() => !!props.commandToEdit);
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
command: '',
|
||||
name: '',
|
||||
command: '',
|
||||
tagIds: [] as number[], // +++ Add tagIds +++
|
||||
});
|
||||
|
||||
const commandError = ref<string | null>(null);
|
||||
@@ -76,9 +97,54 @@ onMounted(() => {
|
||||
if (isEditing.value && props.commandToEdit) {
|
||||
formData.name = props.commandToEdit.name ?? '';
|
||||
formData.command = props.commandToEdit.command;
|
||||
// Initialize tagIds if editing
|
||||
formData.tagIds = props.commandToEdit.tagIds ? [...props.commandToEdit.tagIds] : [];
|
||||
}
|
||||
// Fetch tags if not already loaded (optional, might be better in parent)
|
||||
// if (quickCommandTagsStore.tags.length === 0) {
|
||||
// quickCommandTagsStore.fetchTags();
|
||||
// }
|
||||
});
|
||||
|
||||
// --- Tag Creation Handling ---
|
||||
// Assuming TagInput emits 'create-tag' with the tag name
|
||||
const handleCreateTag = async (tagName: string) => {
|
||||
console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`); // +++ 添加日志 +++
|
||||
if (!tagName || tagName.trim().length === 0) return;
|
||||
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.addTag...`); // +++ 添加日志 +++
|
||||
const newTag = await quickCommandTagsStore.addTag(tagName.trim());
|
||||
if (newTag && !formData.tagIds.includes(newTag.id)) {
|
||||
console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`); // +++ 添加日志 +++
|
||||
// Add the new tag's ID to the selected list
|
||||
formData.tagIds.push(newTag.id);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Tag Deletion Handling ---
|
||||
const handleDeleteTag = async (tagId: number) => {
|
||||
console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`); // +++ 添加日志 +++
|
||||
const tagToDelete = quickCommandTagsStore.tags.find(t => t.id === tagId);
|
||||
if (!tagToDelete) return;
|
||||
|
||||
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
|
||||
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`); // +++ 添加日志 +++
|
||||
const success = await quickCommandTagsStore.deleteTag(tagId);
|
||||
if (success) {
|
||||
// If deletion is successful, TagInput's availableTags will update,
|
||||
// and the tag should disappear from the input.
|
||||
// We also need to remove it from the local formData.tagIds if it was selected.
|
||||
const index = formData.tagIds.indexOf(tagId);
|
||||
if (index > -1) {
|
||||
console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`); // +++ 添加日志 +++
|
||||
formData.tagIds.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
// Optional: Show error notification if deletion fails
|
||||
alert(t('tags.errorDelete', { error: quickCommandTagsStore.error || '未知错误' }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (commandError.value) return; // 如果校验失败则不提交
|
||||
|
||||
@@ -89,9 +155,11 @@ const handleSubmit = async () => {
|
||||
const finalName = formData.name.trim().length > 0 ? formData.name.trim() : null;
|
||||
|
||||
if (isEditing.value && props.commandToEdit) {
|
||||
success = await quickCommandsStore.updateQuickCommand(props.commandToEdit.id, finalName, formData.command.trim());
|
||||
// Pass tagIds to update action
|
||||
success = await quickCommandsStore.updateQuickCommand(props.commandToEdit.id, finalName, formData.command.trim(), formData.tagIds);
|
||||
} else {
|
||||
success = await quickCommandsStore.addQuickCommand(finalName, formData.command.trim());
|
||||
// Pass tagIds to add action
|
||||
success = await quickCommandsStore.addQuickCommand(finalName, formData.command.trim(), formData.tagIds);
|
||||
}
|
||||
|
||||
isSubmitting.value = false;
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
// import { storeToRefs } from 'pinia'; // No longer needed directly
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useTagsStore, TagInfo } from '../stores/tags.store';
|
||||
// import { useTagsStore, TagInfo } from '../stores/tags.store'; // REMOVE dependency on specific store
|
||||
|
||||
// Define a generic tag structure for the prop
|
||||
interface GenericTag {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number[]; // 接收选中的 tag_ids
|
||||
modelValue: number[]; // 接收选中的 tag_ids
|
||||
availableTags?: GenericTag[]; // Optional: The list of tags to choose from/display
|
||||
placeholder?: string; // Optional: Placeholder for the input
|
||||
allowCreate?: boolean; // Optional: Allow creating new tags via Enter (default true)
|
||||
allowDelete?: boolean; // Optional: Allow showing the global delete button (default true)
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const emit = defineEmits(['update:modelValue', 'create-tag', 'delete-tag']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const tagsStore = useTagsStore();
|
||||
const { tags, isLoading, error } = storeToRefs(tagsStore);
|
||||
// const tagsStore = useTagsStore(); // REMOVE
|
||||
// const { tags, isLoading, error } = storeToRefs(tagsStore); // REMOVE
|
||||
|
||||
const inputValue = ref(''); // 输入框的值
|
||||
const inputRef = ref<HTMLInputElement | null>(null); // 输入框引用
|
||||
const showSuggestions = ref(false); // 是否显示建议列表
|
||||
const selectedTagIds = ref<number[]>([]); // 本地维护选中的 tag_ids
|
||||
|
||||
// Default values for props
|
||||
const availableTags = computed(() => props.availableTags ?? []);
|
||||
const placeholder = computed(() => props.placeholder ?? t('tags.inputPlaceholder', '添加或选择标签...'));
|
||||
const allowCreate = computed(() => props.allowCreate !== false); // Default true
|
||||
const allowDelete = computed(() => props.allowDelete !== false); // Default true
|
||||
|
||||
// 监听 props.modelValue 的变化,同步到本地 selectedTagIds
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
// 只有在值确实不同的情况下才更新,避免无限循环
|
||||
@@ -37,66 +53,59 @@ watch(selectedTagIds, (newVal) => {
|
||||
|
||||
|
||||
// 计算属性:所有标签的 Map,方便通过 ID 查找
|
||||
// Use availableTags prop for the map
|
||||
const tagsMap = computed(() => {
|
||||
const map = new Map<number, TagInfo>();
|
||||
tags.value.forEach(tag => map.set(tag.id, tag));
|
||||
return map;
|
||||
const map = new Map<number, GenericTag>();
|
||||
availableTags.value.forEach(tag => map.set(tag.id, tag));
|
||||
return map;
|
||||
});
|
||||
|
||||
// 计算属性:已选中的标签对象
|
||||
const selectedTags = computed(() => {
|
||||
// 确保先从 map 中查找,再过滤掉未找到的 (可能标签已被删除)
|
||||
return selectedTagIds.value
|
||||
.map(id => tagsMap.value.get(id))
|
||||
.filter((tag): tag is TagInfo => tag !== undefined);
|
||||
.map(id => tagsMap.value.get(id)) // Get from the map based on prop
|
||||
.filter((tag): tag is GenericTag => tag !== undefined);
|
||||
});
|
||||
|
||||
// 计算属性:过滤后的建议列表
|
||||
// 计算属性:过滤后的建议列表 (based on availableTags)
|
||||
const suggestions = computed(() => {
|
||||
if (!showSuggestions.value) { // 仅在需要显示时计算
|
||||
return [];
|
||||
}
|
||||
let result: TagInfo[];
|
||||
// 如果输入框为空,显示所有未选中的标签
|
||||
let result: GenericTag[]; // Use GenericTag type
|
||||
// Use availableTags from prop
|
||||
const currentAvailableTags = availableTags.value;
|
||||
// 如果输入框为空,显示所有未选中的可用标签
|
||||
if (!inputValue.value) {
|
||||
result = tags.value.filter(tag => !selectedTagIds.value.includes(tag.id));
|
||||
result = currentAvailableTags.filter(tag => !selectedTagIds.value.includes(tag.id));
|
||||
} else {
|
||||
const lowerCaseInput = inputValue.value.toLowerCase();
|
||||
result = tags.value.filter(tag =>
|
||||
tag.name.toLowerCase().includes(lowerCaseInput) &&
|
||||
!selectedTagIds.value.includes(tag.id) // 排除已选中的
|
||||
);
|
||||
const lowerCaseInput = inputValue.value.toLowerCase();
|
||||
result = currentAvailableTags.filter(tag =>
|
||||
tag.name.toLowerCase().includes(lowerCaseInput) &&
|
||||
!selectedTagIds.value.includes(tag.id) // 排除已选中的
|
||||
);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
// 处理输入框聚焦
|
||||
const handleFocus = async () => {
|
||||
showSuggestions.value = false; // 在异步操作前显式设置为 false
|
||||
// 1. 首先获取最新的标签
|
||||
await tagsStore.fetchTags();
|
||||
// 处理输入框聚焦 (不再 fetch, 仅根据现有 availableTags 判断是否显示)
|
||||
const handleFocus = () => {
|
||||
// 计算建议 (不依赖 showSuggestions ref)
|
||||
let potentialSuggestions: GenericTag[];
|
||||
const currentInput = inputValue.value;
|
||||
const currentAvailableTags = availableTags.value.filter(tag => !selectedTagIds.value.includes(tag.id));
|
||||
|
||||
// 2. 基于更新后的标签列表和当前输入值,计算出实际可以显示的建议标签
|
||||
// (这部分逻辑与 computed 'suggestions' 类似,但不依赖 showSuggestions.value)
|
||||
let potentialSuggestions: TagInfo[];
|
||||
const currentInput = inputValue.value; // 获取当前输入框的值
|
||||
// 过滤掉已选中的标签
|
||||
const availableTags = tags.value.filter(tag => !selectedTagIds.value.includes(tag.id));
|
||||
|
||||
if (!currentInput) {
|
||||
// 如果输入框为空,所有未选中的标签都是潜在建议
|
||||
potentialSuggestions = availableTags;
|
||||
} else {
|
||||
// 如果输入框有值,则根据输入值过滤可用标签
|
||||
const lowerCaseInput = currentInput.toLowerCase();
|
||||
potentialSuggestions = availableTags.filter(tag =>
|
||||
tag.name.toLowerCase().includes(lowerCaseInput)
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 只有当确实存在潜在建议时,才显示建议列表
|
||||
const shouldShow = potentialSuggestions.length > 0;
|
||||
showSuggestions.value = shouldShow; // 最终状态由计算结果决定
|
||||
if (!currentInput) {
|
||||
potentialSuggestions = currentAvailableTags;
|
||||
} else {
|
||||
const lowerCaseInput = currentInput.toLowerCase();
|
||||
potentialSuggestions = currentAvailableTags.filter(tag =>
|
||||
tag.name.toLowerCase().includes(lowerCaseInput)
|
||||
);
|
||||
}
|
||||
// 只有当确实存在潜在建议时,才显示建议列表
|
||||
showSuggestions.value = potentialSuggestions.length > 0;
|
||||
};
|
||||
|
||||
// 处理输入框失焦
|
||||
@@ -111,23 +120,18 @@ const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
event.preventDefault(); // 阻止表单提交等默认行为
|
||||
const trimmedInput = inputValue.value.trim();
|
||||
const lowerCaseInput = trimmedInput.toLowerCase();
|
||||
const existingTag = tags.value.find(tag => tag.name.toLowerCase() === lowerCaseInput);
|
||||
// Check against availableTags prop
|
||||
const existingTag = availableTags.value.find(tag => tag.name.toLowerCase() === lowerCaseInput);
|
||||
|
||||
if (existingTag && !selectedTagIds.value.includes(existingTag.id)) {
|
||||
// 如果是现有标签且未选中,则选中它
|
||||
selectTag(existingTag);
|
||||
} else if (!existingTag) {
|
||||
// 如果是新标签,则创建并选中
|
||||
const success = await tagsStore.addTag(trimmedInput);
|
||||
if (success) {
|
||||
// addTag 内部会 fetchTags, store 会更新
|
||||
// 需要等待 DOM 更新和 store 更新完成
|
||||
await nextTick(); // 等待 store 更新
|
||||
const newTag = tags.value.find(tag => tag.name === trimmedInput); // 再次查找确保获取到 ID
|
||||
if (newTag) {
|
||||
selectTag(newTag);
|
||||
}
|
||||
}
|
||||
// 如果是现有标签且未选中,则选中它
|
||||
selectTag(existingTag);
|
||||
} else if (!existingTag && allowCreate.value) { // Only create if allowed and not existing
|
||||
// 如果是新标签,则 emit 事件让父组件处理创建
|
||||
console.log(`[TagInput] Emitting create-tag for: ${trimmedInput}`); // +++ 添加日志 +++
|
||||
emit('create-tag', trimmedInput);
|
||||
// 父组件负责创建、更新 availableTags prop,然后 TagInput 会响应式更新
|
||||
// 父组件也负责将新创建的 tag ID 添加到 modelValue
|
||||
}
|
||||
inputValue.value = ''; // 清空输入框
|
||||
showSuggestions.value = false; // 创建或选择后隐藏建议
|
||||
@@ -137,12 +141,11 @@ const handleKeyDown = async (event: KeyboardEvent) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 选中一个标签 (来自建议列表或 Enter 创建)
|
||||
const selectTag = (tag: TagInfo) => {
|
||||
if (!selectedTagIds.value.includes(tag.id)) {
|
||||
// 使用 .push() 来触发 watch
|
||||
const updatedIds = [...selectedTagIds.value, tag.id];
|
||||
selectedTagIds.value = updatedIds;
|
||||
// 选中一个标签 (来自建议列表或 Enter 匹配)
|
||||
const selectTag = (tag: GenericTag) => {
|
||||
if (!selectedTagIds.value.includes(tag.id)) {
|
||||
// 使用 .push() 来触发 watch -> emit update:modelValue
|
||||
selectedTagIds.value = [...selectedTagIds.value, tag.id];
|
||||
}
|
||||
inputValue.value = ''; // 清空输入框
|
||||
showSuggestions.value = false; // 选择后隐藏建议
|
||||
@@ -150,27 +153,22 @@ const selectTag = (tag: TagInfo) => {
|
||||
};
|
||||
|
||||
// 仅从本地选择中移除一个标签 (点击选中标签的 'x' 或 Backspace)
|
||||
const removeTagLocally = (tagToRemove: TagInfo) => {
|
||||
selectedTagIds.value = selectedTagIds.value.filter(id => id !== tagToRemove.id);
|
||||
const removeTagLocally = (tagToRemove: GenericTag) => {
|
||||
// This will trigger the watch and emit update:modelValue
|
||||
selectedTagIds.value = selectedTagIds.value.filter(id => id !== tagToRemove.id);
|
||||
};
|
||||
|
||||
// 处理全局删除标签 (点击标签上的 'x' 图标) - 这是全局删除
|
||||
const handleDeleteTagGlobally = async (tagToDelete: TagInfo) => {
|
||||
// 弹出确认框,防止误删
|
||||
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
|
||||
const success = await tagsStore.deleteTag(tagToDelete.id);
|
||||
if (success) {
|
||||
// deleteTag 内部会 fetchTags, store 会更新
|
||||
// selectedTagIds 会因为 watch props.modelValue 而自动更新 (如果父组件也更新了)
|
||||
// 或者手动从 selectedTagIds 中移除 (更保险)
|
||||
removeTagLocally(tagToDelete);
|
||||
// 可选:显示成功提示
|
||||
} else {
|
||||
// 可选:显示错误提示
|
||||
alert(t('tags.errorDelete', { error: tagsStore.error || '未知错误' }));
|
||||
}
|
||||
}
|
||||
};
|
||||
// 处理全局删除标签 (点击标签上的 'x' 图标) - Emit event
|
||||
const handleDeleteTagGlobally = (tagToDelete: GenericTag) => {
|
||||
console.log(`[TagInput] handleDeleteTagGlobally called for tag ID: ${tagToDelete.id}, Name: ${tagToDelete.name}`); // +++ 添加日志 +++
|
||||
// Emit event for parent to handle deletion confirmation and API call
|
||||
console.log(`[TagInput] Emitting delete-tag with ID: ${tagToDelete.id}`); // +++ 添加日志 +++
|
||||
emit('delete-tag', tagToDelete.id);
|
||||
// Parent should handle confirmation, call store action, and update modelValue/availableTags
|
||||
// We might still want to remove it locally immediately for better UX,
|
||||
// but relying on parent updating modelValue is cleaner.
|
||||
// removeTagLocally(tagToDelete); // Optional: remove locally immediately
|
||||
}; // Remove the extra closing brace here if it exists, ensure function closes correctly
|
||||
|
||||
</script>
|
||||
|
||||
@@ -186,13 +184,15 @@ const handleDeleteTagGlobally = async (tagToDelete: TagInfo) => {
|
||||
@click.stop="removeTagLocally(tag)"
|
||||
:title="t('tags.removeSelection')"
|
||||
>×</button>
|
||||
<!-- Only show delete button if allowDelete is true -->
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 p-0 bg-transparent border-none cursor-pointer text-text-alt hover:text-error text-xs leading-none"
|
||||
@click.stop="handleDeleteTagGlobally(tag)"
|
||||
:title="t('tags.deleteTagGlobally')"
|
||||
v-if="allowDelete"
|
||||
type="button"
|
||||
class="ml-1 p-0 bg-transparent border-none cursor-pointer text-text-alt hover:text-error text-xs leading-none"
|
||||
@click.stop="handleDeleteTagGlobally(tag)"
|
||||
:title="t('tags.deleteTagGlobally')"
|
||||
>
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -217,9 +217,10 @@ const handleDeleteTagGlobally = async (tagToDelete: TagInfo) => {
|
||||
>
|
||||
{{ suggestion.name }}
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="isLoading" class="absolute bottom-[-1.5em] left-0 text-xs text-text-secondary mt-1">{{ t('tags.loading') }}</div>
|
||||
<div v-if="error" class="absolute bottom-[-1.5em] left-0 text-xs text-error mt-1">{{ t('tags.error', { error: error }) }}</div>
|
||||
</ul>
|
||||
<!-- Remove isLoading and error display as they are no longer managed here -->
|
||||
<!-- <div v-if="isLoading" ...></div> -->
|
||||
<!-- <div v-if="error" ...></div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -118,11 +118,27 @@ const filteredAndGroupedConnections = computed(() => {
|
||||
const tagMap = new Map(tags.value.map(tag => [tag.id, tag]));
|
||||
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
||||
|
||||
// 1. 过滤连接
|
||||
// 1. 过滤连接 (New logic: filter by connection name, host, OR tag name)
|
||||
const filteredConnections = connections.value.filter(conn => {
|
||||
const nameMatch = conn.name && conn.name.toLowerCase().includes(lowerSearchTerm);
|
||||
const hostMatch = conn.host.toLowerCase().includes(lowerSearchTerm);
|
||||
return nameMatch || hostMatch;
|
||||
// Check connection name
|
||||
if (conn.name && conn.name.toLowerCase().includes(lowerSearchTerm)) {
|
||||
return true;
|
||||
}
|
||||
// Check connection host
|
||||
if (conn.host.toLowerCase().includes(lowerSearchTerm)) {
|
||||
return true;
|
||||
}
|
||||
// Check associated tag names
|
||||
if (conn.tag_ids && conn.tag_ids.length > 0) {
|
||||
for (const tagId of conn.tag_ids) {
|
||||
const tag = tagMap.get(tagId); // Use the existing tagMap
|
||||
if (tag && tag.name.toLowerCase().includes(lowerSearchTerm)) {
|
||||
return true; // Match found in tag name
|
||||
}
|
||||
}
|
||||
}
|
||||
// No match found
|
||||
return false;
|
||||
});
|
||||
|
||||
// 2. 分组过滤后的连接
|
||||
|
||||
@@ -906,7 +906,13 @@
|
||||
"command": "Command:",
|
||||
"commandPlaceholder": "e.g., ls -alh /home/user",
|
||||
"errorCommandRequired": "Command cannot be empty",
|
||||
"add": "Add"
|
||||
"add": "Add",
|
||||
"tags": "Tags:",
|
||||
"tagsPlaceholder": "Select or create tags..."
|
||||
},
|
||||
"untagged": "Untagged",
|
||||
"tags": {
|
||||
"clickToEditTag": "Click to edit tag name"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
|
||||
@@ -492,6 +492,8 @@
|
||||
"empty": "クイックコマンドはありません。'+'ボタンをクリックして作成してください!",
|
||||
"form": {
|
||||
"add": "追加",
|
||||
"tags": "タグ:",
|
||||
"tagsPlaceholder": "タグを選択または作成...",
|
||||
"command": "コマンド:",
|
||||
"commandPlaceholder": "例:ls -alh /home/user",
|
||||
"errorCommandRequired": "コマンドは空にできません",
|
||||
@@ -500,6 +502,10 @@
|
||||
"titleAdd": "クイックコマンドの追加",
|
||||
"titleEdit": "クイックコマンドの編集"
|
||||
},
|
||||
"untagged": "タグなし",
|
||||
"tags": {
|
||||
"clickToEditTag": "クリックしてタグ名を編集"
|
||||
},
|
||||
"searchPlaceholder": "名前またはコマンドを検索...",
|
||||
"sortByName": "名前",
|
||||
"sortByUsage": "使用頻度",
|
||||
|
||||
@@ -909,7 +909,13 @@
|
||||
"command": "指令:",
|
||||
"commandPlaceholder": "例如:ls -alh /home/user",
|
||||
"errorCommandRequired": "指令内容不能为空",
|
||||
"add": "添加"
|
||||
"add": "添加",
|
||||
"tags": "标签:",
|
||||
"tagsPlaceholder": "选择或创建标签..."
|
||||
},
|
||||
"untagged": "未标记",
|
||||
"tags": {
|
||||
"clickToEditTag": "点击编辑标签名称"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import { useUiNotificationsStore } from './uiNotifications.store';
|
||||
|
||||
// 定义快捷指令标签接口 (与后端 QuickCommandTag 对应)
|
||||
export interface QuickCommandTag {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export const useQuickCommandTagsStore = defineStore('quickCommandTags', () => {
|
||||
const tags = ref<QuickCommandTag[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
|
||||
// 获取快捷指令标签列表 (带缓存)
|
||||
async function fetchTags() {
|
||||
const cacheKey = 'quickCommandTagsCache';
|
||||
error.value = null;
|
||||
|
||||
// 1. 尝试从 localStorage 加载缓存
|
||||
try {
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
if (cachedData) {
|
||||
console.log('[QuickCmdTagStore] Loading quick command tags from cache.');
|
||||
tags.value = JSON.parse(cachedData);
|
||||
isLoading.value = false;
|
||||
} else {
|
||||
isLoading.value = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[QuickCmdTagStore] Failed to load or parse cache:', e);
|
||||
localStorage.removeItem(cacheKey);
|
||||
isLoading.value = true;
|
||||
}
|
||||
|
||||
// 2. 后台获取最新数据
|
||||
isLoading.value = true;
|
||||
try {
|
||||
console.log('[QuickCmdTagStore] Fetching latest quick command tags from server...');
|
||||
// 使用新的 API 端点
|
||||
const response = await apiClient.get<QuickCommandTag[]>('/quick-command-tags');
|
||||
const freshData = response.data;
|
||||
const freshDataString = JSON.stringify(freshData);
|
||||
|
||||
// 3. 对比并更新
|
||||
const currentDataString = JSON.stringify(tags.value);
|
||||
if (currentDataString !== freshDataString) {
|
||||
console.log('[QuickCmdTagStore] Tags data changed, updating state and cache.');
|
||||
tags.value = freshData;
|
||||
localStorage.setItem(cacheKey, freshDataString);
|
||||
} else {
|
||||
console.log('[QuickCmdTagStore] Tags data is up-to-date.');
|
||||
}
|
||||
error.value = null;
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdTagStore] Failed to fetch tags:', err);
|
||||
error.value = err.response?.data?.message || err.message || '获取快捷指令标签列表失败';
|
||||
if (error.value) { // Check if error.value is not null
|
||||
uiNotificationsStore.showError(error.value); // 显示错误通知
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新快捷指令标签 (添加后清除缓存)
|
||||
async function addTag(name: string): Promise<QuickCommandTag | null> {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
// 使用新的 API 端点
|
||||
const response = await apiClient.post<{ message: string, tag: QuickCommandTag }>('/quick-command-tags', { name });
|
||||
const newTag = response.data.tag;
|
||||
localStorage.removeItem('quickCommandTagsCache'); // 清除缓存
|
||||
await fetchTags(); // 重新获取以更新列表
|
||||
uiNotificationsStore.showSuccess('快捷指令标签已添加');
|
||||
return newTag;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdTagStore] Failed to add tag:', err);
|
||||
error.value = err.response?.data?.message || err.message || '添加快捷指令标签失败';
|
||||
if (error.value) { // Check if error.value is not null
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新快捷指令标签
|
||||
async function updateTag(id: number, name: string): Promise<boolean> {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
// 使用新的 API 端点
|
||||
await apiClient.put(`/quick-command-tags/${id}`, { name });
|
||||
localStorage.removeItem('quickCommandTagsCache');
|
||||
await fetchTags();
|
||||
uiNotificationsStore.showSuccess('快捷指令标签已更新');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdTagStore] Failed to update tag:', err);
|
||||
error.value = err.response?.data?.message || err.message || '更新快捷指令标签失败';
|
||||
if (error.value) { // Check if error.value is not null
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除快捷指令标签
|
||||
async function deleteTag(id: number): Promise<boolean> {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
// 使用新的 API 端点
|
||||
await apiClient.delete(`/quick-command-tags/${id}`);
|
||||
localStorage.removeItem('quickCommandTagsCache');
|
||||
await fetchTags();
|
||||
uiNotificationsStore.showSuccess('快捷指令标签已删除');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdTagStore] Failed to delete tag:', err);
|
||||
error.value = err.response?.data?.message || err.message || '删除快捷指令标签失败';
|
||||
if (error.value) { // Check if error.value is not null
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tags,
|
||||
isLoading,
|
||||
error,
|
||||
fetchTags,
|
||||
addTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
};
|
||||
});
|
||||
@@ -1,62 +1,224 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue'; // Import watch
|
||||
import { useUiNotificationsStore } from './uiNotifications.store';
|
||||
import type { QuickCommand } from '../types/quick-commands.types'; // 引入本地 QuickCommand 类型
|
||||
import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store'; // +++ Import new tag store +++
|
||||
import { useI18n } from 'vue-i18n'; // +++ Import i18n for "Untagged" +++
|
||||
// Assuming QuickCommand type in types includes tagIds now, or define it here
|
||||
// import type { QuickCommand } from '../types/quick-commands.types';
|
||||
|
||||
// 定义前端使用的快捷指令接口 (可以与后端一致)
|
||||
export type QuickCommandFE = QuickCommand;
|
||||
// 定义前端使用的快捷指令接口 (包含 tagIds)
|
||||
export interface QuickCommandFE { // Renamed from QuickCommand if necessary
|
||||
id: number;
|
||||
name: string | null;
|
||||
command: string;
|
||||
usage_count: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
tagIds: number[]; // +++ Add tagIds +++
|
||||
}
|
||||
|
||||
// 定义排序类型
|
||||
export type QuickCommandSortByType = 'name' | 'usage_count';
|
||||
|
||||
// 定义分组后的数据结构
|
||||
export interface GroupedQuickCommands {
|
||||
groupName: string;
|
||||
tagId: number | null; // null for "Untagged" group
|
||||
commands: QuickCommandFE[];
|
||||
}
|
||||
|
||||
// +++ localStorage key for expanded groups +++
|
||||
const EXPANDED_GROUPS_STORAGE_KEY = 'quickCommandsExpandedGroups';
|
||||
|
||||
export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
const quickCommandsList = ref<QuickCommandFE[]>([]);
|
||||
const quickCommandsList = ref<QuickCommandFE[]>([]); // Should now contain QuickCommandFE with tagIds
|
||||
const searchTerm = ref('');
|
||||
const sortBy = ref<QuickCommandSortByType>('name'); // 默认按名称排序
|
||||
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
|
||||
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Inject new tag store +++
|
||||
const { t } = useI18n(); // +++ For "Untagged" translation +++
|
||||
const selectedIndex = ref<number>(-1); // Index in the flatVisibleCommands list
|
||||
|
||||
// +++ State for expanded groups +++
|
||||
const expandedGroups = ref<Record<string, boolean>>({});
|
||||
|
||||
// --- Getters ---
|
||||
|
||||
// 计算属性:根据搜索词过滤和排序指令
|
||||
const filteredAndSortedCommands = computed(() => {
|
||||
// +++ 重写 Getter: 过滤、分组、排序指令 +++
|
||||
const filteredAndGroupedCommands = computed((): GroupedQuickCommands[] => {
|
||||
const term = searchTerm.value.toLowerCase().trim();
|
||||
let filtered = quickCommandsList.value;
|
||||
const allTags = quickCommandTagsStore.tags; // 获取快捷指令专属标签
|
||||
const tagMap = new Map(allTags.map(tag => [tag.id, tag.name]));
|
||||
const untaggedGroupName = t('quickCommands.untagged', '未标记'); // 获取 "未标记" 的翻译
|
||||
|
||||
// 1. 过滤 (New logic: filter by command name, command content, OR tag name)
|
||||
let filtered = quickCommandsList.value;
|
||||
if (term) {
|
||||
filtered = filtered.filter(cmd =>
|
||||
(cmd.name && cmd.name.toLowerCase().includes(term)) ||
|
||||
cmd.command.toLowerCase().includes(term)
|
||||
);
|
||||
filtered = filtered.filter(cmd => {
|
||||
// Check command name
|
||||
if (cmd.name && cmd.name.toLowerCase().includes(term)) {
|
||||
return true;
|
||||
}
|
||||
// Check command content
|
||||
if (cmd.command.toLowerCase().includes(term)) {
|
||||
return true;
|
||||
}
|
||||
// Check associated tag names
|
||||
if (cmd.tagIds && cmd.tagIds.length > 0) {
|
||||
for (const tagId of cmd.tagIds) {
|
||||
const tagName = tagMap.get(tagId);
|
||||
if (tagName && tagName.toLowerCase().includes(term)) {
|
||||
return true; // Match found in tag name
|
||||
}
|
||||
}
|
||||
}
|
||||
// No match found
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Pinia store getter 中直接排序可能不是最佳实践,但这里为了简单起见先这样实现
|
||||
// 更好的方式可能是在 fetch 时就按需排序,或者在组件层排序
|
||||
// 注意:这里直接修改 ref 数组的顺序,如果需要在多处使用不同排序,需要创建副本
|
||||
// return [...filtered].sort((a, b) => {
|
||||
// if (sortBy.value === 'usage_count') {
|
||||
// // 按使用次数降序,次数相同按名称升序
|
||||
// if (b.usage_count !== a.usage_count) {
|
||||
// return b.usage_count - a.usage_count;
|
||||
// }
|
||||
// }
|
||||
// // 默认或次数相同时按名称升序 (null 名称排在前面)
|
||||
// const nameA = a.name ?? '';
|
||||
// const nameB = b.name ?? '';
|
||||
// return nameA.localeCompare(nameB);
|
||||
// });
|
||||
// **修正:Getter 不应修改原始数组,返回过滤后的即可,排序由 fetch 控制**
|
||||
return filtered;
|
||||
// 2. 分组
|
||||
const groups: Record<string, { commands: QuickCommandFE[], tagId: number | null }> = {};
|
||||
const untaggedCommands: QuickCommandFE[] = [];
|
||||
|
||||
filtered.forEach(cmd => {
|
||||
let isTagged = false;
|
||||
if (cmd.tagIds && cmd.tagIds.length > 0) {
|
||||
cmd.tagIds.forEach(tagId => {
|
||||
const tagName = tagMap.get(tagId);
|
||||
if (tagName) {
|
||||
if (!groups[tagName]) {
|
||||
groups[tagName] = { commands: [], tagId: tagId };
|
||||
// 初始化展开状态 (如果未定义,默认为 true)
|
||||
if (expandedGroups.value[tagName] === undefined) {
|
||||
expandedGroups.value[tagName] = true;
|
||||
}
|
||||
}
|
||||
// 避免重复添加(如果一个指令有多个相同标签ID? 不太可能但做个防御)
|
||||
if (!groups[tagName].commands.some(c => c.id === cmd.id)) {
|
||||
groups[tagName].commands.push(cmd);
|
||||
}
|
||||
isTagged = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!isTagged) {
|
||||
untaggedCommands.push(cmd);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 排序分组内指令 & 格式化输出
|
||||
const sortedGroupNames = Object.keys(groups).sort((a, b) => a.localeCompare(b));
|
||||
const result: GroupedQuickCommands[] = sortedGroupNames.map(groupName => {
|
||||
const groupData = groups[groupName];
|
||||
// 组内排序
|
||||
groupData.commands.sort((a, b) => {
|
||||
if (sortBy.value === 'usage_count') {
|
||||
if (b.usage_count !== a.usage_count) return b.usage_count - a.usage_count;
|
||||
}
|
||||
const nameA = a.name ?? a.command; // Fallback to command if name is null
|
||||
const nameB = b.name ?? b.command;
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
return {
|
||||
groupName: groupName,
|
||||
tagId: groupData.tagId,
|
||||
commands: groupData.commands
|
||||
};
|
||||
});
|
||||
|
||||
// 4. 处理未标记的分组
|
||||
if (untaggedCommands.length > 0) {
|
||||
// 初始化展开状态 (如果未定义,默认为 true)
|
||||
if (expandedGroups.value[untaggedGroupName] === undefined) {
|
||||
expandedGroups.value[untaggedGroupName] = true;
|
||||
}
|
||||
// 组内排序
|
||||
untaggedCommands.sort((a, b) => {
|
||||
if (sortBy.value === 'usage_count') {
|
||||
if (b.usage_count !== a.usage_count) return b.usage_count - a.usage_count;
|
||||
}
|
||||
const nameA = a.name ?? a.command;
|
||||
const nameB = b.name ?? b.command;
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
result.push({
|
||||
groupName: untaggedGroupName,
|
||||
tagId: null,
|
||||
commands: untaggedCommands
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// +++ 新增 Getter: 获取当前可见的扁平指令列表 (用于键盘导航) +++
|
||||
const flatVisibleCommands = computed((): QuickCommandFE[] => {
|
||||
const flatList: QuickCommandFE[] = [];
|
||||
filteredAndGroupedCommands.value.forEach(group => {
|
||||
// 只添加已展开分组中的指令
|
||||
if (expandedGroups.value[group.groupName]) {
|
||||
flatList.push(...group.commands);
|
||||
}
|
||||
});
|
||||
return flatList;
|
||||
});
|
||||
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
// NEW: Action to select the next command in the filtered list
|
||||
// +++ Load initial expanded groups state from localStorage +++
|
||||
const loadExpandedGroups = () => {
|
||||
try {
|
||||
const storedState = localStorage.getItem(EXPANDED_GROUPS_STORAGE_KEY);
|
||||
if (storedState) {
|
||||
const parsedState = JSON.parse(storedState);
|
||||
if (typeof parsedState === 'object' && parsedState !== null) {
|
||||
expandedGroups.value = parsedState;
|
||||
console.log('[QuickCmdStore] Loaded expanded groups state from localStorage.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[QuickCmdStore] Failed to load or parse expanded groups state:', e);
|
||||
localStorage.removeItem(EXPANDED_GROUPS_STORAGE_KEY);
|
||||
}
|
||||
// Default to empty object if no valid state found
|
||||
expandedGroups.value = {};
|
||||
};
|
||||
|
||||
// +++ Save expanded groups state to localStorage +++
|
||||
const saveExpandedGroups = () => {
|
||||
try {
|
||||
localStorage.setItem(EXPANDED_GROUPS_STORAGE_KEY, JSON.stringify(expandedGroups.value));
|
||||
} catch (e) {
|
||||
console.error('[QuickCmdStore] Failed to save expanded groups state:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// +++ Watch for changes and save +++
|
||||
watch(expandedGroups, saveExpandedGroups, { deep: true });
|
||||
|
||||
// +++ Action to toggle group expansion +++
|
||||
const toggleGroup = (groupName: string) => {
|
||||
// Ensure the group exists in the state before toggling
|
||||
if (expandedGroups.value[groupName] === undefined) {
|
||||
// Default to true if toggling a group that wasn't explicitly set (e.g., newly appeared group)
|
||||
expandedGroups.value[groupName] = false; // Start collapsed if toggled first time? Or true? Let's start true.
|
||||
} else {
|
||||
expandedGroups.value[groupName] = !expandedGroups.value[groupName];
|
||||
}
|
||||
// The watcher will automatically save the state
|
||||
// Reset selection when a group is toggled? Maybe not necessary.
|
||||
// selectedIndex.value = -1;
|
||||
};
|
||||
|
||||
// Action to select the next command in the *visible* flat list
|
||||
const selectNextCommand = () => {
|
||||
const commands = filteredAndSortedCommands.value;
|
||||
const commands = flatVisibleCommands.value; // Use the flat visible list
|
||||
if (commands.length === 0) {
|
||||
selectedIndex.value = -1;
|
||||
return;
|
||||
@@ -64,9 +226,9 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
selectedIndex.value = (selectedIndex.value + 1) % commands.length;
|
||||
};
|
||||
|
||||
// NEW: Action to select the previous command in the filtered list
|
||||
// Action to select the previous command in the *visible* flat list
|
||||
const selectPreviousCommand = () => {
|
||||
const commands = filteredAndSortedCommands.value;
|
||||
const commands = flatVisibleCommands.value; // Use the flat visible list
|
||||
if (commands.length === 0) {
|
||||
selectedIndex.value = -1;
|
||||
return;
|
||||
@@ -74,37 +236,48 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
selectedIndex.value = (selectedIndex.value - 1 + commands.length) % commands.length;
|
||||
};
|
||||
|
||||
// 从后端获取快捷指令 (带缓存和排序)
|
||||
// 从后端获取快捷指令 (包含 tagIds,不再发送 sortBy)
|
||||
const fetchQuickCommands = async () => {
|
||||
const cacheKey = 'quickCommandsCache';
|
||||
// 将排序方式加入缓存键,确保不同排序有不同缓存
|
||||
const cacheKeyWithSort = `${cacheKey}_${sortBy.value}`;
|
||||
error.value = null; // 重置错误
|
||||
// 简化缓存:只缓存原始列表,不再区分排序
|
||||
const cacheKey = 'quickCommandsListCache';
|
||||
error.value = null;
|
||||
|
||||
// 1. 尝试从 localStorage 加载缓存
|
||||
try {
|
||||
const cachedData = localStorage.getItem(cacheKeyWithSort);
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
if (cachedData) {
|
||||
console.log(`[QuickCmdStore] Loading commands from cache (sort: ${sortBy.value}).`);
|
||||
quickCommandsList.value = JSON.parse(cachedData);
|
||||
isLoading.value = false; // 先显示缓存
|
||||
console.log(`[QuickCmdStore] Loading commands from cache.`);
|
||||
// 确保解析后的数据符合 QuickCommandFE 结构 (特别是 tagIds)
|
||||
const parsedData = JSON.parse(cachedData) as QuickCommandFE[];
|
||||
// 基本验证,确保 tagIds 是数组
|
||||
if (Array.isArray(parsedData) && parsedData.every(item => Array.isArray(item.tagIds))) {
|
||||
quickCommandsList.value = parsedData;
|
||||
isLoading.value = false;
|
||||
} else {
|
||||
console.warn('[QuickCmdStore] Cached data format invalid, ignoring cache.');
|
||||
localStorage.removeItem(cacheKey);
|
||||
isLoading.value = true;
|
||||
}
|
||||
} else {
|
||||
isLoading.value = true; // 无缓存,初始加载
|
||||
isLoading.value = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[QuickCmdStore] Failed to load or parse commands cache:', e);
|
||||
localStorage.removeItem(cacheKeyWithSort); // 解析失败则移除缓存
|
||||
isLoading.value = true; // 缓存无效,需要加载
|
||||
localStorage.removeItem(cacheKey);
|
||||
isLoading.value = true;
|
||||
}
|
||||
|
||||
// 2. 后台获取最新数据
|
||||
isLoading.value = true; // 标记正在后台获取
|
||||
isLoading.value = true;
|
||||
try {
|
||||
console.log(`[QuickCmdStore] Fetching latest commands from server (sort: ${sortBy.value})...`);
|
||||
const response = await apiClient.get<QuickCommandFE[]>('/quick-commands', {
|
||||
params: { sortBy: sortBy.value }
|
||||
});
|
||||
const freshData = response.data;
|
||||
console.log(`[QuickCmdStore] Fetching latest commands from server...`);
|
||||
// 不再发送 sortBy 参数
|
||||
const response = await apiClient.get<QuickCommandFE[]>('/quick-commands');
|
||||
// 确保返回的数据包含 tagIds 数组
|
||||
const freshData = response.data.map(cmd => ({
|
||||
...cmd,
|
||||
tagIds: Array.isArray(cmd.tagIds) ? cmd.tagIds : [] // 确保 tagIds 是数组
|
||||
}));
|
||||
const freshDataString = JSON.stringify(freshData);
|
||||
|
||||
// 3. 对比并更新
|
||||
@@ -112,37 +285,37 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
if (currentDataString !== freshDataString) {
|
||||
console.log('[QuickCmdStore] Commands data changed, updating state and cache.');
|
||||
quickCommandsList.value = freshData;
|
||||
localStorage.setItem(cacheKeyWithSort, freshDataString); // 更新对应排序的缓存
|
||||
localStorage.setItem(cacheKey, freshDataString); // 更新缓存
|
||||
} else {
|
||||
console.log('[QuickCmdStore] Commands data is up-to-date.');
|
||||
}
|
||||
error.value = null; // 清除错误
|
||||
error.value = null;
|
||||
} catch (err: any) {
|
||||
console.error('[QuickCmdStore] 获取快捷指令失败:', err);
|
||||
error.value = err.response?.data?.message || '获取快捷指令时发生错误';
|
||||
// 保留缓存数据,仅设置错误状态
|
||||
uiNotificationsStore.showError(error.value ?? '未知错误');
|
||||
if (error.value) {
|
||||
uiNotificationsStore.showError(error.value);
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false; // 加载完成
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 清除所有排序的快捷指令缓存
|
||||
// 清除快捷指令列表缓存
|
||||
const clearQuickCommandsCache = () => {
|
||||
const cacheKeyBase = 'quickCommandsCache';
|
||||
// 移除两种排序的缓存
|
||||
localStorage.removeItem(`${cacheKeyBase}_name`);
|
||||
localStorage.removeItem(`${cacheKeyBase}_usage_count`);
|
||||
console.log('[QuickCmdStore] Cleared all quick commands caches.');
|
||||
localStorage.removeItem('quickCommandsListCache');
|
||||
console.log('[QuickCmdStore] Cleared quick commands list cache.');
|
||||
};
|
||||
|
||||
|
||||
// 添加快捷指令 (添加后清除缓存)
|
||||
const addQuickCommand = async (name: string | null, command: string): Promise<boolean> => {
|
||||
// 添加快捷指令 (发送 tagIds)
|
||||
const addQuickCommand = async (name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
|
||||
try {
|
||||
await apiClient.post('/quick-commands', { name, command });
|
||||
clearQuickCommandsCache(); // 清除所有排序缓存
|
||||
await fetchQuickCommands(); // 刷新当前排序的列表和缓存
|
||||
// 在请求体中包含 tagIds
|
||||
const response = await apiClient.post<{ message: string, command: QuickCommandFE }>('/quick-commands', { name, command, tagIds });
|
||||
// 后端现在返回完整的 command 对象,可以直接使用或触发刷新
|
||||
clearQuickCommandsCache(); // 清除缓存
|
||||
await fetchQuickCommands(); // 重新获取以确保数据同步
|
||||
uiNotificationsStore.showSuccess('快捷指令已添加');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
@@ -153,12 +326,14 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 更新快捷指令
|
||||
const updateQuickCommand = async (id: number, name: string | null, command: string): Promise<boolean> => {
|
||||
// 更新快捷指令 (发送 tagIds)
|
||||
const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
|
||||
try {
|
||||
await apiClient.put(`/quick-commands/${id}`, { name, command });
|
||||
clearQuickCommandsCache(); // 清除所有排序缓存
|
||||
await fetchQuickCommands(); // 刷新当前排序的列表和缓存
|
||||
// 在请求体中包含 tagIds (即使是 undefined 也要发送,让后端知道是否要更新)
|
||||
const response = await apiClient.put<{ message: string, command: QuickCommandFE }>(`/quick-commands/${id}`, { name, command, tagIds });
|
||||
// 后端现在返回完整的 command 对象
|
||||
clearQuickCommandsCache(); // 清除缓存
|
||||
await fetchQuickCommands(); // 重新获取以确保数据同步
|
||||
uiNotificationsStore.showSuccess('快捷指令已更新');
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
@@ -214,12 +389,12 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
selectedIndex.value = -1; // Reset selection when search term changes
|
||||
};
|
||||
|
||||
// 设置排序方式并重新获取数据
|
||||
const setSortBy = async (newSortBy: QuickCommandSortByType) => {
|
||||
// 设置排序方式 (只更新本地状态,不再重新获取数据)
|
||||
const setSortBy = (newSortBy: QuickCommandSortByType) => {
|
||||
if (sortBy.value !== newSortBy) {
|
||||
sortBy.value = newSortBy;
|
||||
// 排序方式改变,不需要清除缓存,fetchQuickCommands 会读取对应排序的缓存或重新获取
|
||||
await fetchQuickCommands();
|
||||
// 排序现在由 filteredAndGroupedCommands getter 处理,无需重新 fetch
|
||||
selectedIndex.value = -1; // Reset selection when sort changes
|
||||
}
|
||||
};
|
||||
|
||||
@@ -236,8 +411,10 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
sortBy,
|
||||
isLoading,
|
||||
error,
|
||||
filteredAndSortedCommands, // 使用计算属性
|
||||
selectedIndex, // NEW: Expose selected index
|
||||
filteredAndGroupedCommands, // Expose the grouped data
|
||||
flatVisibleCommands, // Expose the flat visible list for navigation logic if needed outside
|
||||
selectedIndex, // Index within flatVisibleCommands
|
||||
expandedGroups, // Expose expanded groups state
|
||||
fetchQuickCommands,
|
||||
addQuickCommand,
|
||||
updateQuickCommand,
|
||||
@@ -245,8 +422,64 @@ export const useQuickCommandsStore = defineStore('quickCommands', () => {
|
||||
incrementUsage,
|
||||
setSearchTerm,
|
||||
setSortBy,
|
||||
selectNextCommand, // NEW: Expose action
|
||||
selectPreviousCommand, // NEW: Expose action
|
||||
resetSelection, // Ensure resetSelection is exported
|
||||
selectNextCommand,
|
||||
selectPreviousCommand,
|
||||
resetSelection,
|
||||
toggleGroup, // +++ Expose toggleGroup action +++
|
||||
loadExpandedGroups, // +++ Expose load action +++
|
||||
|
||||
// +++ Action to assign a tag to multiple commands +++
|
||||
async assignCommandsToTagAction(commandIds: number[], tagId: number): Promise<boolean> {
|
||||
if (!commandIds || commandIds.length === 0) {
|
||||
console.warn('[Store] assignCommandsToTagAction: No command IDs provided.');
|
||||
return false;
|
||||
}
|
||||
isLoading.value = true; // Use the store's isLoading state
|
||||
error.value = null; // Use the store's error state
|
||||
try {
|
||||
const response = await apiClient.post('/quick-commands/bulk-assign-tag', { commandIds, tagId });
|
||||
if (response.data.success) {
|
||||
console.log(`[Store] Successfully assigned tag ${tagId} to ${commandIds.length} commands via API.`);
|
||||
|
||||
// --- Manual state update for immediate UI feedback ---
|
||||
let updatedCount = 0;
|
||||
commandIds.forEach(cmdId => {
|
||||
const commandIndex = quickCommandsList.value.findIndex(cmd => cmd.id === cmdId);
|
||||
if (commandIndex !== -1) {
|
||||
const command = quickCommandsList.value[commandIndex];
|
||||
// Ensure tagIds exists and add the new tagId if not already present
|
||||
if (!Array.isArray(command.tagIds)) {
|
||||
command.tagIds = [];
|
||||
}
|
||||
if (!command.tagIds.includes(tagId)) {
|
||||
command.tagIds.push(tagId);
|
||||
updatedCount++;
|
||||
}
|
||||
} else {
|
||||
console.warn(`[Store] assignCommandsToTagAction: Command ID ${cmdId} not found in local list for manual update.`);
|
||||
}
|
||||
});
|
||||
console.log(`[Store] Manually updated tagIds for ${updatedCount} commands in local state.`);
|
||||
// --- End manual state update ---
|
||||
|
||||
// Optionally, still fetch for full consistency, but UI should update based on manual change first.
|
||||
// clearQuickCommandsCache();
|
||||
// await fetchQuickCommands();
|
||||
return true;
|
||||
} else {
|
||||
// This case might not happen if backend throws errors instead
|
||||
error.value = response.data.message || '批量分配标签失败 (未知)';
|
||||
if (error.value) uiNotificationsStore.showError(error.value); // Check if error.value is not null
|
||||
return false;
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[Store] Error assigning tag to commands:', err);
|
||||
error.value = err.response?.data?.message || err.message || '批量分配标签时发生网络或服务器错误';
|
||||
if (error.value) uiNotificationsStore.showError(error.value); // Check if error.value is not null
|
||||
return false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -26,49 +26,89 @@
|
||||
</div>
|
||||
<!-- List Area -->
|
||||
<div class="flex-grow overflow-y-auto p-2">
|
||||
<!-- Loading State -->
|
||||
<!-- Loading State (Only show if loading AND no commands are displayed yet) -->
|
||||
<div v-if="isLoading && filteredAndSortedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||
<i class="fas fa-spinner fa-spin text-xl mb-2"></i>
|
||||
<p>{{ t('common.loading', '加载中...') }}</p>
|
||||
<!-- Loading State (Show if loading and no groups are ready yet) -->
|
||||
<div v-if="isLoading && filteredAndGroupedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||
<i class="fas fa-spinner fa-spin text-xl mb-2"></i>
|
||||
<p>{{ t('common.loading', '加载中...') }}</p>
|
||||
</div>
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="filteredAndSortedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||
<i class="fas fa-bolt text-xl mb-2"></i>
|
||||
<p class="mb-3">{{ $t('quickCommands.empty', '没有快捷指令。') }}</p>
|
||||
<button @click="openAddForm" class="px-4 py-2 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||
<!-- Empty State (Show if not loading and no groups exist) -->
|
||||
<div v-else-if="!isLoading && filteredAndGroupedCommands.length === 0" class="p-6 text-center text-text-secondary text-sm flex flex-col items-center justify-center h-full">
|
||||
<i class="fas fa-bolt text-xl mb-2"></i>
|
||||
<p class="mb-3">{{ $t('quickCommands.empty', '没有快捷指令。') }}</p>
|
||||
<button @click="openAddForm" class="px-4 py-2 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||
{{ $t('quickCommands.addFirst', '创建第一个快捷指令') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Command List -->
|
||||
<ul v-else class="list-none p-0 m-0" ref="commandListRef">
|
||||
<li
|
||||
v-for="(cmd, index) in filteredAndSortedCommands"
|
||||
:key="cmd.id"
|
||||
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||
:class="{ 'bg-primary/20 font-medium': index === storeSelectedIndex }"
|
||||
@click="executeCommand(cmd)"
|
||||
>
|
||||
<!-- Command Info -->
|
||||
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
||||
<span v-if="cmd.name" class="font-medium text-sm truncate mb-0.5 text-foreground">{{ cmd.name }}</span>
|
||||
<span class="text-xs truncate font-mono" :class="{ 'text-sm': !cmd.name, 'text-text-secondary': true }">{{ cmd.command }}</span>
|
||||
</div>
|
||||
<!-- Actions (Show on Hover) -->
|
||||
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
||||
<!-- Usage Count -->
|
||||
<span class="text-xs bg-border px-1.5 py-0.5 rounded mr-2 text-text-secondary" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
|
||||
<!-- Edit Button -->
|
||||
<button @click.stop="openEditForm(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-primary" :title="$t('common.edit', '编辑')">
|
||||
<i class="fas fa-edit text-sm"></i>
|
||||
</button>
|
||||
<!-- Delete Button -->
|
||||
<button @click.stop="confirmDelete(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-error" :title="$t('common.delete', '删除')">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Grouped Command List -->
|
||||
<div v-else class="list-none p-0 m-0" ref="commandListContainerRef"> <!-- Changed ref name -->
|
||||
<div v-for="groupData in filteredAndGroupedCommands" :key="groupData.groupName" class="mb-1 last:mb-0">
|
||||
<!-- Group Header -->
|
||||
<!-- Group Header - Modified for inline editing -->
|
||||
<div
|
||||
class="group px-3 py-2 font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
|
||||
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
|
||||
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
|
||||
>
|
||||
<i
|
||||
:class="['fas', expandedGroups[groupData.groupName] ? 'fa-chevron-down' : 'fa-chevron-right', 'mr-2 w-4 text-center text-text-secondary group-hover:text-foreground transition-transform duration-200 ease-in-out', {'transform rotate-0': !expandedGroups[groupData.groupName]}]"
|
||||
@click.stop="toggleGroup(groupData.groupName)"
|
||||
class="cursor-pointer flex-shrink-0"
|
||||
></i>
|
||||
<!-- Editing State -->
|
||||
<input
|
||||
v-if="editingTagId === (groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
||||
:key="groupData.tagId === null ? 'untagged-input' : `tag-input-${groupData.tagId}`"
|
||||
:ref="(el) => setTagInputRef(el, groupData.tagId === null ? 'untagged' : groupData.tagId)"
|
||||
type="text"
|
||||
v-model="editedTagName"
|
||||
class="text-sm bg-input border border-primary rounded px-1 py-0 w-full"
|
||||
@blur="finishEditingTag"
|
||||
@keydown.enter.prevent="finishEditingTag"
|
||||
@keydown.esc.prevent="cancelEditingTag"
|
||||
@click.stop
|
||||
/>
|
||||
<!-- Display State -->
|
||||
<span
|
||||
v-else
|
||||
class="text-sm inline-block overflow-hidden text-ellipsis whitespace-nowrap flex-grow"
|
||||
:class="{ 'cursor-pointer hover:underline': true }"
|
||||
:title="t('quickCommands.tags.clickToEditTag', '点击编辑标签')"
|
||||
@click.stop="startEditingTag(groupData.tagId, groupData.groupName)"
|
||||
>
|
||||
{{ groupData.groupName }}
|
||||
</span>
|
||||
<!-- Optional: Add count? -->
|
||||
<!-- <span v-if="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId)" class="ml-auto text-xs text-text-secondary pl-2">({{ groupData.commands.length }})</span> -->
|
||||
</div>
|
||||
<!-- Command Items List (only show if expanded) -->
|
||||
<ul v-show="quickCommandsStore.expandedGroups[groupData.groupName]" class="list-none p-0 m-0 pl-3">
|
||||
<li
|
||||
v-for="(cmd) in groupData.commands"
|
||||
:key="cmd.id"
|
||||
:data-command-id="cmd.id"
|
||||
class="group flex justify-between items-center px-3 py-2.5 mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
|
||||
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
|
||||
@click="executeCommand(cmd)"
|
||||
>
|
||||
<!-- Command Info (Structure remains the same) -->
|
||||
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
|
||||
<span v-if="cmd.name" class="font-medium text-sm truncate mb-0.5 text-foreground">{{ cmd.name }}</span>
|
||||
<span class="text-xs truncate font-mono" :class="{ 'text-sm': !cmd.name, 'text-text-secondary': true }">{{ cmd.command }}</span>
|
||||
</div>
|
||||
<!-- Actions (Structure remains the same) -->
|
||||
<div class="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
||||
<span class="text-xs bg-border px-1.5 py-0.5 rounded mr-2 text-text-secondary" :title="t('quickCommands.usageCount', '使用次数')">{{ cmd.usage_count }}</span>
|
||||
<button @click.stop="openEditForm(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-primary" :title="$t('common.edit', '编辑')">
|
||||
<i class="fas fa-edit text-sm"></i>
|
||||
</button>
|
||||
<button @click.stop="confirmDelete(cmd)" class="p-1.5 rounded hover:bg-black/10 transition-colors duration-150 text-text-secondary hover:text-error" :title="$t('common.delete', '删除')">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,13 +124,15 @@
|
||||
<script setup lang="ts">
|
||||
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 { useQuickCommandsStore, type QuickCommandFE, type QuickCommandSortByType, type GroupedQuickCommands } from '../stores/quickCommands.store'; // Import GroupedQuickCommands
|
||||
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; // +++ Import the new tag store +++
|
||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import AddEditQuickCommandForm from '../components/AddEditQuickCommandForm.vue'; // 导入表单组件
|
||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
||||
|
||||
const quickCommandsStore = useQuickCommandsStore();
|
||||
const quickCommandTagsStore = useQuickCommandTagsStore(); // +++ Instantiate the new tag store +++
|
||||
const uiNotificationsStore = useUiNotificationsStore(); // 如果需要显示通知
|
||||
const { t } = useI18n();
|
||||
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
||||
@@ -99,16 +141,32 @@ const hoveredItemId = ref<number | null>(null);
|
||||
const isFormVisible = ref(false);
|
||||
const commandToEdit = ref<QuickCommandFE | null>(null);
|
||||
// const selectedIndex = ref<number>(-1); // REMOVED: Use store's selectedIndex
|
||||
const commandListRef = ref<HTMLUListElement | null>(null); // Ref for the command list UL
|
||||
const commandListContainerRef = ref<HTMLDivElement | null>(null); // Changed ref name to match template
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null); // +++ Ref for the search input +++
|
||||
let unregisterFocus: (() => void) | null = null; // +++ 保存注销函数 +++
|
||||
|
||||
// +++ State for inline tag editing +++
|
||||
const editingTagId = ref<number | null | 'untagged'>(null);
|
||||
const editedTagName = ref('');
|
||||
const tagInputRefs = ref(new Map<string | number, HTMLInputElement | null>());
|
||||
|
||||
// --- 从 Store 获取状态和 Getter ---
|
||||
const searchTerm = computed(() => quickCommandsStore.searchTerm);
|
||||
const sortBy = computed(() => quickCommandsStore.sortBy);
|
||||
const filteredAndSortedCommands = computed(() => quickCommandsStore.filteredAndSortedCommands);
|
||||
// Use the new grouped getter
|
||||
const filteredAndGroupedCommands = computed(() => quickCommandsStore.filteredAndGroupedCommands);
|
||||
const isLoading = computed(() => quickCommandsStore.isLoading);
|
||||
const { selectedIndex: storeSelectedIndex } = storeToRefs(quickCommandsStore); // Get selectedIndex reactively
|
||||
// selectedIndex now refers to the index within the flatVisibleCommands list
|
||||
// Also get expandedGroups reactively for the template
|
||||
const { selectedIndex: storeSelectedIndex, flatVisibleCommands, expandedGroups } = storeToRefs(quickCommandsStore);
|
||||
|
||||
// --- Helper function for selection check ---
|
||||
const isCommandSelected = (commandId: number): boolean => {
|
||||
if (storeSelectedIndex.value < 0 || !flatVisibleCommands.value[storeSelectedIndex.value]) {
|
||||
return false;
|
||||
}
|
||||
return flatVisibleCommands.value[storeSelectedIndex.value].id === commandId;
|
||||
};
|
||||
|
||||
// --- 事件定义 ---
|
||||
const emit = defineEmits<{
|
||||
@@ -116,10 +174,30 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
quickCommandsStore.fetchQuickCommands(); // 组件挂载时获取数据
|
||||
onMounted(async () => { // Make onMounted async
|
||||
// Load expanded groups state first
|
||||
quickCommandsStore.loadExpandedGroups();
|
||||
// Then fetch commands (which might initialize expandedGroups for new groups)
|
||||
await quickCommandsStore.fetchQuickCommands();
|
||||
// Also fetch the quick command tags using the correct store instance
|
||||
await quickCommandTagsStore.fetchTags();
|
||||
});
|
||||
|
||||
// +++ Watcher to focus input when editing starts +++
|
||||
watch(editingTagId, async (newId) => {
|
||||
if (newId !== null) {
|
||||
await nextTick();
|
||||
const inputRef = tagInputRefs.value.get(newId);
|
||||
if (inputRef) {
|
||||
inputRef.focus();
|
||||
inputRef.select();
|
||||
} else {
|
||||
console.error(`[QuickCmdView] Watcher: Input ref for ID ${newId} not found.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// +++ 注册/注销自定义聚焦动作 +++
|
||||
onMounted(() => {
|
||||
// +++ 保存返回的注销函数 +++
|
||||
@@ -140,21 +218,25 @@ const updateSearchTerm = (event: Event) => {
|
||||
// selectedIndex.value = -1; // REMOVED: Store handles resetting index
|
||||
};
|
||||
|
||||
// 滚动到选中的项目
|
||||
const scrollToSelected = async (index: number) => { // Accept index as argument
|
||||
await nextTick(); // 等待 DOM 更新
|
||||
if (index < 0 || !commandListRef.value) return;
|
||||
// +++ 重构滚动逻辑 +++
|
||||
const scrollToSelected = async (index: number) => {
|
||||
await nextTick(); // 等待 DOM 更新
|
||||
if (index < 0 || !commandListContainerRef.value || !flatVisibleCommands.value[index]) return;
|
||||
|
||||
const listElement = commandListRef.value;
|
||||
const selectedItem = listElement.children[index] as HTMLLIElement;
|
||||
const selectedCommandId = flatVisibleCommands.value[index].id;
|
||||
const listContainer = commandListContainerRef.value;
|
||||
|
||||
if (selectedItem) {
|
||||
// 使用 scrollIntoView 使元素可见,滚动最小距离
|
||||
selectedItem.scrollIntoView({
|
||||
behavior: 'smooth', // 可以使用 'auto' 来实现即时滚动
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
// Find the element using the data attribute
|
||||
const selectedElement = listContainer.querySelector(`li[data-command-id="${selectedCommandId}"]`) as HTMLLIElement;
|
||||
|
||||
if (selectedElement) {
|
||||
selectedElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
});
|
||||
} else {
|
||||
console.warn(`[QuickCmdView] scrollToSelected: Could not find element for command ID ${selectedCommandId}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes in the store's selectedIndex and scroll
|
||||
@@ -162,12 +244,13 @@ watch(storeSelectedIndex, (newIndex) => {
|
||||
scrollToSelected(newIndex);
|
||||
});
|
||||
|
||||
// Renamed function to avoid conflict if needed, and added logic
|
||||
// Keyboard navigation now operates on the flat visible list
|
||||
const handleSearchInputKeydown = (event: KeyboardEvent) => {
|
||||
const commands = filteredAndSortedCommands.value;
|
||||
if (!commands.length) return;
|
||||
// Use flatVisibleCommands for navigation logic
|
||||
const commands = flatVisibleCommands.value;
|
||||
if (!commands.length) return;
|
||||
|
||||
switch (event.key) {
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
quickCommandsStore.selectNextCommand(); // Use store action
|
||||
@@ -193,16 +276,37 @@ const handleSearchInputBlur = () => {
|
||||
setTimeout(() => {
|
||||
// 检查焦点是否还在组件内部的其他可聚焦元素上(例如按钮)
|
||||
// 如果焦点移出整个组件区域,则重置选择
|
||||
if (document.activeElement !== searchInputRef.value && !commandListRef.value?.contains(document.activeElement)) {
|
||||
quickCommandsStore.resetSelection();
|
||||
if (document.activeElement !== searchInputRef.value && !commandListContainerRef.value?.contains(document.activeElement)) {
|
||||
quickCommandsStore.resetSelection();
|
||||
}
|
||||
}, 100); // 短暂延迟
|
||||
}, 100); // 短暂延迟
|
||||
};
|
||||
|
||||
// 切换排序方式
|
||||
// 切换排序方式 (Action remains the same, store handles the logic change)
|
||||
const toggleSortBy = () => {
|
||||
const newSortBy = sortBy.value === 'name' ? 'usage_count' : 'name';
|
||||
quickCommandsStore.setSortBy(newSortBy);
|
||||
const newSortBy = sortBy.value === 'name' ? 'usage_count' : 'name';
|
||||
quickCommandsStore.setSortBy(newSortBy);
|
||||
};
|
||||
|
||||
// +++ Action to toggle group expansion +++
|
||||
const toggleGroup = (groupName: string) => {
|
||||
quickCommandsStore.toggleGroup(groupName);
|
||||
// After toggling, selection might become invalid if the selected item is now hidden
|
||||
// Reset selection or check if the selected item is still visible
|
||||
nextTick(() => { // Wait for DOM update potentially caused by v-show
|
||||
const selectedCmdId = storeSelectedIndex.value >= 0 && flatVisibleCommands.value[storeSelectedIndex.value]
|
||||
? flatVisibleCommands.value[storeSelectedIndex.value].id
|
||||
: null;
|
||||
if (selectedCmdId !== null) {
|
||||
const newIndex = flatVisibleCommands.value.findIndex(cmd => cmd.id === selectedCmdId);
|
||||
if (newIndex === -1) { // Selected item is no longer visible
|
||||
quickCommandsStore.resetSelection();
|
||||
} else {
|
||||
// Update index if it shifted, though usually reset is safer/simpler
|
||||
// storeSelectedIndex.value = newIndex;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 计算排序按钮的 title 和 icon
|
||||
@@ -259,5 +363,117 @@ const focusSearchInput = (): boolean => {
|
||||
};
|
||||
defineExpose({ focusSearchInput });
|
||||
|
||||
// +++ Methods for inline tag editing +++
|
||||
const setTagInputRef = (el: any, id: string | number) => {
|
||||
if (el) {
|
||||
tagInputRefs.value.set(id, el as HTMLInputElement);
|
||||
} else {
|
||||
tagInputRefs.value.delete(id);
|
||||
}
|
||||
};
|
||||
|
||||
const startEditingTag = (tagId: number | null, currentName: string) => {
|
||||
editingTagId.value = tagId === null ? 'untagged' : tagId;
|
||||
editedTagName.value = tagId === null ? '' : currentName; // Clear input for "Untagged"
|
||||
// Focus logic is handled by the watcher
|
||||
};
|
||||
|
||||
const finishEditingTag = async () => {
|
||||
const currentEditingId = editingTagId.value;
|
||||
const newName = editedTagName.value.trim();
|
||||
const originalGroup = filteredAndGroupedCommands.value.find(g => g.tagId === currentEditingId); // Find original group data
|
||||
|
||||
// Basic validation
|
||||
if (newName === '' && currentEditingId !== 'untagged') {
|
||||
cancelEditingTag();
|
||||
return;
|
||||
}
|
||||
if (newName === '' && currentEditingId === 'untagged') {
|
||||
cancelEditingTag();
|
||||
return;
|
||||
}
|
||||
|
||||
let operationSuccess = false;
|
||||
|
||||
try {
|
||||
if (currentEditingId === 'untagged') {
|
||||
// --- Create new tag and assign commands ---
|
||||
console.log(`[QuickCmdView] Creating new tag: ${newName}`);
|
||||
const newTag = await quickCommandTagsStore.addTag(newName);
|
||||
if (newTag) {
|
||||
operationSuccess = true;
|
||||
uiNotificationsStore.showSuccess(t('quickCommands.tags.createSuccess')); // Use specific translation key
|
||||
const untaggedGroup = filteredAndGroupedCommands.value.find(g => g.tagId === null);
|
||||
const commandIdsToAssign = untaggedGroup ? untaggedGroup.commands.map(c => c.id) : [];
|
||||
|
||||
if (commandIdsToAssign.length > 0) {
|
||||
console.log(`[QuickCmdView] Assigning ${commandIdsToAssign.length} commands to new tag ID: ${newTag.id}`);
|
||||
console.log(`[QuickCmdView] Command IDs to assign: ${JSON.stringify(commandIdsToAssign)}`); // +++ 添加日志 +++
|
||||
// Call the store action to assign commands to the new tag
|
||||
const assignSuccess = await quickCommandsStore.assignCommandsToTagAction(commandIdsToAssign, newTag.id);
|
||||
if (assignSuccess) {
|
||||
// Success/Error Notifications and list refresh are handled within the store action
|
||||
console.log(`[QuickCmdView] assignCommandsToTagAction reported success.`);
|
||||
} else {
|
||||
console.error(`[QuickCmdView] assignCommandsToTagAction reported failure.`);
|
||||
// Optionally show a specific error here if the store action doesn't cover all cases
|
||||
}
|
||||
// Remove TODO and temporary warning/refresh
|
||||
// console.warn("TODO: Implement assignCommandsToTagAction in quickCommands.store and backend");
|
||||
// uiNotificationsStore.showWarning("标签已创建,但指令分配功能尚未实现");
|
||||
// await quickCommandsStore.fetchQuickCommands(); // Store action handles refresh
|
||||
} else {
|
||||
uiNotificationsStore.showInfo(t('quickCommands.tags.noCommandsToAssign'));
|
||||
}
|
||||
|
||||
// Update expanded group state
|
||||
const untaggedGroupName = t('quickCommands.untagged', '未标记');
|
||||
if (expandedGroups.value[untaggedGroupName] !== undefined) {
|
||||
const currentState = expandedGroups.value[untaggedGroupName];
|
||||
delete expandedGroups.value[untaggedGroupName]; // Remove old key
|
||||
expandedGroups.value[newName] = currentState; // Add new key
|
||||
}
|
||||
}
|
||||
// addTag failure handled in store
|
||||
} else if (typeof currentEditingId === 'number') {
|
||||
// --- Update existing tag ---
|
||||
const originalTagName = originalGroup?.groupName;
|
||||
if (!originalTagName) {
|
||||
console.error(`[QuickCmdView] Cannot find original group name for tag ID ${currentEditingId}`);
|
||||
cancelEditingTag();
|
||||
return;
|
||||
}
|
||||
if (originalTagName === newName) {
|
||||
operationSuccess = true; // No change needed
|
||||
} else {
|
||||
console.log(`[QuickCmdView] Updating tag ID ${currentEditingId} from "${originalTagName}" to "${newName}"`);
|
||||
const updateResult = await quickCommandTagsStore.updateTag(currentEditingId, newName);
|
||||
if (updateResult) {
|
||||
operationSuccess = true;
|
||||
// uiNotificationsStore.showSuccess(t('quickCommands.tags.updateSuccess'));
|
||||
// Update expanded group state
|
||||
if (expandedGroups.value[originalTagName] !== undefined) {
|
||||
const currentState = expandedGroups.value[originalTagName];
|
||||
delete expandedGroups.value[originalTagName];
|
||||
expandedGroups.value[newName] = currentState;
|
||||
}
|
||||
// Refresh commands to reflect potential grouping changes if names clashed etc.
|
||||
await quickCommandsStore.fetchQuickCommands();
|
||||
}
|
||||
// updateTag failure handled in store
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("[QuickCmdView] Error during finishEditingTag:", error);
|
||||
uiNotificationsStore.showError(t('common.unexpectedError'));
|
||||
} finally {
|
||||
editingTagId.value = null; // Exit edit mode regardless of success
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEditingTag = () => {
|
||||
editingTagId.value = null;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user