重构(前端): 持久化快速命令排序和密码切换

添加持久化排序字段并重新排序快速命令和标签的端点,更新前端以支持手动拖放排序,并为连接和凭据表单添加密码可见性切换。此外,将 SSH 连接测试作为连接列表中的默认操作,并刷新相关模块文档和更改日志。
This commit is contained in:
yinjianm
2026-04-19 02:50:44 +08:00
parent 00d7c6c2f3
commit 8ce007a305
33 changed files with 1996 additions and 975 deletions
+11 -10
View File
@@ -1597,6 +1597,17 @@ onBeforeUnmount(() => {
<span>{{ t('connections.actions.connect', '连接') }}</span>
</button>
<button
v-if="conn.type === 'SSH'"
:disabled="getSingleTestButtonInfo(conn.id, conn.type).disabled"
:title="getSingleTestButtonInfo(conn.id, conn.type).title"
class="px-4 py-2 rounded-lg border border-border bg-background text-foreground hover:bg-border transition-colors inline-flex items-center gap-2 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
@click.stop="handleTestSingleConnection(conn)"
>
<i :class="getSingleTestButtonInfo(conn.id, conn.type).iconClass"></i>
<span>{{ getSingleTestButtonInfo(conn.id, conn.type).text }}</span>
</button>
<div class="relative">
<button
@click.stop="toggleMoreMenu(conn.id)"
@@ -1618,16 +1629,6 @@ onBeforeUnmount(() => {
<i class="fas fa-pen w-4 text-center"></i>
<span>{{ t('connections.actions.edit', '编辑') }}</span>
</button>
<button
v-if="conn.type === 'SSH'"
:disabled="getSingleTestButtonInfo(conn.id, conn.type).disabled"
:title="getSingleTestButtonInfo(conn.id, conn.type).title"
class="w-full px-3 py-2 text-left text-sm text-foreground hover:bg-header transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
@click.stop="handleTestSingleConnection(conn); closeMoreMenu()"
>
<i :class="[getSingleTestButtonInfo(conn.id, conn.type).iconClass, 'w-4 text-center']"></i>
<span>{{ getSingleTestButtonInfo(conn.id, conn.type).text }}</span>
</button>
<button
class="w-full px-3 py-2 text-left text-sm text-foreground hover:bg-header transition-colors flex items-center gap-2"
@click.stop="handleCloneConnection(conn); closeMoreMenu()"
@@ -68,8 +68,17 @@
<div
class="group font-semibold flex items-center text-foreground rounded-md hover:bg-header/80 transition-colors duration-150"
:style="{ padding: isCompactMode ? `calc(0.25rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.5rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }"
:class="{ 'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) }"
:draggable="groupData.tagId !== null && !dragDisabledBySearch"
:class="{
'cursor-pointer': editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId),
'cursor-grab': groupData.tagId !== null && !dragDisabledBySearch,
'qc-drop-target': isGroupDropTarget(groupData.tagId),
}"
@click="editingTagId !== (groupData.tagId === null ? 'untagged' : groupData.tagId) ? toggleGroup(groupData.groupName) : null"
@dragstart="handleGroupDragStart($event, groupData.tagId)"
@dragover.prevent="handleGroupDragOver(groupData.tagId)"
@drop.prevent="handleGroupDrop(groupData.tagId)"
@dragend="resetDragState"
>
<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]}]"
@@ -114,10 +123,20 @@
:title="cmd.command"
class="group flex justify-between items-center mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
:style="{ padding: isCompactMode ? `calc(0.1rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.625rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }"
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
:draggable="!dragDisabledBySearch"
:class="{
'bg-primary/20 font-medium': isCommandSelected(cmd.id),
'cursor-grab': !dragDisabledBySearch,
'qc-drop-target': isCommandDropTarget(cmd.id, groupData.tagId),
'opacity-70': isDraggingCommand(cmd.id, groupData.tagId),
}"
@click="selectCommand(cmd.id)"
@dblclick="executeCommand(cmd)"
@contextmenu.prevent="showQuickCommandContextMenu($event, cmd)"
@dragstart="handleCommandDragStart($event, cmd.id, groupData.tagId)"
@dragover.prevent="handleCommandDragOver(cmd.id, groupData.tagId)"
@drop.prevent="handleCommandDrop(cmd.id, groupData.tagId)"
@dragend="resetDragState"
>
<!-- Command Info -->
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
@@ -162,10 +181,20 @@
:title="cmd.command"
class="group flex justify-between items-center mb-1 cursor-pointer rounded-md hover:bg-primary/10 transition-colors duration-150"
:style="{ padding: isCompactMode ? `calc(0.1rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` : `calc(0.625rem * var(--qc-row-size-multiplier)) calc(0.75rem * var(--qc-row-size-multiplier))` }"
:class="{ 'bg-primary/20 font-medium': isCommandSelected(cmd.id) }"
:draggable="!dragDisabledBySearch"
:class="{
'bg-primary/20 font-medium': isCommandSelected(cmd.id),
'cursor-grab': !dragDisabledBySearch,
'qc-drop-target': isCommandDropTarget(cmd.id, null),
'opacity-70': isDraggingCommand(cmd.id, null),
}"
@click="selectCommand(cmd.id)"
@dblclick="executeCommand(cmd)"
@contextmenu.prevent="showQuickCommandContextMenu($event, cmd)"
@dragstart="handleCommandDragStart($event, cmd.id, null)"
@dragover.prevent="handleCommandDragOver(cmd.id, null)"
@drop.prevent="handleCommandDrop(cmd.id, null)"
@dragend="resetDragState"
>
<!-- Command Info -->
<div class="flex flex-col overflow-hidden mr-2 flex-grow">
@@ -379,6 +408,17 @@ const flatFilteredCommands = computed(() => {
return quickCommandsStore.flatVisibleCommands;
});
const dragDisabledBySearch = computed(() => searchTerm.value.trim().length > 0);
const draggingGroupTagId = ref<number | null>(null);
const groupDropTargetTagId = ref<number | null>(null);
const draggingCommand = ref<{ commandId: number; groupTagId: number | null } | null>(null);
const commandDropTarget = ref<{ commandId: number; groupTagId: number | null } | null>(null);
const dragDisabledTitle = computed(() =>
dragDisabledBySearch.value
? t('quickCommands.dragDisabledBySearch', '搜索结果中不可拖动排序')
: t('quickCommands.dragCommand', '拖动排序快捷指令')
);
// --- Compact Mode ---
const isCompactMode = computed(() => quickCommandsCompactModeBoolean.value);
@@ -403,6 +443,163 @@ const selectCommand = (commandId: number) => {
// --- 生命周期钩子 ---
const resetDragState = () => {
draggingGroupTagId.value = null;
groupDropTargetTagId.value = null;
draggingCommand.value = null;
commandDropTarget.value = null;
};
const moveById = <T extends { id: number }>(items: T[], sourceId: number, targetId: number): T[] => {
const clonedItems = [...items];
const sourceIndex = clonedItems.findIndex((item) => item.id === sourceId);
const targetIndex = clonedItems.findIndex((item) => item.id === targetId);
if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) {
return clonedItems;
}
const [sourceItem] = clonedItems.splice(sourceIndex, 1);
clonedItems.splice(targetIndex, 0, sourceItem);
return clonedItems;
};
const isGroupDropTarget = (tagId: number | null): boolean =>
tagId !== null && groupDropTargetTagId.value === tagId;
const isDraggingCommand = (commandId: number, groupTagId: number | null): boolean =>
draggingCommand.value?.commandId === commandId && draggingCommand.value?.groupTagId === groupTagId;
const isCommandDropTarget = (commandId: number, groupTagId: number | null): boolean =>
commandDropTarget.value?.commandId === commandId && commandDropTarget.value?.groupTagId === groupTagId;
const handleGroupDragStart = (event: DragEvent, tagId: number | null) => {
if (dragDisabledBySearch.value || tagId === null) {
event.preventDefault();
return;
}
draggingGroupTagId.value = tagId;
groupDropTargetTagId.value = null;
event.dataTransfer?.setData('text/plain', String(tagId));
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
}
};
const handleGroupDragOver = (tagId: number | null) => {
if (draggingGroupTagId.value === null || dragDisabledBySearch.value || tagId === null || tagId === draggingGroupTagId.value) {
return;
}
groupDropTargetTagId.value = tagId;
};
const handleGroupDrop = async (tagId: number | null) => {
if (draggingGroupTagId.value === null || dragDisabledBySearch.value || tagId === null || tagId === draggingGroupTagId.value) {
resetDragState();
return;
}
const taggedGroups = filteredAndGroupedCommands.value
.filter((group) => group.tagId !== null)
.map((group) => ({ ...group, id: group.tagId as number }));
const reorderedGroups = moveById(taggedGroups, draggingGroupTagId.value, tagId);
const reorderedVisibleTagIds = reorderedGroups.map((group) => group.id);
const globalTagIds = [...quickCommandTagsStore.tags]
.sort((a, b) => (a.sort_order - b.sort_order) || (a.id - b.id))
.map((tag) => tag.id);
const visibleTagIdSet = new Set(reorderedVisibleTagIds);
let visibleIndex = 0;
const mergedTagIds = globalTagIds.map((existingTagId) => {
if (!visibleTagIdSet.has(existingTagId)) {
return existingTagId;
}
const nextVisibleTagId = reorderedVisibleTagIds[visibleIndex];
visibleIndex += 1;
return nextVisibleTagId ?? existingTagId;
});
await quickCommandTagsStore.reorderTags(mergedTagIds);
resetDragState();
};
const handleCommandDragStart = (event: DragEvent, commandId: number, groupTagId: number | null) => {
if (dragDisabledBySearch.value) {
event.preventDefault();
return;
}
draggingCommand.value = { commandId, groupTagId };
commandDropTarget.value = null;
event.dataTransfer?.setData('text/plain', String(commandId));
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
}
};
const handleCommandDragOver = (commandId: number, groupTagId: number | null) => {
if (!draggingCommand.value || dragDisabledBySearch.value) {
return;
}
if (draggingCommand.value.groupTagId !== groupTagId || draggingCommand.value.commandId === commandId) {
return;
}
commandDropTarget.value = { commandId, groupTagId };
};
const handleCommandDrop = async (commandId: number, groupTagId: number | null) => {
if (!draggingCommand.value || dragDisabledBySearch.value) {
resetDragState();
return;
}
if (draggingCommand.value.groupTagId !== groupTagId || draggingCommand.value.commandId === commandId) {
resetDragState();
return;
}
let currentCommands: QuickCommandFE[] = [];
if (showQuickCommandTagsBoolean.value) {
currentCommands = filteredAndGroupedCommands.value.find((group) => group.tagId === groupTagId)?.commands ?? [];
} else {
currentCommands = flatFilteredCommands.value;
}
const reorderedCommands = moveById(currentCommands, draggingCommand.value.commandId, commandId);
if (showQuickCommandTagsBoolean.value) {
if (groupTagId !== null) {
await quickCommandsStore.reorderCommandsInTag(groupTagId, reorderedCommands.map((item) => item.id));
} else {
const reorderedUntaggedIds = reorderedCommands.map((item) => item.id);
const globalCommandIds = [...quickCommandsStore.quickCommandsList]
.sort((a, b) => (a.sort_order - b.sort_order) || (a.id - b.id))
.map((command) => command.id);
let untaggedIndex = 0;
const mergedCommandIds = globalCommandIds.map((existingCommandId) => {
const command = quickCommandsStore.quickCommandsList.find((item) => item.id === existingCommandId);
if (!command || command.tagIds.length > 0) {
return existingCommandId;
}
const nextUntaggedId = reorderedUntaggedIds[untaggedIndex];
untaggedIndex += 1;
return nextUntaggedId ?? existingCommandId;
});
await quickCommandsStore.reorderQuickCommands(mergedCommandIds);
}
} else {
await quickCommandsStore.reorderQuickCommands(reorderedCommands.map((item) => item.id));
}
resetDragState();
};
onMounted(async () => { // Make onMounted async
// Load expanded groups state first
quickCommandsStore.loadExpandedGroups();
@@ -518,8 +715,10 @@ const handleSearchInputBlur = () => {
// 切换排序方式 (Action remains the same, store handles the logic change)
const toggleSortBy = () => {
const newSortBy = sortBy.value === 'name' ? 'last_used' : 'name';
quickCommandsStore.setSortBy(newSortBy);
const sortModes: QuickCommandSortByType[] = ['manual', 'name', 'last_used'];
const currentIndex = sortModes.indexOf(sortBy.value);
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % sortModes.length;
quickCommandsStore.setSortBy(sortModes[nextIndex]);
};
// +++ Action to toggle group expansion +++
@@ -896,3 +1095,10 @@ const handleQuickCommandMenuAction = async (action: QuickCommandContextAction, c
};
</script>
<style scoped>
.qc-drop-target {
outline: 1px dashed color-mix(in srgb, var(--color-primary, #3b82f6) 72%, transparent);
outline-offset: -2px;
}
</style>