Files
nexus-terminal/packages/frontend/src/stores/quickCommands.store.ts
T
Baobhan Sith fea90fb6ab update
2025-05-13 10:24:02 +08:00

488 lines
21 KiB
TypeScript

import { defineStore } from 'pinia';
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
import { ref, computed, watch } from 'vue'; // Import watch
import { useUiNotificationsStore } from './uiNotifications.store';
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';
// 定义前端使用的快捷指令接口 (包含 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' | 'last_used';
// 定义分组后的数据结构
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[]>([]); // 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 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 ---
// +++ 重写 Getter: 过滤、分组、排序指令 +++
const filteredAndGroupedCommands = computed((): GroupedQuickCommands[] => {
const term = searchTerm.value.toLowerCase().trim();
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 => {
// 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;
});
}
// 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;
} else if (sortBy.value === 'last_used') {
if (b.updated_at !== a.updated_at) return b.updated_at - a.updated_at;
}
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;
} else if (sortBy.value === 'last_used') {
if (b.updated_at !== a.updated_at) return b.updated_at - a.updated_at;
}
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 ---
// +++ 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 = flatVisibleCommands.value; // Use the flat visible list
if (commands.length === 0) {
selectedIndex.value = -1;
return;
}
selectedIndex.value = (selectedIndex.value + 1) % commands.length;
};
// Action to select the previous command in the *visible* flat list
const selectPreviousCommand = () => {
const commands = flatVisibleCommands.value; // Use the flat visible list
if (commands.length === 0) {
selectedIndex.value = -1;
return;
}
selectedIndex.value = (selectedIndex.value - 1 + commands.length) % commands.length;
};
// 从后端获取快捷指令 (包含 tagIds,不再发送 sortBy)
const fetchQuickCommands = async () => {
// 简化缓存:只缓存原始列表,不再区分排序
const cacheKey = 'quickCommandsListCache';
error.value = null;
// 1. 尝试从 localStorage 加载缓存
try {
const cachedData = localStorage.getItem(cacheKey);
if (cachedData) {
// 确保解析后的数据符合 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;
}
} catch (e) {
console.error('[QuickCmdStore] Failed to load or parse commands cache:', e);
localStorage.removeItem(cacheKey);
isLoading.value = true;
}
// 2. 后台获取最新数据
isLoading.value = true;
try {
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. 对比并更新
const currentDataString = JSON.stringify(quickCommandsList.value);
if (currentDataString !== freshDataString) {
console.log('[QuickCmdStore] Commands data changed, updating state and cache.');
quickCommandsList.value = freshData;
localStorage.setItem(cacheKey, freshDataString); // 更新缓存
} else {
}
error.value = null;
} catch (err: any) {
console.error('[QuickCmdStore] 获取快捷指令失败:', err);
error.value = err.response?.data?.message || '获取快捷指令时发生错误';
if (error.value) {
uiNotificationsStore.showError(error.value);
}
} finally {
isLoading.value = false;
}
};
// 清除快捷指令列表缓存
const clearQuickCommandsCache = () => {
localStorage.removeItem('quickCommandsListCache');
console.log('[QuickCmdStore] Cleared quick commands list cache.');
};
// 添加快捷指令 (发送 tagIds)
const addQuickCommand = async (name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
try {
// 在请求体中包含 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) {
console.error('添加快捷指令失败:', err);
const message = err.response?.data?.message || '添加快捷指令时发生错误';
uiNotificationsStore.showError(message);
return false;
}
};
// 更新快捷指令 (发送 tagIds)
const updateQuickCommand = async (id: number, name: string | null, command: string, tagIds?: number[]): Promise<boolean> => {
try {
// 在请求体中包含 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) {
console.error('更新快捷指令失败:', err);
const message = err.response?.data?.message || '更新快捷指令时发生错误';
uiNotificationsStore.showError(message);
return false;
}
};
// 删除快捷指令
const deleteQuickCommand = async (id: number) => {
try {
await apiClient.delete(`/quick-commands/${id}`);
clearQuickCommandsCache(); // 清除所有排序缓存
// 从本地列表中移除
const index = quickCommandsList.value.findIndex(cmd => cmd.id === id);
if (index !== -1) {
quickCommandsList.value.splice(index, 1);
}
uiNotificationsStore.showSuccess('快捷指令已删除');
} catch (err: any) {
console.error('删除快捷指令失败:', err);
const message = err.response?.data?.message || '删除快捷指令时发生错误';
uiNotificationsStore.showError(message);
}
};
// 增加使用次数 (调用 API,然后更新本地数据)
const incrementUsage = async (id: number) => {
try {
await apiClient.post(`/quick-commands/${id}/increment-usage`); // 使用 apiClient
// 更新本地计数,避免重新请求整个列表
const command = quickCommandsList.value.find(cmd => cmd.id === id);
if (command) {
command.usage_count += 1;
// 如果当前是按使用次数排序,可能需要重新排序或刷新列表
if (sortBy.value === 'usage_count') {
// 清除所有排序缓存并重新获取当前排序
clearQuickCommandsCache();
await fetchQuickCommands();
}
}
} catch (err: any) {
console.error('增加使用次数失败:', err);
// 这里可以选择不提示用户错误,因为这是一个后台操作
}
};
// 设置搜索词
const setSearchTerm = (term: string) => {
searchTerm.value = term;
selectedIndex.value = -1; // Reset selection when search term changes
};
// 设置排序方式 (只更新本地状态,不再重新获取数据)
const setSortBy = (newSortBy: QuickCommandSortByType) => {
if (sortBy.value !== newSortBy) {
sortBy.value = newSortBy;
// 排序现在由 filteredAndGroupedCommands getter 处理,无需重新 fetch
selectedIndex.value = -1; // Reset selection when sort changes
}
};
// Action to reset the selection
const resetSelection = () => {
selectedIndex.value = -1;
};
// Removed duplicate resetSelection definition
return {
quickCommandsList,
searchTerm,
sortBy,
isLoading,
error,
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,
deleteQuickCommand,
incrementUsage,
setSearchTerm,
setSortBy,
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;
}
},
};
});