feat:关系图谱优化
This commit is contained in:
1 parent
c69e87d1db
commit
dcf3d2fd6a
5 files changed
+515
-24
No files matched your search
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,24 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<el-row>
|
||||
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
|
||||
<el-form-item style="width: 250px" label="关系类型" prop="relationName">
|
||||
<el-input v-model="state.queryForm.relationName" placeholder="请输入关系类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px" label="关系分类" prop="category">
|
||||
<dict-select v-model="state.queryForm.category" dict-code="sys_user_relation_category" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="Search" type="primary" @click="getDataList">
|
||||
{{ $t("common.queryBtn") }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">
|
||||
{{ $t("common.resetBtn") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<div class="mb8" style="width: 100%">
|
||||
<el-button class="ml10" icon="folder-add" type="primary" v-auth="'dzc_sysUserRelationType_add'"
|
||||
@@ -80,6 +98,8 @@ const { sys_user_relation_category, sys_user_relation_type, yes_no_type } = useD
|
||||
"yes_no_type",
|
||||
)
|
||||
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const relationStatusOptions = [
|
||||
{ label: "正常", value: "0", elTagType: "success" },
|
||||
{ label: "停用", value: "1", elTagType: "danger" },
|
||||
@@ -101,6 +121,15 @@ const selectionChangeHandle = (rows: { relationTypeId: string }[]) => {
|
||||
multiple.value = !rows.length
|
||||
}
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields()
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
getDataList()
|
||||
}
|
||||
|
||||
const handleDelete = async (ids: string[]) => {
|
||||
try {
|
||||
await useMessageBox().confirm("此操作将永久删除")
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
<user-info-form ref="userInfoFormRef" :model="newUserForm" :loading="submitting" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="选择已有用户" name="existing">
|
||||
<el-form ref="existingUserFormRef" :model="existingUserForm" :rules="existingUserRules" label-width="100px">
|
||||
<el-form ref="existingUserFormRef" :model="existingUserForm" :rules="existingUserRules"
|
||||
label-width="100px">
|
||||
<el-form-item label="关联用户" prop="userId">
|
||||
<select-user v-model="existingUserForm.userId"
|
||||
:exclude-user-ids="targetUser ? [targetUser.userId] : []" placeholder="输入姓名或手机号选择用户"
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="修改用户关系" width="760px" :close-on-click-modal="false" destroy-on-close>
|
||||
<div class="relation-summary">
|
||||
<div class="summary-user">
|
||||
<div class="summary-avatar">{{ getInitial(subjectUserName) }}</div>
|
||||
<div>
|
||||
<div class="summary-name">{{ subjectUserName }}</div>
|
||||
<div class="summary-role">关系主语</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="summary-arrow">
|
||||
<Right />
|
||||
</el-icon>
|
||||
<div class="summary-user">
|
||||
<div class="summary-avatar is-object">{{ getInitial(objectUserName) }}</div>
|
||||
<div>
|
||||
<div class="summary-name">{{ objectUserName }}</div>
|
||||
<div class="summary-role">关系对象</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" v-loading="relationOptionsLoading" :model="form" :rules="rules" label-width="100px">
|
||||
<el-divider content-position="center">绑定用户</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="11" class="mb20">
|
||||
<el-form-item label="关系主语" prop="subjectUserId">
|
||||
<select-user
|
||||
v-model="form.subjectUserId"
|
||||
:exclude-user-ids="form.objectUserId ? [form.objectUserId] : []"
|
||||
placeholder="请选择关系主语用户"
|
||||
@change="selectedSubjectUser = $event"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="2" class="swap-column">
|
||||
<el-tooltip content="交换关系双方" placement="top">
|
||||
<el-button circle icon="Switch" @click="swapUsers" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="11" class="mb20">
|
||||
<el-form-item label="关系对象" prop="objectUserId">
|
||||
<select-user
|
||||
v-model="form.objectUserId"
|
||||
:exclude-user-ids="form.subjectUserId ? [form.subjectUserId] : []"
|
||||
placeholder="请选择关系对象用户"
|
||||
@change="selectedObjectUser = $event"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="center">关系信息</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="关系类型" prop="relationTypeId">
|
||||
<el-select v-model="form.relationTypeId" filterable placeholder="请选择关系类型" class="w100">
|
||||
<el-option
|
||||
v-for="item in relationTypeOptions"
|
||||
:key="item.relationTypeId"
|
||||
:label="formatRelationType(item)"
|
||||
:value="item.relationTypeId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="生效时间" prop="effectiveTime">
|
||||
<el-date-picker
|
||||
v-model="form.effectiveTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择生效时间"
|
||||
class="w100"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mb20">
|
||||
<el-form-item label="失效时间" prop="expireTime">
|
||||
<el-date-picker
|
||||
v-model="form.expireTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择失效时间"
|
||||
class="w100"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="mb20">
|
||||
<el-form-item label="关系备注" prop="remark">
|
||||
<el-input v-model.trim="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入关系备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<template #title>{{ relationDescription }}</template>
|
||||
</el-alert>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="RelationEditForm">
|
||||
import type { FormInstance, FormRules } from "element-plus"
|
||||
import { type RelationGraphNode, type RelationUpdateForm, updateRelation } from "/@/api/admin/sysUserRelation"
|
||||
import { fetchList as fetchRelationTypes } from "/@/api/admin/sysUserRelationType"
|
||||
import { useMessage } from "/@/hooks/message"
|
||||
|
||||
interface RelationTypeOption {
|
||||
relationTypeId: string
|
||||
relationName: string
|
||||
subjectRoleName?: string
|
||||
objectRoleName?: string
|
||||
directedFlag?: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface RelationUserNames {
|
||||
subjectUserName?: string
|
||||
objectUserName?: string
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "success"): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const relationOptionsLoading = ref(false)
|
||||
const relationOptionsLoaded = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const relationTypeOptions = ref<RelationTypeOption[]>([])
|
||||
const selectedSubjectUser = ref<any>()
|
||||
const selectedObjectUser = ref<any>()
|
||||
const initialUserNames = reactive<Required<RelationUserNames>>({
|
||||
subjectUserName: "关系主语",
|
||||
objectUserName: "关系对象",
|
||||
})
|
||||
|
||||
const createInitialForm = (): RelationUpdateForm => ({
|
||||
relationId: "",
|
||||
subjectUserId: "",
|
||||
objectUserId: "",
|
||||
relationTypeId: "",
|
||||
effectiveTime: "",
|
||||
expireTime: "",
|
||||
remark: "",
|
||||
})
|
||||
|
||||
const form = reactive<RelationUpdateForm>(createInitialForm())
|
||||
|
||||
const selectedRelationType = computed(() =>
|
||||
relationTypeOptions.value.find((item) => String(item.relationTypeId) === String(form.relationTypeId)),
|
||||
)
|
||||
|
||||
const subjectUserName = computed(() => getSelectedUserName(selectedSubjectUser.value, initialUserNames.subjectUserName, form.subjectUserId))
|
||||
|
||||
const objectUserName = computed(() => getSelectedUserName(selectedObjectUser.value, initialUserNames.objectUserName, form.objectUserId))
|
||||
|
||||
const relationDescription = computed(() => {
|
||||
const relationType = selectedRelationType.value
|
||||
const roles = formatDirectionRoles(relationType?.subjectRoleName, relationType?.objectRoleName)
|
||||
const connector = relationType?.directedFlag === "1" ? " → " : " ↔ "
|
||||
return `将把关系调整为“${subjectUserName.value}${connector}${objectUserName.value}”${roles}`
|
||||
})
|
||||
|
||||
const validateDifferentUsers = (_rule: any, _value: string, callback: (error?: Error) => void) => {
|
||||
if (form.subjectUserId && form.objectUserId && String(form.subjectUserId) === String(form.objectUserId)) {
|
||||
callback(new Error("关系双方不能是同一个用户"))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const validateExpireTime = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
if (value && form.effectiveTime && value < form.effectiveTime) {
|
||||
callback(new Error("失效时间不能早于生效时间"))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const rules: FormRules = {
|
||||
subjectUserId: [
|
||||
{ required: true, message: "请选择关系主语用户", trigger: "change" },
|
||||
{ validator: validateDifferentUsers, trigger: "change" },
|
||||
],
|
||||
objectUserId: [
|
||||
{ required: true, message: "请选择关系对象用户", trigger: "change" },
|
||||
{ validator: validateDifferentUsers, trigger: "change" },
|
||||
],
|
||||
relationTypeId: [{ required: true, message: "请选择关系类型", trigger: "change" }],
|
||||
expireTime: [{ validator: validateExpireTime, trigger: "change" }],
|
||||
}
|
||||
|
||||
const loadRelationTypes = async () => {
|
||||
if (relationOptionsLoaded.value) return
|
||||
relationOptionsLoading.value = true
|
||||
try {
|
||||
const { data }: any = await fetchRelationTypes({ current: 1, size: 1000 })
|
||||
relationTypeOptions.value = (data?.records || []).filter((item: RelationTypeOption) => item.status === "0")
|
||||
relationOptionsLoaded.value = true
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "加载关系类型失败")
|
||||
} finally {
|
||||
relationOptionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openDialog = async (relation: RelationGraphNode, names: RelationUserNames = {}) => {
|
||||
if (!relation.relationId || !relation.subjectUserId || !relation.objectUserId) {
|
||||
useMessage().warning("当前节点没有可修改的关系")
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(form, createInitialForm(), {
|
||||
relationId: String(relation.relationId),
|
||||
subjectUserId: String(relation.subjectUserId),
|
||||
objectUserId: String(relation.objectUserId),
|
||||
relationTypeId: String(relation.relationTypeId || ""),
|
||||
effectiveTime: relation.effectiveTime || "",
|
||||
expireTime: relation.expireTime || "",
|
||||
remark: relation.remark || "",
|
||||
})
|
||||
initialUserNames.subjectUserName = names.subjectUserName || String(relation.subjectUserId)
|
||||
initialUserNames.objectUserName = names.objectUserName || String(relation.objectUserId)
|
||||
selectedSubjectUser.value = undefined
|
||||
selectedObjectUser.value = undefined
|
||||
visible.value = true
|
||||
await nextTick()
|
||||
formRef.value?.clearValidate()
|
||||
await loadRelationTypes()
|
||||
}
|
||||
|
||||
const swapUsers = () => {
|
||||
const subjectUserId = form.subjectUserId
|
||||
const subjectUser = selectedSubjectUser.value
|
||||
const subjectName = initialUserNames.subjectUserName
|
||||
form.subjectUserId = form.objectUserId
|
||||
form.objectUserId = subjectUserId
|
||||
selectedSubjectUser.value = selectedObjectUser.value
|
||||
selectedObjectUser.value = subjectUser
|
||||
initialUserNames.subjectUserName = initialUserNames.objectUserName
|
||||
initialUserNames.objectUserName = subjectName
|
||||
nextTick(() => formRef.value?.validateField(["subjectUserId", "objectUserId"]).catch(() => undefined))
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await updateRelation({ ...form })
|
||||
useMessage().success("用户关系修改成功")
|
||||
visible.value = false
|
||||
emit("success")
|
||||
} catch (error: any) {
|
||||
useMessage().error(error?.msg || "修改用户关系失败")
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatRelationType = (item: RelationTypeOption) => {
|
||||
const roles = [item.subjectRoleName, item.objectRoleName].filter(Boolean).join(" / ")
|
||||
return roles ? `${item.relationName}(${roles})` : item.relationName
|
||||
}
|
||||
|
||||
const formatDirectionRoles = (subjectRoleName?: string, objectRoleName?: string) => {
|
||||
if (!subjectRoleName && !objectRoleName) return ""
|
||||
return `(主语:${subjectRoleName || "-"},对象:${objectRoleName || "-"})`
|
||||
}
|
||||
|
||||
const getSelectedUserName = (user: any, fallback: string, userId: string) => {
|
||||
if (!userId) return "未选择用户"
|
||||
return user?.name || user?.nickname || user?.username || fallback || userId
|
||||
}
|
||||
|
||||
const getInitial = (name?: string) => name?.trim().slice(0, 1) || "用"
|
||||
|
||||
defineExpose({ openDialog })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.relation-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.summary-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.summary-avatar {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.summary-avatar.is-object {
|
||||
background: #7c3aed;
|
||||
}
|
||||
|
||||
.summary-name {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.summary-role {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.summary-arrow {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.swap-column {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -5,8 +5,8 @@
|
||||
<div class="toolbar-left">
|
||||
<el-button class="back-pill" icon="ArrowLeft" @click="goBack">返回用户列表</el-button>
|
||||
<div class="title-block">
|
||||
<div class="page-title">{{ graphRoot?.displayName || "用户" }}的关系图谱</div>
|
||||
<div class="page-subtitle">节点布局固定,拖动画布或缩放可查看完整关系</div>
|
||||
<div class="page-title">{{ entryNode?.displayName || "用户" }}的关系图谱</div>
|
||||
<div class="page-subtitle">绿色标记当前进入用户,蓝色标记图谱根节点</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
@@ -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,
|
||||
}"
|
||||
>
|
||||
<Handle type="target" :position="Position.Left" class="relation-handle" />
|
||||
<div v-if="data.graph.root" class="root-badge">核心用户</div>
|
||||
<div v-else-if="data.graph.repeated" class="repeat-badge">重复引用</div>
|
||||
<div class="node-badges">
|
||||
<div v-if="data.graph.root" class="root-badge">图谱根节点</div>
|
||||
<div v-if="isEntryNode(data.graph)" class="entry-badge">当前用户</div>
|
||||
<div v-if="data.graph.repeated" class="repeat-badge">重复引用</div>
|
||||
</div>
|
||||
<div class="node-avatar" :class="`gender-${data.graph.gender ?? 0}`">
|
||||
<img v-if="data.avatarUrl" :src="data.avatarUrl" :alt="data.graph.displayName" />
|
||||
<span v-else>{{ getInitial(data.graph.displayName) }}</span>
|
||||
@@ -108,13 +112,22 @@
|
||||
{{ data.graph.userRoleName || data.graph.relationName || "关联用户" }}
|
||||
</div>
|
||||
</div>
|
||||
<el-tooltip content="以此人为关联对象新增用户" placement="top">
|
||||
<button class="node-add" type="button" @click.stop="openCreateDialog(data.graph)">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<div class="node-actions">
|
||||
<el-tooltip v-if="!data.graph.root" content="修改当前绑定关系" placement="top">
|
||||
<button class="node-action node-edit" type="button" @click.stop="openEditDialog(data.graph)">
|
||||
<el-icon>
|
||||
<EditPen />
|
||||
</el-icon>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="以此人为关联对象新增用户" placement="top">
|
||||
<button class="node-action node-add" type="button" @click.stop="openCreateDialog(data.graph)">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<Handle type="source" :position="Position.Right" class="relation-handle" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -151,7 +164,8 @@
|
||||
<div class="detail-user-name">{{ selectedNode.displayName }}</div>
|
||||
<div class="detail-user-id">用户ID:{{ selectedNode.userId }}</div>
|
||||
</div>
|
||||
<el-tag v-if="selectedNode.root" type="primary">根节点</el-tag>
|
||||
<el-tag v-if="isEntryNode(selectedNode)" type="success">当前用户</el-tag>
|
||||
<el-tag v-else-if="selectedNode.root" type="primary">图谱根节点</el-tag>
|
||||
<el-tag v-else-if="selectedNode.repeated" type="info">重复节点</el-tag>
|
||||
</div>
|
||||
|
||||
@@ -209,9 +223,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-button type="primary" icon="UserFilled" class="detail-add-button" @click="openCreateDialog()">
|
||||
以此人为对象新增关系用户
|
||||
</el-button>
|
||||
<div class="detail-actions">
|
||||
<el-button v-if="!selectedNode.root" icon="EditPen" class="detail-action-button" @click="openEditDialog()">
|
||||
修改关系
|
||||
</el-button>
|
||||
<el-button type="primary" icon="UserFilled" class="detail-action-button" @click="openCreateDialog()">
|
||||
新增关系用户
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else description="点击图谱节点查看详情" />
|
||||
</aside>
|
||||
@@ -219,6 +238,7 @@
|
||||
</div>
|
||||
|
||||
<relation-create-user ref="createUserRef" @success="handleCreateSuccess" />
|
||||
<relation-edit-form ref="editFormRef" @success="handleEditSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<RelationGraphNode>()
|
||||
const entryNode = ref<RelationGraphNode>()
|
||||
const selectedNode = ref<RelationGraphNode>()
|
||||
const flowNodes = ref<Node<RelationNodeData>[]>([])
|
||||
const flowEdges = ref<Edge[]>([])
|
||||
const graphDepth = ref(0)
|
||||
const flowStore = ref<VueFlowStore>()
|
||||
const createUserRef = ref<InstanceType<typeof RelationCreateUser>>()
|
||||
const editFormRef = ref<InstanceType<typeof RelationEditForm>>()
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user