族谱树对接完成

main
helloworld180 7 months ago
parent 0f196ca887
commit 56071bdbf4

@ -5,7 +5,7 @@ import {getToken} from '@/token/auth' // 注意这里使用了解构赋值来导
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
// 去vue.config.js文件改baseurl // 去vue.config.js文件改baseurl
baseURL: '/api', // 注意!! 这里全局统一加上了 '/api' 前缀但是vue.config.js文件在发送请求时去掉了/api所以页面的请求接口若有/api还是要携带/api baseURL: 'http://10.133.25.59:8083/loveforest/api', // 注意!! 这里全局统一加上了 '/api' 前缀但是vue.config.js文件在发送请求时去掉了/api所以页面的请求接口若有/api还是要携带/api
timeout: 5000, // 请求超时时间 timeout: 5000, // 请求超时时间
}); });

@ -5,7 +5,7 @@
<div class="operation-bar"> <div class="operation-bar">
<el-button type="primary" @click="showAddDialog"></el-button> <el-button type="primary" @click="showAddDialog"></el-button>
<el-button type="success" @click="editMember"></el-button> <el-button type="success" @click="editMember"></el-button>
<el-button type="danger" @click="saveTreeData"></el-button> <!-- <el-button type="danger" @click="saveTreeData"></el-button> -->
</div> </div>
<!-- 族谱树图 --> <!-- 族谱树图 -->
@ -15,9 +15,9 @@
:collapse-enabled="true" :collapse-enabled="true"
node-text="name" node-text="name"
> >
<template v-slot:node="{ node }"> <template #node="{ node }">
<div class="custom-node" :class="{ 'current-user': node.isCurrentUser }"> <div class="custom-node" :class="{ 'current-user': node.isCurrentUser }">
<el-avatar :size="50" :src="node.avatar" /> <img :size="50" :src="node.avatar" />
<div class="node-info"> <div class="node-info">
<span class="name">{{ node.name }}</span> <span class="name">{{ node.name }}</span>
<span class="relation">{{ node.relation }}</span> <span class="relation">{{ node.relation }}</span>
@ -35,8 +35,8 @@
v-model="dialogVisible" v-model="dialogVisible"
width="500px" width="500px"
> >
<el-form :model="currentMember" label-width="100px"> <el-form :model="currentMember" ref="memberForm" label-width="100px" :rules="rules">
<el-form-item label="姓名"> <el-form-item label="姓名" prop="name">
<el-input v-model="currentMember.name"></el-input> <el-input v-model="currentMember.name"></el-input>
</el-form-item> </el-form-item>
@ -56,7 +56,7 @@
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="所属分支"> <el-form-item label="所属分支" prop="parentId">
<el-select v-model="currentMember.parentId" placeholder="选择所属分支"> <el-select v-model="currentMember.parentId" placeholder="选择所属分支">
<el-option <el-option
v-for="member in availableParents" v-for="member in availableParents"
@ -64,16 +64,16 @@
:label="member.name" :label="member.name"
:value="member.id" :value="member.id"
/> />
<el-option label="新建分支" value="new" /> <!-- <el-option label="新建分支" value="new" /> -->
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="关系"> <el-form-item label="关系" prop="relation">
<el-select v-model="currentMember.relation"> <el-select v-model="currentMember.relation">
<el-option label="祖辈" value="grandparent" /> <el-option label="子女" value="grandparent" />
<el-option label="父辈" value="parent" /> <!-- <el-option label="父辈" value="parent" />
<el-option label="同辈" value="sibling" /> <el-option label="同辈" value="sibling" />
<el-option label="子辈" value="child" /> <el-option label="子辈" value="child" /> -->
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -143,10 +143,22 @@ export default {
}, },
data() { data() {
return { return {
userId: 3,
rules: {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
parentId: [
{ required: true, message: '请选择所属分支', trigger: 'change' }
],
relation: [
{ required: true, message: '请选择关系', trigger: 'change' }
]
},
treeData :{ treeData :{
id: '1', id: '1',
name: '爷爷', name: '爷爷',
avatar: '/avatar/1.jpg', avatar: 'https://via.placeholder.com/150',
relation: 'grandfather', relation: 'grandfather',
birthDate: '1940-01-01', birthDate: '1940-01-01',
phone: '13800138000', phone: '13800138000',
@ -154,7 +166,7 @@ export default {
{ {
id: '2', id: '2',
name: '父亲', name: '父亲',
avatar: '/avatar/2.jpg', avatar: '../../assets/pictures/space/post1.png',
relation: 'father', relation: 'father',
birthDate: '1965-01-01', birthDate: '1965-01-01',
phone: '13800138001', phone: '13800138001',
@ -207,7 +219,7 @@ export default {
phone: '', phone: '',
isCurrentUser: false isCurrentUser: false
}, },
uploadUrl: '/api/upload', uploadUrl: '/upload',
flattenedTreeData: [ flattenedTreeData: [
{ {
id: 1, id: 1,
@ -281,15 +293,28 @@ export default {
// //
async fetchTreeData() { async fetchTreeData() {
try { try {
const { data } = await request.get('/api/family-tree') const response = await request.get(`/family-tree/${this.userId}`)
this.treeData = data this.processTreeData(response)
this.flattenedTreeData = flattenTree(data) this.treeData = response
this.flattenedTreeData = flattenTree(response)
this.availableParents = this.flattenedTreeData this.availableParents = this.flattenedTreeData
// console.log('', response)
} catch (error) { } catch (error) {
ElMessage.error('获取族谱数据失败') ElMessage.error('获取族谱数据失败')
} }
}, },
processTreeData(node) {
//
node.avatar = node.avatar || '/default-avatar.png'
node.relation = node.relation || '未知'
node.birthDate = node.birthDate || '未知'
node.phone = node.phone || '未知'
//
if (node.children && node.children.length) {
node.children.forEach(child => this.processTreeData(child))
}
},
// //
showAddDialog() { showAddDialog() {
this.dialogTitle = '添加成员' this.dialogTitle = '添加成员'
@ -354,23 +379,31 @@ export default {
// //
async saveMember() { async saveMember() {
const isValid = await new Promise((resolve) => {
this.$refs.memberForm.validate((valid) => {
resolve(valid);
});
});
if (!isValid) {
ElMessage.error('请填写完整的表单信息!');
return;
}
try { try {
const url = this.currentMember.id ? const url = this.currentMember.id ?
`/api/family-tree/${this.currentMember.id}` : `/family-tree/${this.currentMember.id}` :
'/api/family-tree' '/family-tree'
await request({
const { data } = await request({
url, url,
method: this.currentMember.id ? 'put' : 'post', method: this.currentMember.id ? 'put' : 'post',
data: this.currentMember data: this.currentMember
}) })
// console.log(':',response) response
if (data.code === 0) {
ElMessage.success('保存成功') ElMessage.success('保存成功')
this.dialogVisible = false this.dialogVisible = false
this.fetchTreeData() this.fetchTreeData()
}
} catch (error) { } catch (error) {
console.log('新增成员失败', this.currentMember)
ElMessage.error('保存失败') ElMessage.error('保存失败')
} }
}, },
@ -378,12 +411,28 @@ export default {
// //
async deleteMember() { async deleteMember() {
try { try {
const { data } = await request.delete(`/api/family-tree/${this.currentMember.id}`) const confirmResult = await this.$confirm(
if (data.code === 0) { '此操作将永久删除该成员, 是否继续?',
ElMessage.success('删除成功') '提示',
this.dialogVisible = false {
this.fetchTreeData() confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
} }
).catch(err => err);
// confirmResult 'confirm'
// confirmResult 'cancel'
if (confirmResult !== 'confirm') {
return; //
}
//
await request.delete(`/family-tree/${this.currentMember.id}`);
ElMessage.success('删除成功');
this.dialogVisible = false;
this.fetchTreeData();
} catch (error) { } catch (error) {
ElMessage.error('删除失败') ElMessage.error('删除失败')
} }
@ -392,10 +441,8 @@ export default {
// //
async saveTreeData() { async saveTreeData() {
try { try {
const { data } = await request.put('/api/family-tree', this.treeData) await request.put('/family-tree', this.treeData)
if (data.code === 0) {
ElMessage.success('保存成功') ElMessage.success('保存成功')
}
} catch (error) { } catch (error) {
ElMessage.error('保存失败') ElMessage.error('保存失败')
} }

@ -1,337 +1,178 @@
<template> <template>
<div class="background"> <div class="family-tree">
<div class="tree"> <!-- 工具栏 -->
<!-- 操作按钮 --> <div class="toolbar">
<div class="operation-bar"> <el-button @click="zoomIn"></el-button>
<el-button type="primary" @click="showAddDialog()"></el-button> <el-button @click="zoomOut"></el-button>
<el-button type="success" @click="saveTreeData"></el-button> <el-button @click="resetZoom"></el-button>
</div> </div>
<!-- 族谱树 --> <!-- 树图容器 -->
<TreeChart :json="treeData"> <div class="tree-wrapper" ref="treeWrapper">
<template v-slot:node="{ node }"> <TreeChart
<div class="node-info" :class="{ 'is-current-user': node.isCurrentUser }"> :json="treeData"
<img :src="node.image_url" alt="image" class="node-image" @click="handleNodeClick(node)" /> :collapse-enabled="true"
<div class="info"> :horizontal-gap="60"
<div class="name">{{ node.name }}</div> :vertical-gap="60"
<button @click="toggleNode(node)"> node-text="name"
{{ node.expanded ? '隐藏下一代' : '显示下一代' }} class="tree-chart"
</button> >
<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>
<!-- 信息区域 -->
<div class="info-wrapper">
<div class="name">{{ node.name }}</div>
<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> </div>
</template> </template>
</TreeChart> </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>
</div> </div>
</template> </template>
<script> <script>
import TreeChart from "vue-tree-chart-3"; import TreeChart from 'vue-tree-chart-3'
import { ElMessage } from 'element-plus'; import { ref } from 'vue'
import request from '@/utils/axiosConfig';
export default { export default {
components: { components: { TreeChart },
TreeChart
},
data() {
return {
treeData: {
name: '爸爸',
image_url: require('@/assets/pictures/space/Inbase.png'),
expanded: true, //
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"
}
]
},
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;
},
// setup() {
handleNodeClick(node) { const treeWrapper = ref(null)
this.dialogTitle = '编辑成员'; const scale = ref(1)
this.currentNode = { ...node };
this.dialogVisible = true;
console.log('Node clicked:', node);
},
// //
showAddDialog() { const zoomIn = () => {
this.dialogTitle = '添加成员'; scale.value = Math.min(scale.value + 0.1, 2)
this.currentNode = { updateScale()
id: '', }
name: '',
generation: '',
image_url: '',
isCurrentUser: false,
expanded: true,
children: []
};
this.dialogVisible = true;
},
// const zoomOut = () => {
handleAvatarSuccess(response) { scale.value = Math.max(scale.value - 0.1, 0.5)
if (response && response.url) { updateScale()
this.currentNode.image_url = response.url; // URL
console.log('上传头像的url' + response)
} }
},
// const resetZoom = () => {
async saveNode() { scale.value = 1
try { updateScale()
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); const updateScale = () => {
if (res.code === 0) { if (treeWrapper.value) {
ElMessage.success('保存成功'); treeWrapper.value.style.transform = `scale(${scale.value})`
this.dialogVisible = false;
await this.fetchTreeData();
} }
} catch (error) {
ElMessage.error('保存失败');
} }
},
// return {
async deleteNode() { treeWrapper,
try { zoomIn,
const res = await request.delete(`/api/family-tree/node/${this.currentNode.id}`); zoomOut,
if (res.code === 0) { resetZoom
ElMessage.success('删除成功');
this.dialogVisible = false;
await this.fetchTreeData();
}
} catch (error) {
ElMessage.error('删除失败');
} }
}, },
// data() {
async saveTreeData() { return {
try { treeData: {
const res = await request.put('/api/family-tree', this.treeData); name: '爷爷',
if (res.code === 0) { avatar: '../../assets/pictures/space/post1.png',
ElMessage.success('保存成功'); relation: '祖父',
birthDate: '1940-01-01',
children: [
{
name: '父亲',
avatar: 'https://via.placeholder.com/150',
relation: '父亲',
birthDate: '1965-01-01',
children: [
{
name: '我',
avatar: '/avatars/me.jpg',
relation: '本人',
birthDate: '1990-01-01',
isCurrentUser: true,
children: []
} }
} catch (error) { ]
ElMessage.error('保存失败'); }
]
} }
} }
} }
}; }
</script> </script>
<style scoped> <style scoped>
.background { .family-tree {
height: 100%;
display: flex; display: flex;
align-content: center; flex-direction: column;
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%;
} }
.tree { .toolbar {
display: flex; padding: 10px;
/* position: absolute; */ border-bottom: 1px solid #eee;
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;
} }
.operation-bar { .tree-wrapper {
margin-bottom: 20px; flex: 1;
text-align: center; overflow: auto;
padding: 20px;
transform-origin: center center;
transition: transform 0.3s;
} }
.node-info { .custom-node {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
cursor: pointer;
padding: 10px; padding: 10px;
border: 1px solid #ddd;
border-radius: 8px; 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 { .is-current {
background: rgba(64, 158, 255, 0.1);
border: 2px solid #409EFF; border: 2px solid #409EFF;
background: #ecf5ff;
} }
.node-image { .avatar-wrapper {
width: 40px; margin-bottom: 8px;
height: 40px; text-align: center;
border-radius: 50%;
margin-bottom: 10px;
} }
.info { .info-wrapper {
font-size: 14px; text-align: center;
} }
.children { .name {
margin-top: 20px; font-weight: bold;
padding-left: 20px; font-size: 16px;
display: flex; margin-bottom: 4px;
flex-direction: column;
} }
.avatar-uploader { .relation {
display: flex; color: #666;
justify-content: center; font-size: 14px;
align-items: center; margin-bottom: 2px;
width: 80px;
height: 80px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
} }
.avatar { .birth-date {
width: 100px; color: #999;
height: 100px; font-size: 12px;
display: block;
} }
</style> </style>

@ -1,12 +1,12 @@
const { defineConfig } = require('@vue/cli-service') const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({ module.exports = defineConfig({
devServer:{ devServer:{
host: 'localhost', // host: 'localhost',
port: 8080, // port: 8080,
https: false, https: false,
proxy:{ proxy:{
'/api': { //设置拦截器 拦截器格式 斜杠+拦截器名字,名字可以自己定 '/api': { //设置拦截器 拦截器格式 斜杠+拦截器名字,名字可以自己定
target: 'https://911fb0525ms3.vicp.fun/loveforest/api/', //代理的目标地址 target: 'http://10.133.25.59:8083/loveforest', //代理的目标地址
changeOrigin: true, //是否设置同源,输入是的 changeOrigin: true, //是否设置同源,输入是的
pathRewrite: { //路径重写 pathRewrite: { //路径重写
'^/api': '' //选择忽略拦截器里面的内容 '^/api': '' //选择忽略拦截器里面的内容

Loading…
Cancel
Save