族谱树修改、个人信息接口修改

main
helloworld180 2 months ago
parent 9091b90582
commit ccb7bda023

@ -0,0 +1,132 @@
.background {
display: flex;
align-content: center;
justify-content: center;
background-image: url("../../pictures/space/Inbase3.png");
background-size: cover;
background-position: center top;
background-repeat: no-repeat;
min-height: calc(100vh - 60px);
width: 100%;
}
.tree {
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.8);
width: 1100px;
border-radius: 30px;
margin-top: 30px;
margin-left: 240px;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.operation-bar {
margin-bottom: 20px;
text-align: center;
}
.tree-container {
overflow: auto;
padding: 20px;
}
/* 节点样式 */
.custom-node {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
border: 1px solid #dcdfe6;
border-radius: 8px;
background: white;
min-width: 150px;
}
.current-user {
border: 2px solid #409EFF;
background: #ecf5ff;
}
.node-info {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
cursor: pointer;
padding: 10px;
border-radius: 8px;
transition: all 0.3s;
margin-top: 8px;
}
.node-info:hover {
background: rgba(0, 0, 0, 0.1);
}
/* 文字样式 */
.name {
font-weight: bold;
font-size: 16px;
margin-bottom: 4px;
}
.relation {
color: #666;
font-size: 12px;
}
.birth-date,
.phone {
color: #999;
font-size: 12px;
}
/* 头像上传样式 */
.avatar-uploader {
text-align: center;
}
.avatar-wrapper {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
overflow: hidden;
width: 120px;
height: 120px;
}
.avatar-wrapper:hover {
border-color: #409EFF;
}
.avatar {
width: 120px;
height: 120px;
display: block;
object-fit: cover;
}
.el-icon {
font-size: 28px;
color: #8c939d;
width: 100%;
height: 100%;
line-height: 100px;
text-align: center;
}
.file-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}

@ -5,7 +5,7 @@ import community from '@/views/community/trends.vue'
import chat from '@/views/chat/chat.vue'
import space from '@/views/space/space.vue'
import informationPage from '@/views/space/informationPage.vue'
import familyTree from '@/views/space/familyTree1.vue'
import familyTree from '@/views/space/familyTree.vue'
import {createRouter ,createWebHashHistory} from 'vue-router'

@ -0,0 +1,26 @@
// 将树形数据扁平化
export function flattenTree(tree) {
const result = []
function flatten(node) {
if (!node) return
result.push({
id: node.id,
name: node.name,
parentId: node.parentId,
avatar: node.avatar,
relation: node.relation,
birthDate: node.birthDate,
phone: node.phone,
isCurrentUser: node.isCurrentUser
})
if (Array.isArray(node.children)) {
node.children.forEach(child => flatten(child))
}
}
flatten(tree)
return result
}

