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

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 chat from '@/views/chat/chat.vue'
import space from '@/views/space/space.vue' import space from '@/views/space/space.vue'
import informationPage from '@/views/space/informationPage.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' 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> <template>
<div class="family-tree-container" ref="treeContainer"> <div class="background">
<!-- 顶部操作栏 --> <div class="tree">
<!-- 操作栏 -->
<div class="operation-bar"> <div class="operation-bar">
<el-button type="primary" @click="showAddDialog('root')"></el-button> <el-button type="primary" @click="showAddDialog"></el-button>
<el-button type="success" @click="saveTreeData"></el-button> <el-button type="success" @click="editMember"></el-button>
<el-button type="danger" @click="saveTreeData"></el-button>
</div> </div>
<!-- 族谱树展示 --> <!-- 族谱树图 -->
<div class="tree-wrapper"> <div class="tree-container">
<vue-family-tree <TreeChart
v-if="treeData && Object.keys(treeData).length" :json="treeData"
:data="treeData" :collapse-enabled="true"
:enableDrag="true" node-text="name"
@node-click="handleNodeClick" >
@node-contextmenu="showContextMenu" <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> </div>
<!-- 节点操作弹窗 --> <!-- 添加/编辑成员弹窗 -->
<el-dialog <el-dialog
:title="dialogTitle" :title="dialogTitle"
v-model="dialogVisible" v-model="dialogVisible"
width="500px" 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-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>
<el-form-item label="关系"> <el-form-item label="关系">
<el-select v-model="currentNode.relation"> <el-select v-model="currentMember.relation">
<el-option label="爷爷/外公" value="grandfather"></el-option> <el-option label="祖辈" value="grandparent" />
<el-option label="奶奶/外婆" value="grandmother"></el-option> <el-option label="父辈" value="parent" />
<el-option label="父亲" value="father"></el-option> <el-option label="同辈" value="sibling" />
<el-option label="母亲" value="mother"></el-option> <el-option label="子辈" value="child" />
<el-option label="子女" value="child"></el-option>
<el-option label="孙辈" value="grandchild"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="头像">
<el-upload <el-form-item label="出生日期">
class="avatar-uploader" <el-date-picker
:action="uploadUrl" v-model="currentMember.birthDate"
:show-file-list="false" type="date"
:on-success="handleAvatarSuccess" placeholder="选择日期"
> />
<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>
<el-form-item label="联系电话">
<el-input v-model="currentMember.phone"></el-input>
</el-form-item>
<el-form-item label="是否本人"> <el-form-item label="是否本人">
<el-switch v-model="currentNode.isUser"></el-switch> <el-switch v-model="currentMember.isCurrentUser" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <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> </template>
</el-dialog> </el-dialog>
<!-- 右键菜单 --> <!-- 编辑成员选择弹窗 -->
<div v-if="contextMenuVisible" class="context-menu" :style="contextMenuStyle"> <el-dialog
<el-button text @click="handleEdit"></el-button> title="选择要编辑的成员"
<el-button text @click="handleAddChild"></el-button> v-model="editDialogVisible"
<el-button text type="danger" @click="handleDelete"></el-button> 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> </div>
</div> </div>
</template> </template>
<script> <script>
import TreeChart from 'vue-tree-chart-3'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import VueFamilyTree from 'vue-family-tree' import { Plus } from '@element-plus/icons-vue'
import request from '@/utils/axiosConfig' import request from '@/utils/axiosConfig'
import { flattenTree } from '@/utils/treeUtils'
export default { export default {
name: 'FamilyTree', name: 'FamilyTree',
components: { components: {
VueFamilyTree TreeChart,
Plus
}, },
data() { data() {
return { return {
treeContainer: null, treeData :{
treeData: {
id: '1', id: '1',
name: '张三', name: '爷爷',
avatar: '/avatar/1.jpg',
relation: 'grandfather', relation: 'grandfather',
avatar: 'http://example.com/avatar1.png', birthDate: '1940-01-01',
phone: '13800138000',
children: [ children: [
{ {
id: '2', id: '2',
name: '父亲', name: '父亲',
avatar: '/avatar/2.jpg',
relation: 'father', relation: 'father',
avatar: 'url2', birthDate: '1965-01-01',
phone: '13800138001',
children: [ children: [
{ {
id: '3', id: '3',
name: '我', name: '我',
avatar: '/avatar/3.jpg',
relation: 'self', relation: 'self',
avatar: 'url3', birthDate: '1990-01-01',
isUser: true, phone: '13800138002',
isCurrentUser: true,
children: [] 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, dialogVisible: false,
dialogTitle: '', editDialogVisible: false,
currentNode: { dialogTitle: '添加成员',
selectedMemberId: '',
currentMember: {
id: '', id: '',
name: '', name: '',
relation: '',
avatar: '', avatar: '',
isUser: false, parentId: '',
children: [] relation: '',
birthDate: '',
phone: '',
isCurrentUser: false
}, },
contextMenuVisible: false, uploadUrl: '/api/upload',
contextMenuStyle: { flattenedTreeData: [
left: '0px', {
top: '0px' id: 1,
name: "祖父",
parentId: null,
avatar: "grandfather.jpg",
relation: "祖父",
birthDate: "1940-01-01",
phone: "1234567890",
isCurrentUser: false
}, },
selectedNode: null, {
// id: 2,
uploadUrl: 'http://api.example.com/upload', name: "父亲",
defaultAvatar: 'http://example.com/default-avatar.png', parentId: 1,
relationTypes: [ avatar: "father.jpg",
{ label: '爷爷/外公', value: 'grandfather' }, relation: "父亲",
{ label: '奶奶/外婆', value: 'grandmother' }, birthDate: "1970-05-10",
{ label: '父亲', value: 'father' }, phone: "0987654321",
{ label: '母亲', value: 'mother' }, isCurrentUser: false
{ label: '子女', value: 'child' }, },
{ label: '孙辈', value: 'grandchild' } {
id: 3,
name: "我",
parentId: 2,
avatar: "me.jpg",
relation: "本人",
birthDate: "1995-08-20",
phone: "1122334455",
isCurrentUser: true
}
], ],
sampleTreeData: { availableParents: [
id: '1', {
name: '张三', id: 1,
relation: 'grandfather', name: "祖父",
avatar: 'http://example.com/avatar1.png', parentId: null,
children: [] 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() { created() {
// this.initTreeData() this.fetchTreeData()
// document.addEventListener('click', this.handleClickOutside)
}, },
// beforeDestroy() {
// document.removeEventListener('click', this.handleClickOutside)
// },
methods: { methods: {
async initTreeData() { //
async fetchTreeData() {
try { try {
const res = await request.get('/api/family-tree') const { data } = await request.get('/api/family-tree')
if (res.code === 0 && res.data) { this.treeData = data
this.treeData = res.data this.flattenedTreeData = flattenTree(data)
} this.availableParents = this.flattenedTreeData
} catch (error) { } catch (error) {
ElMessage.error('获取族谱数据失败') ElMessage.error('获取族谱数据失败')
} }
}, },
handleClickOutside(e) { //
if (this.contextMenuVisible && !e.target.closest('.context-menu')) { showAddDialog() {
this.contextMenuVisible = false this.dialogTitle = '添加成员'
this.currentMember = {
id: '',
name: '',
avatar: '',
parentId: '',
relation: '',
birthDate: '',
phone: '',
isCurrentUser: false
}
this.dialogVisible = true
},
//
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
} }
}, },
async saveNode() { //
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 { try {
const res = await request.post('/api/family-tree/node', this.currentNode) const url = this.currentMember.id ?
if (res.code === 0) { `/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('保存成功') ElMessage.success('保存成功')
this.dialogVisible = false this.dialogVisible = false
await this.initTreeData() this.fetchTreeData()
} }
} catch (error) { } catch (error) {
ElMessage.error('保存失败') ElMessage.error('保存失败')
} }
}, },
async handleDelete() { //
if (!this.selectedNode) { async deleteMember() {
ElMessage.error('请选择一个节点')
return
}
try { try {
const res = await request.delete(`/api/family-tree/node/${this.selectedNode.id}`) const { data } = await request.delete(`/api/family-tree/${this.currentMember.id}`)
if (res.code === 0) { if (data.code === 0) {
ElMessage.success('删除成功') ElMessage.success('删除成功')
await this.initTreeData() this.dialogVisible = false
this.fetchTreeData()
} }
} catch (error) { } catch (error) {
ElMessage.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() { async saveTreeData() {
if (!this.treeData || !Object.keys(this.treeData).length) {
ElMessage.warning('暂无数据可保存')
return
}
try { try {
const res = await request.put('/api/family-tree', this.treeData) const { data } = await request.put('/api/family-tree', this.treeData)
if (res.code === 0) { if (data.code === 0) {
ElMessage.success('保存成功') ElMessage.success('保存成功')
} }
} catch (error) { } catch (error) {
@ -250,50 +405,5 @@ export default {
</script> </script>
<style scoped> <style scoped>
.family-tree-container { @import url('../../assets/css/space/familyTree.css');
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;
}
</style> </style>

@ -46,7 +46,7 @@
</template> </template>
<script> <script>
import axios from 'axios'; import axios from '@/utils/axiosConfig';
export default { export default {
data() { data() {
@ -73,6 +73,31 @@ export default {
this.user.avatar = URL.createObjectURL(file); 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() { async saveProfile() {
try { try {
@ -109,11 +134,12 @@ export default {
.base{ .base{
display: flex; display: flex;
background-image: url("../../assets/pictures/space/Inbase.png"); background-image: url("../../assets/pictures/space/Inbase.png");
background-size: 100%;
justify-content: center; justify-content: center;
align-content: center; align-items: center;
height: 100vh;
padding:12px 200px; padding:12px 200px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
} }
.profile { .profile {
display: flex; display: flex;

Loading…
Cancel
Save