parent
4723722530
commit
9091b90582
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 12 KiB |
@ -1,17 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<link rel="icon" href="./favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<title>恋爱森林</title>
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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>
|
<template>
|
||||||
<div class="background">
|
<div class="family-tree-container" ref="treeContainer">
|
||||||
<div class="tree">
|
<!-- 顶部操作栏 -->
|
||||||
<TreeChart :json="treeData">
|
<div class="operation-bar">
|
||||||
<template v-slot:node="{ node }">
|
<el-button type="primary" @click="showAddDialog('root')">添加祖辈节点</el-button>
|
||||||
<div class="node-info">
|
<el-button type="success" @click="saveTreeData">保存族谱</el-button>
|
||||||
<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 class="tree-wrapper">
|
||||||
|
<vue-family-tree
|
||||||
|
v-if="treeData && Object.keys(treeData).length"
|
||||||
|
:data="treeData"
|
||||||
|
:enableDrag="true"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
@node-contextmenu="showContextMenu"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="node.expanded" class="children">
|
|
||||||
<!-- 子节点展示 -->
|
<!-- 节点操作弹窗 -->
|
||||||
<TreeChart :json="node.children" v-if="node.children && node.children.length"/>
|
<el-dialog
|
||||||
</div>
|
: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>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 {
|
export default {
|
||||||
|
name: 'FamilyTree',
|
||||||
components: {
|
components: {
|
||||||
TreeChart
|
VueFamilyTree
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
treeContainer: null,
|
||||||
treeData: {
|
treeData: {
|
||||||
name: '爸爸',
|
id: '1',
|
||||||
image_url: require('@/assets/pictures/space/Inbase.png'),
|
name: '张三',
|
||||||
expanded: true, // 默认展开
|
relation: 'grandfather',
|
||||||
children: [
|
avatar: 'http://example.com/avatar1.png',
|
||||||
{
|
|
||||||
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: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'grandchild',
|
id: '2',
|
||||||
image_url: "https://static.refined-x.com/avat.jpg"
|
name: '父亲',
|
||||||
},
|
relation: 'father',
|
||||||
{
|
avatar: 'url2',
|
||||||
name: 'grandchild2',
|
|
||||||
image_url: "https://static.refined-x.com/avat1.jpg",
|
|
||||||
expanded: true,
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name:'dd',
|
id: '3',
|
||||||
image_url: "https://static.refined-x.com/avat1.jpg",
|
name: '我',
|
||||||
|
relation: 'self',
|
||||||
|
avatar: 'url3',
|
||||||
|
isUser: true,
|
||||||
|
children: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'grandchild3',
|
|
||||||
image_url: "https://static.refined-x.com/avat2.jpg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name:'hh',
|
|
||||||
image_url: "https://static.refined-x.com/avat2.jpg"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
dialogVisible: false,
|
||||||
name: 'children3',
|
dialogTitle: '',
|
||||||
image_url: "https://static.refined-x.com/avat.jpg"
|
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: {
|
methods: {
|
||||||
toggleNode(node) {
|
async initTreeData() {
|
||||||
node.expanded = !node.expanded; // 切换展开/收起
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
* {
|
.family-tree-container {
|
||||||
margin: 0;
|
padding: 20px;
|
||||||
padding: 0;
|
position: relative;
|
||||||
box-sizing: border-box;
|
}
|
||||||
}
|
|
||||||
.background {
|
.operation-bar {
|
||||||
display: flex;
|
margin-bottom: 20px;
|
||||||
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%;
|
|
||||||
}
|
}
|
||||||
.tree {
|
|
||||||
display: flex;
|
.tree-wrapper {
|
||||||
position: absolute;
|
min-height: 600px;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: #f5f7fa;
|
||||||
width: 1100px;
|
padding: 20px;
|
||||||
height: 700px;
|
border-radius: 8px;
|
||||||
border-radius: 30px;
|
|
||||||
margin-top: 30px;
|
|
||||||
margin-left: 240px;
|
|
||||||
padding-top: 100px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-info {
|
.avatar-uploader {
|
||||||
display: flex;
|
border: 1px dashed #d9d9d9;
|
||||||
flex-direction: column;
|
border-radius: 6px;
|
||||||
align-items: center;
|
cursor: pointer;
|
||||||
text-align: center;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-image {
|
.avatar {
|
||||||
width: 40px;
|
width: 100px;
|
||||||
height: 40px;
|
height: 100px;
|
||||||
border-radius: 50%;
|
display: block;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.context-menu {
|
||||||
font-size: 14px;
|
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 {
|
.context-menu .el-button {
|
||||||
margin-top: 20px;
|
display: block;
|
||||||
padding-left: 20px;
|
width: 100%;
|
||||||
display: flex;
|
text-align: left;
|
||||||
flex-direction: column;
|
padding: 8px 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
Loading…
Reference in new issue