feat(frontend): 增强连接树悬停操作与拖拽占位反馈

为连接管理页左侧标签树增加悬停工具按钮,
补充分隔标题行与节点拖拽目标高亮提示。

同时同步多语言文案与 helloagents 归档记录,
为后续真实重排交互预留可见反馈入口
This commit is contained in:
yinjianm
2026-03-25 23:37:33 +08:00
parent a6935a9c12
commit 9e49fcea61
10 changed files with 211 additions and 7 deletions
+3
View File
@@ -369,6 +369,9 @@
"scopeTreeSearch": "Search tag tree...",
"scopeSearchMode": "Matched paths are expanded",
"scopeTreeNoMatch": "No matching tree nodes",
"scopeDragPlaceholder": "Drag-reorder is reserved for now; only target placeholder feedback is shown.",
"scopePinAction": "Focus this scope",
"scopeDragAction": "Drag to reorder (reserved)",
"untaggedGroup": "Untagged",
"noUntaggedConnections": "No untagged connections found."
},
+3
View File
@@ -273,6 +273,9 @@
"scopeTreeSearch": "タグツリーを検索...",
"scopeSearchMode": "一致したパスを自動展開中",
"scopeTreeNoMatch": "一致するツリーノードはありません",
"scopeDragPlaceholder": "ドラッグ並べ替えは予約中で、現在は配置先のプレースホルダーのみ表示します。",
"scopePinAction": "この範囲にフォーカス",
"scopeDragAction": "ドラッグで並べ替え(予約)",
"table": {
"actions": "アクション",
"authMethod": "認証方法",
+3
View File
@@ -369,6 +369,9 @@
"scopeTreeSearch": "搜索标签树...",
"scopeSearchMode": "命中路径已自动展开",
"scopeTreeNoMatch": "没有匹配的树节点",
"scopeDragPlaceholder": "拖拽排序预留中,当前仅展示目标占位反馈。",
"scopePinAction": "定位到此范围",
"scopeDragAction": "拖拽重排(预留)",
"untaggedGroup": "未标记",
"noUntaggedConnections": "没有未标记的连接。"
},
@@ -95,6 +95,10 @@ const selectedConnectionIdsForBatch = ref<Set<number>>(new Set());
const showBatchEditForm = ref(false);
const isDeletingSelectedConnections = ref(false);
const expandedTreeNodes = ref<Record<string, boolean>>({});
const hoveredTreeNodeId = ref<ScopeId | null>(null);
const draggingTreeNodeId = ref<ScopeId | null>(null);
const dropTargetTreeNodeId = ref<ScopeId | null>(null);
const treeDragNoticeVisible = ref(false);
const connectionTestStates = ref<Map<number, ConnectionTestState>>(new Map());
const isTestingAll = ref(false);
@@ -537,6 +541,10 @@ const getTreeNodeRowClass = (node: TagTreeNode) => {
return 'bg-primary/15 text-foreground border-primary/30 shadow-sm';
}
if (dropTargetTreeNodeId.value === node.id) {
return 'border-amber-400/35 bg-amber-500/10 text-foreground shadow-sm';
}
if (matchingTreeNodeIds.value.has(node.id)) {
return 'border-emerald-400/30 bg-emerald-500/8 text-emerald-100 shadow-sm';
}
@@ -710,6 +718,38 @@ const clearTreeSearch = () => {
treeSearchQuery.value = '';
};
const setHoveredTreeNode = (nodeId: ScopeId | null) => {
hoveredTreeNodeId.value = nodeId;
};
const toggleTreeNodeFromAction = (node: TagTreeNode) => {
if (!node.expandable) {
return;
}
toggleTreeNode(node.id);
};
const startTreeDrag = (node: TagTreeNode) => {
draggingTreeNodeId.value = node.id;
dropTargetTreeNodeId.value = node.id;
treeDragNoticeVisible.value = true;
};
const updateTreeDropTarget = (node: TagTreeNode) => {
if (!draggingTreeNodeId.value || draggingTreeNodeId.value === node.id) {
return;
}
dropTargetTreeNodeId.value = node.id;
};
const finishTreeDrag = () => {
draggingTreeNodeId.value = null;
dropTargetTreeNodeId.value = null;
treeDragNoticeVisible.value = false;
};
const connectTo = (connection: ConnectionInfo) => {
sessionStore.handleConnectRequest(connection);
};
@@ -1067,8 +1107,12 @@ onBeforeUnmount(() => {
<div class="p-3 space-y-5">
<section>
<div class="px-2 mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-text-secondary/80">
{{ t('connections.scopePrimary', '视图') }}
<div class="px-2 mb-2 flex items-center gap-3">
<span class="text-[11px] font-semibold uppercase tracking-[0.18em] text-text-secondary/80 whitespace-nowrap">
{{ t('connections.scopePrimary', '视图') }}
</span>
<span class="h-px flex-1 bg-gradient-to-r from-border/80 to-transparent"></span>
<span class="text-[10px] text-text-secondary/70">{{ primaryScopeNodes.length }}</span>
</div>
<div class="space-y-1">
<button
@@ -1097,12 +1141,23 @@ onBeforeUnmount(() => {
</section>
<section>
<div class="px-2 mb-2 flex items-center justify-between gap-3 text-xs font-semibold uppercase tracking-[0.18em] text-text-secondary/80">
<span>{{ t('connections.scopeExplorerTitle', '标签资源管理器') }}</span>
<span class="text-[11px] tracking-normal normal-case text-text-secondary">{{ visibleTagTreeNodes.length }}</span>
<div class="px-2 mb-2 flex items-center gap-3">
<span class="text-[11px] font-semibold uppercase tracking-[0.18em] text-text-secondary/80 whitespace-nowrap">
{{ t('connections.scopeExplorerTitle', '标签资源管理器') }}
</span>
<span class="h-px flex-1 bg-gradient-to-r from-border/80 to-transparent"></span>
<span class="text-[10px] text-text-secondary/70">{{ visibleTagTreeNodes.length }}</span>
</div>
<div v-show="tagsSectionExpanded" class="space-y-2">
<div
v-if="treeDragNoticeVisible"
class="mx-2 rounded-xl border border-amber-400/25 bg-amber-500/10 px-3 py-2 text-[11px] text-amber-100 flex items-center gap-2"
>
<i class="fas fa-grip-lines"></i>
<span>{{ t('connections.scopeDragPlaceholder', '拖拽排序预留中,当前仅展示目标占位反馈') }}</span>
</div>
<div class="flex flex-wrap items-center gap-2 px-2">
<button
@click="expandAllTreeNodes"
@@ -1136,11 +1191,19 @@ onBeforeUnmount(() => {
v-for="node in visibleTagTreeNodes"
:key="node.id"
:class="[
'w-full flex items-center justify-between gap-3 rounded-xl border px-3 py-2.5 text-left transition-all duration-150',
'group w-full flex items-center justify-between gap-3 rounded-xl border px-3 py-2.5 text-left transition-all duration-150',
getTreeNodeRowClass(node),
node.count === 0 ? 'opacity-55' : ''
]"
:style="{ paddingLeft: `${0.75 + node.level * 1.05}rem` }"
draggable="true"
@mouseenter="setHoveredTreeNode(node.id)"
@mouseleave="setHoveredTreeNode(null)"
@dragstart="startTreeDrag(node)"
@dragenter.prevent="updateTreeDropTarget(node)"
@dragover.prevent
@dragend="finishTreeDrag"
@drop.prevent="finishTreeDrag"
>
<button
class="flex items-center gap-2 min-w-0 flex-1"
@@ -1164,6 +1227,36 @@ onBeforeUnmount(() => {
>
{{ node.count }}
</span>
<div
:class="[
'flex items-center gap-1 flex-shrink-0 transition-opacity duration-150',
hoveredTreeNodeId === node.id ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
]"
>
<button
v-if="node.expandable"
@click.stop="toggleTreeNodeFromAction(node)"
class="w-7 h-7 rounded-lg border border-border/60 bg-background text-text-secondary hover:bg-border hover:text-foreground transition-colors inline-flex items-center justify-center"
:title="(expandedTreeNodes[node.id] ?? true) ? t('common.collapse', '收起') : t('common.expand', '展开')"
>
<i :class="['fas text-[11px]', (expandedTreeNodes[node.id] ?? true) ? 'fa-compress-alt' : 'fa-expand-alt']"></i>
</button>
<button
@click.stop="selectScope(node.id)"
class="w-7 h-7 rounded-lg border border-border/60 bg-background text-text-secondary hover:bg-border hover:text-foreground transition-colors inline-flex items-center justify-center"
:title="t('connections.scopePinAction', '定位到此范围')"
>
<i class="fas fa-crosshairs text-[11px]"></i>
</button>
<button
@mousedown.stop
class="w-7 h-7 rounded-lg border border-border/60 bg-background text-text-secondary hover:bg-border hover:text-foreground transition-colors inline-flex items-center justify-center cursor-grab active:cursor-grabbing"
:title="t('connections.scopeDragAction', '拖拽重排(预留)')"
>
<i class="fas fa-grip-lines text-[11px]"></i>
</button>
</div>
</div>
</div>
</div>