@ -1,244 +1,399 @@
<template>
<div class="family-tree-container" ref="treeContainer">
<!-- 顶部操作栏 -->
<div class="background">
<div class="tree">
<!-- 操作栏 -->
<div class="operation-bar">
<el-button type="primary" @click="showAddDialog('root')"></el-button>
<el-button type="success" @click="saveTreeData"></el-button>
<el-button type="primary" @click="showAddDialog"></el-button>
<el-button type="success" @click="editMember"></el-button>
<el-button type="danger" @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 class="tree-container">
<TreeChart
:json="treeData"
:collapse-enabled="true"
node-text="name"
>
<template v-slot:node="{ node }">
<div class="custom-node" :class="{ 'current-user': node.isCurrentUser }">
<el-avatar :size="50" :src="node.avatar" />
<div class="node-info">
<span class="name">{{ node.name }}</span>
<span class="relation">{{ node.relation }}</span>
<span class="birth-date">{{ node.birthDate }}</span>
<span class="phone">{{ node.phone }}</span>
</div>
</div>
</template>
</TreeChart>
</div>
<!-- 节点操作弹窗 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
<!-- 添加/编辑成员弹窗 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="500px"
:destroy-on-close="true"
>
<el-form :model="currentNode" label-width="100px">
<el-form :model="currentMember" label-width="100px">
<el-form-item label="姓名">
<el-input v-model="currentNode.name"></el-input>
<el-input v-model="currentMember.name"></el-input>
</el-form-item>
<el-form-item label="头像">
<div class="avatar-uploader">
<div class="avatar-wrapper">
<img v-if="currentMember.avatar" :src="currentMember.avatar" class="avatar" >
<el-icon v-else><Plus /></el-icon>
<input
type="file"
ref="fileInput"
@change="handleAvatarChange"
accept="image/jpeg,image/png"
class="file-input"
>
</div>
</div>
</el-form-item>
<el-form-item label="所属分支">
<el-select v-model="currentMember.parentId" placeholder="选择所属分支">
<el-option
v-for="member in availableParents"
:key="member.id"
:label="member.name"
:value="member.id"
/>
<el-option label="新建分支" value="new" />
</el-select>
</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 v-model="currentMember.relation">
<el-option label="祖辈" value="grandparent" />
<el-option label="父辈" value="parent" />
<el-option label="同辈" value="sibling" />
<el-option label="子辈" value="child" />
</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 label="出生日期">
<el-date-picker
v-model="currentMember.birthDate"
type="date"
placeholder="选择日期"
/>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="currentMember.phone"></el-input>
</el-form-item>
<el-form-item label="是否本人">
<el-switch v-model="currentNode.isUser"></el-switch>
<el-switch v-model="currentMember.isCurrentUser" />
</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="primary" @click="saveMember"></el-button>
<el-button
v-if="currentMember.id"
type="danger"
@click="deleteMember"
>删除</el-button>
</template>
</el-dialog>
<!-- 编辑成员选择弹窗 -->
<el-dialog
title="选择要编辑的成员"
v-model="editDialogVisible"
width="400px"
>
<el-select v-model="selectedMemberId" placeholder="请选择成员" style="width: 100%">
<el-option
v-for="member in flattenedTreeData"
:key="member.id"
:label="member.name"
:value="member.id"
/>
</el-select>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleEditMember"></el-button>
</template>
</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 { Plus } from '@element-plus/icons-vue'
import request from '@/utils/axiosConfig'
import { flattenTree } from '@/utils/treeUtils'
export default {
name: 'FamilyTree',
components: {
VueFamilyTree
TreeChart,
Plus
},
data() {
return {
treeContainer: null,
treeData: {
treeData :{
id: '1',
name: '张三',
name: '爷爷',
avatar: '/avatar/1.jpg',
relation: 'grandfather',
avatar: 'http://example.com/avatar1.png',
birthDate: '1940-01-01',
phone: '13800138000',
children: [
{
id: '2',
name: '父亲',
relation: 'father',
avatar: 'url2',
children: [
{
id: '3',
name: '我',
relation: 'self',
avatar: 'url3',
isUser: true,
children: []
}
]
}
]
{
id: '2',
name: '父亲',
avatar: '/avatar/2.jpg',
relation: 'father',
birthDate: '1965-01-01',
phone: '13800138001',
children: [
{
id: '3',
name: '我',
avatar: '/avatar/3.jpg',
relation: 'self',
birthDate: '1990-01-01',
phone: '13800138002',
isCurrentUser: true,
children: []
}
]
},
{
id: '2',
name: '父亲',
avatar: '/avatar/2.jpg',
relation: 'father',
birthDate: '1965-01-01',
phone: '13800138001',
children: [
{
id: '3',
name: '我',
avatar: '/avatar/3.jpg',
relation: 'self',
birthDate: '1990-01-01',
phone: '13800138002',
isCurrentUser: true,
children: []
}
]
}
],
},
dialogVisible: false,
dialogTitle: '',
currentNode: {
editDialogVisible: false,
dialogTitle: '添加成员',
selectedMemberId: '',
currentMember: {
id: '',
name: '',
relation: '',
avatar: '',
isUser: false,
children: []
},
contextMenuVisible: false,
contextMenuStyle: {
left: '0px',
top: '0px'
parentId: '',
relation: '',
birthDate: '',
phone: '',
isCurrentUser: false
},
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' }
uploadUrl: '/api/upload',
flattenedTreeData: [
{
id: 1,
name: "祖父",
parentId: null,
avatar: "grandfather.jpg",
relation: "祖父",
birthDate: "1940-01-01",
phone: "1234567890",
isCurrentUser: false
},
{
id: 2,
name: "父亲",
parentId: 1,
avatar: "father.jpg",
relation: "父亲",
birthDate: "1970-05-10",
phone: "0987654321",
isCurrentUser: false
},
{
id: 3,
name: "我",
parentId: 2,
avatar: "me.jpg",
relation: "本人",
birthDate: "1995-08-20",
phone: "1122334455",
isCurrentUser: true
}
],
sampleTreeData: {
id: '1',
name: '张三',
relation: 'grandfather',
avatar: 'http://example.com/avatar1.png',
children: []
}
availableParents: [
{
id: 1,
name: "祖父",
parentId: null,
avatar: "grandfather.jpg",
relation: "祖父",
birthDate: "1940-01-01",
phone: "1234567890",
isCurrentUser: false
},
{
id: 2,
name: "父亲",
parentId: 1,
avatar: "father.jpg",
relation: "父亲",
birthDate: "1970-05-10",
phone: "0987654321",
isCurrentUser: false
},
{
id: 3,
name: "我",
parentId: 2,
avatar: "me.jpg",
relation: "本人",
birthDate: "1995-08-20",
phone: "1122334455",
isCurrentUser: true
}
]
}
},
mounted() {
// this.initTreeData()
// document.addEventListener('click', this.handleClickOutside)
created() {
this.fetchTreeData()
},
// beforeDestroy() {
// document.removeEventListener('click', this.handleClickOutside)
// },
methods: {
async initTreeData() {
//
async fetchTreeData() {
try {
const res = await request.get('/api/family-tree')
if (res.code === 0 && res.data) {
this.treeData = res.data
}
const { data } = await request.get('/api/family-tree')
this.treeData = data
this.flattenedTreeData = flattenTree(data)
this.availableParents = this.flattenedTreeData
} catch (error) {
ElMessage.error('获取族谱数据失败')
}
},
handleClickOutside(e) {
if (this.contextMenuVisible && !e.target.closest('.context-menu')) {
this.contextMenuVisible = false
//
showAddDialog() {
this.dialogTitle = '添加成员'
this.currentMember = {
id: '',
name: '',
avatar: '',
parentId: '',
relation: '',
birthDate: '',
phone: '',
isCurrentUser: false
}
this.dialogVisible = true
},
async saveNode() {
//
editMember() {
this.editDialogVisible = true
},
//
async handleEditMember() {
if (!this.selectedMemberId) {
ElMessage.warning('请选择要编辑的成员')
return
}
const member = this.flattenedTreeData.find(m => m.id === this.selectedMemberId)
if (member) {
this.currentMember = { ...member }
this.dialogTitle = '编辑成员'
this.editDialogVisible = false
this.dialogVisible = true
}
},
//
handleAvatarChange(event) {
const file = event.target.files[0]
if (!file) return
//
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJPG) {
ElMessage.error('上传头像图片只能是 JPG/PNG 格式!')
return
}
// 使 FileReader
const reader = new FileReader()
reader.onload = (e) => {
this.currentMember.avatar = e.target.result
}
reader.readAsDataURL(file)
//
// const formData = new FormData()
// formData.append('file', file)
// API...
},
//
async saveMember() {
try {
const res = await request.post('/api/family-tree/node', this.currentNode)
if (res.code === 0) {
const url = this.currentMember.id ?
`/api/family-tree/${this.currentMember.id}` :
'/api/family-tree'
const { data } = await request({
url,
method: this.currentMember.id ? 'put' : 'post',
data: this.currentMember
})
if (data.code === 0) {
ElMessage.success('保存成功')
this.dialogVisible = false
await this.initTreeData()
this.fetchTreeData()
}
} catch (error) {
ElMessage.error('保存失败')
}
},
async handleDelete() {
if (!this.selectedNode) {
ElMessage.error('请选择一个节点')
return
}
//
async deleteMember() {
try {
const res = await request.delete(`/api/family-tree/node/${this.selectedNode.id}`)
if (res.code === 0) {
const { data } = await request.delete(`/api/family-tree/${this.currentMember.id}`)
if (data.code === 0) {
ElMessage.success('删除成功')
await this.initTreeData()
this.dialogVisible = false
this.fetchTreeData()
}
} 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) {
const { data } = await request.put('/api/family-tree', this.treeData)
if (data.code === 0) {
ElMessage.success('保存成功')
}
} catch (error) {
@ -250,50 +405,5 @@ export default {
</script>
<style scoped>
.family-tree-container {
padding: 20px;
position: relative;
}
.operation-bar {
margin-bottom: 20px;
}
.tree-wrapper {
min-height: 600px;
background: #f5f7fa;
padding: 20px;
border-radius: 8px;
}
.avatar-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar {
width: 100px;
height: 100px;
display: block;
}
.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;
}
.context-menu .el-button {
display: block;
width: 100%;
text-align: left;
padding: 8px 20px;
}
@import url('../../assets/css/space/familyTree.css');
</style>

@ -46,7 +46,7 @@
</template>
<script>
import axios from 'axios';
import axios from '@/utils/axiosConfig';
export default {
data() {
@ -73,6 +73,31 @@ export default {
this.user.avatar = URL.createObjectURL(file);
}
},
//
async fetchUserInfo() {
try {
//
const response = await axios.get("/api/getUserInfo", {
params: {
userId: "12345", // ID
},
});
//
this.user.avatar = response.data.avatar;
this.user.account = response.data.account;
this.user.email = response.data.email;
this.user.birthday = response.data.avatar;
this.user.job = response.data.job;
this.user.name = response.data.name;
this.user.password = response.data.password;
this.user.location = response.data.location;
this.user.signature = response.data.signature;
} catch (error) {
console.error("获取用户信息失败:", error);
// alert("");
}
},
//
async saveProfile() {
try {
@ -109,11 +134,12 @@ export default {
.base{
display: flex;
background-image: url("../../assets/pictures/space/Inbase.png");
background-size: 100%;
justify-content: center;
align-content: center;
height: 100vh;
align-items: center;
padding:12px 200px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.profile {
display: flex;

Loading…
Cancel
Save