parent
4723722530
commit
9091b90582
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 12 KiB |
@ -1,17 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="./favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>恋爱森林</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 14 KiB |
@ -1,150 +1,299 @@
|
||||
<template>
|
||||
<div class="background">
|
||||
<div class="tree">
|
||||
<TreeChart :json="treeData">
|
||||
<template v-slot:node="{ node }">
|
||||
<div class="node-info">
|
||||
<img :src="node.image_url" alt="image" class="node-image" />
|
||||
<div class="info">
|
||||
<div class="name">{{ node.name }}</div>
|
||||
<button @click="toggleNode(node)">
|
||||
{{ node.expanded ? '隐藏下一代' : '显示下一代' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="node.expanded" class="children">
|
||||
<!-- 子节点展示 -->
|
||||
<TreeChart :json="node.children" v-if="node.children && node.children.length"/>
|
||||
</div>
|
||||
<div class="family-tree-container" ref="treeContainer">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="operation-bar">
|
||||
<el-button type="primary" @click="showAddDialog('root')">添加祖辈节点</el-button>
|
||||
<el-button type="success" @click="saveTreeData">保存族谱</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 族谱树展示 -->
|
||||
<div class="tree-wrapper">
|
||||
<vue-family-tree
|
||||
v-if="treeData && Object.keys(treeData).length"
|
||||
:data="treeData"
|
||||
:enableDrag="true"
|
||||
@node-click="handleNodeClick"
|
||||
@node-contextmenu="showContextMenu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 节点操作弹窗 -->
|
||||
<el-dialog
|
||||
:title="dialogTitle"
|
||||
v-model="dialogVisible"
|
||||
width="500px"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<el-form :model="currentNode" label-width="100px">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="currentNode.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="关系">
|
||||
<el-select v-model="currentNode.relation">
|
||||
<el-option label="爷爷/外公" value="grandfather"></el-option>
|
||||
<el-option label="奶奶/外婆" value="grandmother"></el-option>
|
||||
<el-option label="父亲" value="father"></el-option>
|
||||
<el-option label="母亲" value="mother"></el-option>
|
||||
<el-option label="子女" value="child"></el-option>
|
||||
<el-option label="孙辈" value="grandchild"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="头像">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:action="uploadUrl"
|
||||
:show-file-list="false"
|
||||
:on-success="handleAvatarSuccess"
|
||||
>
|
||||
<img v-if="currentNode.avatar" :src="currentNode.avatar" class="avatar">
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否本人">
|
||||
<el-switch v-model="currentNode.isUser"></el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveNode">确定</el-button>
|
||||
</template>
|
||||
</TreeChart>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<div v-if="contextMenuVisible" class="context-menu" :style="contextMenuStyle">
|
||||
<el-button text @click="handleEdit">编辑节点</el-button>
|
||||
<el-button text @click="handleAddChild">添加子节点</el-button>
|
||||
<el-button text type="danger" @click="handleDelete">删除节点</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TreeChart from "vue-tree-chart-3";
|
||||
import { ElMessage } from 'element-plus'
|
||||
import VueFamilyTree from 'vue-family-tree'
|
||||
import request from '@/utils/axiosConfig'
|
||||
|
||||
export default {
|
||||
name: 'FamilyTree',
|
||||
components: {
|
||||
TreeChart
|
||||
VueFamilyTree
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
treeContainer: null,
|
||||
treeData: {
|
||||
name: '爸爸',
|
||||
image_url: require('@/assets/pictures/space/Inbase.png'),
|
||||
expanded: true, // 默认展开
|
||||
id: '1',
|
||||
name: '张三',
|
||||
relation: 'grandfather',
|
||||
avatar: 'http://example.com/avatar1.png',
|
||||
children: [
|
||||
{
|
||||
name: 'children1',
|
||||
image_url: "https://static.refined-x.com/avat1.jpg",
|
||||
expanded: true,
|
||||
},
|
||||
{
|
||||
name: 'children2',
|
||||
image_url: "https://static.refined-x.com/avat2.jpg",
|
||||
expanded: true,
|
||||
mate: [{
|
||||
name: 'mate',
|
||||
image_url: "https://static.refined-x.com/avat3.jpg"
|
||||
}],
|
||||
children: [
|
||||
{
|
||||
name: 'grandchild',
|
||||
image_url: "https://static.refined-x.com/avat.jpg"
|
||||
},
|
||||
{
|
||||
name: 'grandchild2',
|
||||
image_url: "https://static.refined-x.com/avat1.jpg",
|
||||
expanded: true,
|
||||
children:[
|
||||
{
|
||||
name:'dd',
|
||||
image_url: "https://static.refined-x.com/avat1.jpg",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'grandchild3',
|
||||
image_url: "https://static.refined-x.com/avat2.jpg"
|
||||
},
|
||||
{
|
||||
name:'hh',
|
||||
image_url: "https://static.refined-x.com/avat2.jpg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'children3',
|
||||
image_url: "https://static.refined-x.com/avat.jpg"
|
||||
}
|
||||
{
|
||||
id: '2',
|
||||
name: '父亲',
|
||||
relation: 'father',
|
||||
avatar: 'url2',
|
||||
children: [
|
||||
{
|
||||
id: '3',
|
||||
name: '我',
|
||||
relation: 'self',
|
||||
avatar: 'url3',
|
||||
isUser: true,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
dialogVisible: false,
|
||||
dialogTitle: '',
|
||||
currentNode: {
|
||||
id: '',
|
||||
name: '',
|
||||
relation: '',
|
||||
avatar: '',
|
||||
isUser: false,
|
||||
children: []
|
||||
},
|
||||
contextMenuVisible: false,
|
||||
contextMenuStyle: {
|
||||
left: '0px',
|
||||
top: '0px'
|
||||
},
|
||||
selectedNode: null,
|
||||
// 静态数据
|
||||
uploadUrl: 'http://api.example.com/upload',
|
||||
defaultAvatar: 'http://example.com/default-avatar.png',
|
||||
relationTypes: [
|
||||
{ label: '爷爷/外公', value: 'grandfather' },
|
||||
{ label: '奶奶/外婆', value: 'grandmother' },
|
||||
{ label: '父亲', value: 'father' },
|
||||
{ label: '母亲', value: 'mother' },
|
||||
{ label: '子女', value: 'child' },
|
||||
{ label: '孙辈', value: 'grandchild' }
|
||||
],
|
||||
sampleTreeData: {
|
||||
id: '1',
|
||||
name: '张三',
|
||||
relation: 'grandfather',
|
||||
avatar: 'http://example.com/avatar1.png',
|
||||
children: []
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// this.initTreeData()
|
||||
// document.addEventListener('click', this.handleClickOutside)
|
||||
},
|
||||
// beforeDestroy() {
|
||||
// document.removeEventListener('click', this.handleClickOutside)
|
||||
// },
|
||||
methods: {
|
||||
toggleNode(node) {
|
||||
node.expanded = !node.expanded; // 切换展开/收起
|
||||
async initTreeData() {
|
||||
try {
|
||||
const res = await request.get('/api/family-tree')
|
||||
if (res.code === 0 && res.data) {
|
||||
this.treeData = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取族谱数据失败')
|
||||
}
|
||||
},
|
||||
|
||||
handleClickOutside(e) {
|
||||
if (this.contextMenuVisible && !e.target.closest('.context-menu')) {
|
||||
this.contextMenuVisible = false
|
||||
}
|
||||
},
|
||||
|
||||
async saveNode() {
|
||||
try {
|
||||
const res = await request.post('/api/family-tree/node', this.currentNode)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('保存成功')
|
||||
this.dialogVisible = false
|
||||
await this.initTreeData()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
},
|
||||
|
||||
async handleDelete() {
|
||||
if (!this.selectedNode) {
|
||||
ElMessage.error('请选择一个节点')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await request.delete(`/api/family-tree/node/${this.selectedNode.id}`)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
await this.initTreeData()
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
this.contextMenuVisible = false
|
||||
},
|
||||
|
||||
showAddDialog(type) {
|
||||
this.dialogTitle = '添加成员'
|
||||
Object.assign(this.currentNode, {
|
||||
id: '',
|
||||
name: '',
|
||||
relation: '',
|
||||
avatar: '',
|
||||
isUser: false,
|
||||
parentId: type === 'root' ? null : (this.selectedNode ? this.selectedNode.id : null)
|
||||
})
|
||||
this.dialogVisible = true
|
||||
},
|
||||
|
||||
handleEdit() {
|
||||
if (!this.selectedNode) return
|
||||
this.dialogTitle = '编辑成员'
|
||||
Object.assign(this.currentNode, this.selectedNode)
|
||||
this.dialogVisible = true
|
||||
this.contextMenuVisible = false
|
||||
},
|
||||
|
||||
handleAvatarSuccess(res) {
|
||||
if (res && res.data) {
|
||||
this.currentNode.avatar = res.data.url
|
||||
}
|
||||
},
|
||||
|
||||
showContextMenu(e, node) {
|
||||
e.preventDefault()
|
||||
this.selectedNode = node
|
||||
this.contextMenuVisible = true
|
||||
this.contextMenuStyle.left = e.clientX + 'px'
|
||||
this.contextMenuStyle.top = e.clientY + 'px'
|
||||
},
|
||||
|
||||
async saveTreeData() {
|
||||
if (!this.treeData || !Object.keys(this.treeData).length) {
|
||||
ElMessage.warning('暂无数据可保存')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await request.put('/api/family-tree', this.treeData)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
.family-tree-container {
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.background {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
background-image: url("../../assets/pictures/space/Inbase3.png");
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
.operation-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tree {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
width: 1100px;
|
||||
height: 700px;
|
||||
border-radius: 30px;
|
||||
margin-top: 30px;
|
||||
margin-left: 240px;
|
||||
padding-top: 100px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.tree-wrapper {
|
||||
min-height: 600px;
|
||||
background: #f5f7fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.node-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
.avatar-uploader {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 10px;
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 14px;
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 5px 0;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.children {
|
||||
margin-top: 20px;
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.context-menu .el-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
Loading…
Reference in new issue