diff --git a/src/api/admin/sysUserRelation.ts b/src/api/admin/sysUserRelation.ts index 9eb2ef22..b9b02bea 100755 --- a/src/api/admin/sysUserRelation.ts +++ b/src/api/admin/sysUserRelation.ts @@ -70,6 +70,10 @@ export interface RelationCreateForm { remark?: string } +export interface RelationUpdateForm extends RelationCreateForm { + relationId: string +} + // 关系图谱 export function getRelationGraph(userId?: string) { return request({ @@ -95,3 +99,12 @@ export function createRelation(data: RelationCreateForm) { data, }) } + +// 修改已有用户关系 +export function updateRelation(data: RelationUpdateForm) { + return request({ + url: "/admin/sysUserRelation/update", + method: "put", + data, + }) +} diff --git a/src/views/admin/system/relation-type/index.vue b/src/views/admin/system/relation-type/index.vue index bbbe82b5..6cec3e58 100644 --- a/src/views/admin/system/relation-type/index.vue +++ b/src/views/admin/system/relation-type/index.vue @@ -1,6 +1,24 @@ + + + + diff --git a/src/views/admin/system/user/relation-tree.vue b/src/views/admin/system/user/relation-tree.vue index 0617fa8f..bd8aa2a2 100644 --- a/src/views/admin/system/user/relation-tree.vue +++ b/src/views/admin/system/user/relation-tree.vue @@ -5,8 +5,8 @@
返回用户列表
-
{{ graphRoot?.displayName || "用户" }}的关系图谱
-
节点布局固定,拖动画布或缩放可查看完整关系
+
{{ entryNode?.displayName || "用户" }}的关系图谱
+
绿色标记当前进入用户,蓝色标记图谱根节点
@@ -90,13 +90,17 @@ class="relation-node" :class="{ 'is-root': data.graph.root, + 'is-entry': isEntryNode(data.graph), 'is-repeated': data.graph.repeated, 'is-selected': selectedNode?.nodeId === data.graph.nodeId, }" > -
核心用户
-
重复引用
+
+
图谱根节点
+
当前用户
+
重复引用
+
{{ getInitial(data.graph.displayName) }} @@ -108,13 +112,22 @@ {{ data.graph.userRoleName || data.graph.relationName || "关联用户" }}
- - - +
+ + + + + + +
@@ -151,7 +164,8 @@
{{ selectedNode.displayName }}
用户ID:{{ selectedNode.userId }}
- 根节点 + 当前用户 + 图谱根节点 重复节点 @@ -209,9 +223,14 @@ - - 以此人为对象新增关系用户 - +
+ + 修改关系 + + + 新增关系用户 + +
@@ -219,6 +238,7 @@ + @@ -230,6 +250,7 @@ import { MiniMap } from "@vue-flow/minimap" import { getRelationGraph, type RelationGraphNode } from "/@/api/admin/sysUserRelation" import { useMessage } from "/@/hooks/message" import RelationCreateUser from "./relation-create-user.vue" +import RelationEditForm from "./relation-edit-form.vue" import "@vue-flow/core/dist/style.css" import "@vue-flow/core/dist/theme-default.css" @@ -245,12 +266,14 @@ const route = useRoute() const router = useRouter() const loading = ref(false) const graphRoot = ref() +const entryNode = ref() const selectedNode = ref() const flowNodes = ref[]>([]) const flowEdges = ref([]) const graphDepth = ref(0) const flowStore = ref() const createUserRef = ref>() +const editFormRef = ref>() const nodeWidth = 230 const levelGap = 320 @@ -271,15 +294,17 @@ const loadGraph = async () => { try { const { data } = await getRelationGraph(String(userId.value)) graphRoot.value = data - selectedNode.value = data const graph = buildFlowElements(data) flowNodes.value = graph.nodes flowEdges.value = graph.edges graphDepth.value = graph.depth + entryNode.value = findGraphNodeByUserId(data, String(userId.value)) || data + selectedNode.value = entryNode.value await nextTick() fitGraph() } catch (error: any) { graphRoot.value = undefined + entryNode.value = undefined selectedNode.value = undefined flowNodes.value = [] flowEdges.value = [] @@ -374,10 +399,25 @@ const openCreateDialog = (target = selectedNode.value) => { createUserRef.value?.openDialog(target) } +const openEditDialog = (target = selectedNode.value) => { + if (!target || target.root) { + useMessage().warning("请选择一条需要修改的关系") + return + } + editFormRef.value?.openDialog(target, { + subjectUserName: getGraphUserName(target.subjectUserId), + objectUserName: getGraphUserName(target.objectUserId), + }) +} + const handleCreateSuccess = async () => { await loadGraph() } +const handleEditSuccess = async () => { + await loadGraph() +} + const goBack = () => { if (window.history.length > 1) { router.back() @@ -388,6 +428,23 @@ const goBack = () => { const getInitial = (name?: string) => name?.trim().slice(0, 1) || "用" +const isEntryNode = (node: RelationGraphNode) => entryNode.value?.nodeId === node.nodeId + +const findGraphNodeByUserId = (node: RelationGraphNode, targetUserId: string): RelationGraphNode | undefined => { + if (String(node.userId) === targetUserId) return node + for (const child of node.children || []) { + const targetNode = findGraphNodeByUserId(child, targetUserId) + if (targetNode) return targetNode + } + return undefined +} + +const getGraphUserName = (targetUserId?: string) => { + if (!targetUserId) return "用户" + const graphNode = flowNodes.value.find((node) => String((node.data as RelationNodeData).graph.userId) === String(targetUserId)) + return (graphNode?.data as RelationNodeData | undefined)?.graph.displayName || String(targetUserId) +} + const resolveImageUrl = (url?: string) => { if (!url) return "" return url.includes("http") ? url : `${import.meta.env.VITE_IMAGE_BASE_PATH}${url}` @@ -617,7 +674,7 @@ watch(userId, loadGraph, { immediate: true }) gap: 12px; width: 230px; height: 96px; - padding: 15px 38px 15px 14px; + padding: 15px 68px 15px 14px; box-sizing: border-box; background: rgba(255, 255, 255, 0.96); border: 2px solid #cbd5e1; @@ -633,7 +690,18 @@ watch(userId, loadGraph, { immediate: true }) box-shadow: 0 12px 30px rgba(37, 99, 235, 0.18); } +.relation-node.is-entry { + border-color: #10b981; + box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.16), 0 12px 30px rgba(5, 150, 105, 0.2); +} + +.relation-node.is-entry.is-selected { + transform: translateY(-2px); + box-shadow: 0 0 0 5px rgba(16, 185, 129, 0.2), 0 16px 34px rgba(5, 150, 105, 0.24); +} + .relation-node.is-root { + padding-right: 38px; color: #fff; background: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%); border-color: #1d4ed8; @@ -646,11 +714,17 @@ watch(userId, loadGraph, { immediate: true }) opacity: 0.82; } -.root-badge, -.repeat-badge { +.node-badges { position: absolute; top: -10px; left: 14px; + display: flex; + gap: 5px; +} + +.root-badge, +.entry-badge, +.repeat-badge { padding: 3px 9px; font-size: 11px; font-weight: 600; @@ -662,6 +736,11 @@ watch(userId, loadGraph, { immediate: true }) background: #dbeafe; } +.entry-badge { + color: #047857; + background: #d1fae5; +} + .repeat-badge { color: #475569; background: #e2e8f0; @@ -748,9 +827,15 @@ watch(userId, loadGraph, { immediate: true }) border-radius: 10px; } -.node-add { +.node-actions { position: absolute; right: 8px; + display: flex; + flex-direction: column; + gap: 7px; +} + +.node-action { display: flex; align-items: center; justify-content: center; @@ -764,6 +849,12 @@ watch(userId, loadGraph, { immediate: true }) cursor: pointer; } +.node-edit { + color: #7c3aed; + background: #f5f3ff; + border-color: #ddd6fe; +} + .is-root .node-add { color: #fff; background: rgba(255, 255, 255, 0.16); @@ -888,10 +979,17 @@ watch(userId, loadGraph, { immediate: true }) margin-top: 7px; } -.detail-add-button { - width: 100%; - height: 34px; +.detail-actions { + display: flex; + gap: 8px; margin-top: 12px; +} + +.detail-action-button { + flex: 1; + height: 34px; + margin-left: 0; + padding: 0 10px; border-radius: 999px; }