|
|
|
@ -1,337 +1,178 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="background">
|
|
|
|
|
<div class="tree">
|
|
|
|
|
<!-- 操作按钮 -->
|
|
|
|
|
<div class="operation-bar">
|
|
|
|
|
<el-button type="primary" @click="showAddDialog()">添加祖辈节点</el-button>
|
|
|
|
|
<el-button type="success" @click="saveTreeData">保存族谱</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 族谱树 -->
|
|
|
|
|
<TreeChart :json="treeData">
|
|
|
|
|
<template v-slot:node="{ node }">
|
|
|
|
|
<div class="node-info" :class="{ 'is-current-user': node.isCurrentUser }">
|
|
|
|
|
<img :src="node.image_url" alt="image" class="node-image" @click="handleNodeClick(node)" />
|
|
|
|
|
<div class="info">
|
|
|
|
|
<div class="family-tree">
|
|
|
|
|
<!-- 工具栏 -->
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
<el-button @click="zoomIn">放大</el-button>
|
|
|
|
|
<el-button @click="zoomOut">缩小</el-button>
|
|
|
|
|
<el-button @click="resetZoom">重置</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 树图容器 -->
|
|
|
|
|
<div class="tree-wrapper" ref="treeWrapper">
|
|
|
|
|
<TreeChart
|
|
|
|
|
:json="treeData"
|
|
|
|
|
:collapse-enabled="true"
|
|
|
|
|
:horizontal-gap="60"
|
|
|
|
|
:vertical-gap="60"
|
|
|
|
|
node-text="name"
|
|
|
|
|
class="tree-chart"
|
|
|
|
|
>
|
|
|
|
|
<template #node="{ node }">
|
|
|
|
|
<div
|
|
|
|
|
class="custom-node"
|
|
|
|
|
:class="{ 'is-current': node.isCurrentUser }"
|
|
|
|
|
>
|
|
|
|
|
<!-- 头像区域 -->
|
|
|
|
|
<div class="avatar-wrapper">
|
|
|
|
|
<el-avatar
|
|
|
|
|
:size="50"
|
|
|
|
|
:src="node.avatar"
|
|
|
|
|
@error="() => true"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 信息区域 -->
|
|
|
|
|
<div class="info-wrapper">
|
|
|
|
|
<div class="name">{{ node.name }}</div>
|
|
|
|
|
<button @click="toggleNode(node)">
|
|
|
|
|
{{ node.expanded ? '隐藏下一代' : '显示下一代' }}
|
|
|
|
|
</button>
|
|
|
|
|
<div class="relation">{{ node.relation }}</div>
|
|
|
|
|
<div class="birth-date">{{ node.birthDate }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="node.expanded" class="children">
|
|
|
|
|
<TreeChart :json="node.children" v-if="node.children && node.children.length"/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</TreeChart>
|
|
|
|
|
|
|
|
|
|
<!-- 节点编辑弹窗 -->
|
|
|
|
|
<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.generation">
|
|
|
|
|
<el-option label="祖辈" value="grandparent"></el-option>
|
|
|
|
|
<el-option label="父辈" value="parent"></el-option>
|
|
|
|
|
<el-option label="同辈" value="sibling"></el-option>
|
|
|
|
|
<el-option label="子辈" value="child"></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.image_url" :src="currentNode.image_url" 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.isCurrentUser"></el-switch>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
|
|
|
<el-button type="primary" @click="saveNode">确定</el-button>
|
|
|
|
|
<el-button type="danger" v-if="currentNode.id" @click="deleteNode">删除</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import TreeChart from "vue-tree-chart-3";
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
|
|
import request from '@/utils/axiosConfig';
|
|
|
|
|
import TreeChart from 'vue-tree-chart-3'
|
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
components: {
|
|
|
|
|
TreeChart
|
|
|
|
|
components: { TreeChart },
|
|
|
|
|
|
|
|
|
|
setup() {
|
|
|
|
|
const treeWrapper = ref(null)
|
|
|
|
|
const scale = ref(1)
|
|
|
|
|
|
|
|
|
|
// 缩放功能
|
|
|
|
|
const zoomIn = () => {
|
|
|
|
|
scale.value = Math.min(scale.value + 0.1, 2)
|
|
|
|
|
updateScale()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const zoomOut = () => {
|
|
|
|
|
scale.value = Math.max(scale.value - 0.1, 0.5)
|
|
|
|
|
updateScale()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resetZoom = () => {
|
|
|
|
|
scale.value = 1
|
|
|
|
|
updateScale()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateScale = () => {
|
|
|
|
|
if (treeWrapper.value) {
|
|
|
|
|
treeWrapper.value.style.transform = `scale(${scale.value})`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
treeWrapper,
|
|
|
|
|
zoomIn,
|
|
|
|
|
zoomOut,
|
|
|
|
|
resetZoom
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
treeData: {
|
|
|
|
|
name: '爸爸',
|
|
|
|
|
image_url: require('@/assets/pictures/space/Inbase.png'),
|
|
|
|
|
expanded: true, // 默认展开
|
|
|
|
|
name: '爷爷',
|
|
|
|
|
avatar: '../../assets/pictures/space/post1.png',
|
|
|
|
|
relation: '祖父',
|
|
|
|
|
birthDate: '1940-01-01',
|
|
|
|
|
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"
|
|
|
|
|
}],
|
|
|
|
|
name: '父亲',
|
|
|
|
|
avatar: 'https://via.placeholder.com/150',
|
|
|
|
|
relation: '父亲',
|
|
|
|
|
birthDate: '1965-01-01',
|
|
|
|
|
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: '我',
|
|
|
|
|
avatar: '/avatars/me.jpg',
|
|
|
|
|
relation: '本人',
|
|
|
|
|
birthDate: '1990-01-01',
|
|
|
|
|
isCurrentUser: true,
|
|
|
|
|
children: []
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'children3',
|
|
|
|
|
image_url: "https://static.refined-x.com/avat.jpg"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
dialogVisible: false,
|
|
|
|
|
dialogTitle: '',
|
|
|
|
|
currentNode: {
|
|
|
|
|
id: '',
|
|
|
|
|
name: '',
|
|
|
|
|
generation: '',
|
|
|
|
|
image_url: '',
|
|
|
|
|
isCurrentUser: false,
|
|
|
|
|
expanded: true,
|
|
|
|
|
children: []
|
|
|
|
|
},
|
|
|
|
|
uploadUrl: '',
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
created() {
|
|
|
|
|
this.fetchTreeData();
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
// 获取族谱数据
|
|
|
|
|
async fetchTreeData() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await request.get('/api/family-tree');
|
|
|
|
|
if (res.code === 0 && res.data) {
|
|
|
|
|
this.treeData = res.data;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('获取族谱数据失败');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 切换节点展开/收起
|
|
|
|
|
toggleNode(node) {
|
|
|
|
|
node.expanded = !node.expanded;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 点击节点
|
|
|
|
|
handleNodeClick(node) {
|
|
|
|
|
this.dialogTitle = '编辑成员';
|
|
|
|
|
this.currentNode = { ...node };
|
|
|
|
|
this.dialogVisible = true;
|
|
|
|
|
console.log('Node clicked:', node);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 显示添加节点弹窗
|
|
|
|
|
showAddDialog() {
|
|
|
|
|
this.dialogTitle = '添加成员';
|
|
|
|
|
this.currentNode = {
|
|
|
|
|
id: '',
|
|
|
|
|
name: '',
|
|
|
|
|
generation: '',
|
|
|
|
|
image_url: '',
|
|
|
|
|
isCurrentUser: false,
|
|
|
|
|
expanded: true,
|
|
|
|
|
children: []
|
|
|
|
|
};
|
|
|
|
|
this.dialogVisible = true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 头像上传成功回调
|
|
|
|
|
handleAvatarSuccess(response) {
|
|
|
|
|
if (response && response.url) {
|
|
|
|
|
this.currentNode.image_url = response.url; // 更新图片 URL
|
|
|
|
|
console.log('上传头像的url:' + response)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 保存节点
|
|
|
|
|
async saveNode() {
|
|
|
|
|
try {
|
|
|
|
|
const url = this.currentNode.id ?
|
|
|
|
|
`/api/family-tree/node/${this.currentNode.id}` :
|
|
|
|
|
'/api/family-tree/node';
|
|
|
|
|
const method = this.currentNode.id ? 'put' : 'post';
|
|
|
|
|
|
|
|
|
|
const res = await request[method](url, this.currentNode);
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
ElMessage.success('保存成功');
|
|
|
|
|
this.dialogVisible = false;
|
|
|
|
|
await this.fetchTreeData();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('保存失败');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 删除节点
|
|
|
|
|
async deleteNode() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await request.delete(`/api/family-tree/node/${this.currentNode.id}`);
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
ElMessage.success('删除成功');
|
|
|
|
|
this.dialogVisible = false;
|
|
|
|
|
await this.fetchTreeData();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('删除失败');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 保存整个族谱
|
|
|
|
|
async saveTreeData() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await request.put('/api/family-tree', this.treeData);
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
ElMessage.success('保存成功');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('保存失败');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.background {
|
|
|
|
|
.family-tree {
|
|
|
|
|
height: 100%;
|
|
|
|
|
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;
|
|
|
|
|
min-height: calc(100vh - 60px);
|
|
|
|
|
width: 100%;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree {
|
|
|
|
|
display: flex;
|
|
|
|
|
/* position: absolute; */
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
background: rgba(255, 255, 255, 0.8);
|
|
|
|
|
width: 1100px;
|
|
|
|
|
/* height: 700px; */
|
|
|
|
|
border-radius: 30px;
|
|
|
|
|
margin-top: 30px;
|
|
|
|
|
margin-left: 240px;
|
|
|
|
|
/* padding: 20px; */
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
.toolbar {
|
|
|
|
|
padding: 10px;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.operation-bar {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
.tree-wrapper {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
transform-origin: center center;
|
|
|
|
|
transition: transform 0.3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
text-align: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
.custom-node {
|
|
|
|
|
padding: 10px;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
background: white;
|
|
|
|
|
min-width: 150px;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node-info:hover {
|
|
|
|
|
background: rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.is-current-user {
|
|
|
|
|
background: rgba(64, 158, 255, 0.1);
|
|
|
|
|
.is-current {
|
|
|
|
|
border: 2px solid #409EFF;
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node-image {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
.avatar-wrapper {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
.info-wrapper {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.children {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
padding-left: 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
.name {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar-uploader {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
border: 1px dashed #d9d9d9;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
.relation {
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar {
|
|
|
|
|
width: 100px;
|
|
|
|
|
height: 100px;
|
|
|
|
|
display: block;
|
|
|
|
|
.birth-date {
|
|
|
|
|
color: #999;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</style>
|