From dcf3d2fd6ad8bc9965ec768e97ac4d27e7a2d488 Mon Sep 17 00:00:00 2001
From: dzc <2388224167@qq.com>
Date: Mon, 20 Jul 2026 17:51:14 +0800
Subject: [PATCH] =?UTF-8?q?feat:=E5=85=B3=E7=B3=BB=E5=9B=BE=E8=B0=B1?=
=?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/admin/sysUserRelation.ts | 13 +
.../admin/system/relation-type/index.vue | 29 ++
.../system/user/relation-create-user.vue | 3 +-
.../admin/system/user/relation-edit-form.vue | 350 ++++++++++++++++++
src/views/admin/system/user/relation-tree.vue | 144 +++++--
5 files changed, 515 insertions(+), 24 deletions(-)
create mode 100644 src/views/admin/system/user/relation-edit-form.vue
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 @@
+
+
+
+
+
+
+
+
+
+
+ {{ $t("common.queryBtn") }}
+
+
+ {{ $t("common.resetBtn") }}
+
+
+
+
{
multiple.value = !rows.length
}
+// 清空搜索条件
+const resetQuery = () => {
+ // 清空搜索条件
+ queryRef.value?.resetFields()
+ // 清空多选
+ selectObjs.value = []
+ getDataList()
+}
+
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm("此操作将永久删除")
diff --git a/src/views/admin/system/user/relation-create-user.vue b/src/views/admin/system/user/relation-create-user.vue
index f81700e1..70d47d20 100644
--- a/src/views/admin/system/user/relation-create-user.vue
+++ b/src/views/admin/system/user/relation-create-user.vue
@@ -58,7 +58,8 @@
-
+
+
+
+
+
{{ getInitial(subjectUserName) }}
+
+
{{ subjectUserName }}
+
关系主语
+
+
+
+
+
+
+
{{ getInitial(objectUserName) }}
+
+
{{ objectUserName }}
+
关系对象
+
+
+
+
+
+ 绑定用户
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关系信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ relationDescription }}
+
+
+
+
+ 取消
+ 保存修改
+
+
+
+
+
+
+
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 @@
-
-
-
+
+
+
+
+
+
+
+
@@ -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;
}