小改族谱树

main
helloworld180 2 months ago
parent 4723722530
commit 9091b90582

106
package-lock.json generated

@ -13,8 +13,9 @@
"element-plus": "^2.8.6",
"vite": "^5.4.10",
"vue": "^3.2.13",
"vue-family-tree": "^0.1.8",
"vue-router": "^4.4.5",
"vue-virtual-scroller": "^1.1.2"
"vue-virtual-scroller": "^2.0.0-beta.8"
},
"devDependencies": {
"@babel/core": "^7.12.16",
@ -2321,7 +2322,7 @@
"version": "0.3.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
@ -2336,7 +2337,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@ -2346,7 +2347,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@ -2356,7 +2357,7 @@
"version": "0.3.6",
"resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@ -2373,7 +2374,7 @@
"version": "0.3.25",
"resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@ -2958,7 +2959,7 @@
"version": "22.8.1",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.8.1.tgz",
"integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.8"
@ -4064,7 +4065,7 @@
"version": "8.14.0",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@ -4722,7 +4723,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/bytes": {
@ -9075,6 +9076,11 @@
"dev": true,
"license": "ISC"
},
"node_modules/mitt": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-2.1.0.tgz",
"integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg=="
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz",
@ -10407,7 +10413,6 @@
"version": "2.8.8",
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"license": "MIT",
"optional": true,
"bin": {
@ -11073,12 +11078,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/scrollparent": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz",
"integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==",
"license": "ISC"
},
"node_modules/scule": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
@ -11437,7 +11436,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@ -11456,7 +11454,7 @@
"version": "0.5.21",
"resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
@ -11813,7 +11811,7 @@
"version": "5.36.0",
"resolved": "https://registry.npmmirror.com/terser/-/terser-5.36.0.tgz",
"integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==",
"dev": true,
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@ -11886,7 +11884,7 @@
"version": "2.20.3",
"resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/text-table": {
@ -12079,7 +12077,7 @@
"version": "6.19.8",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@ -12624,6 +12622,38 @@
"node": ">=10"
}
},
"node_modules/vue-family-tree": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/vue-family-tree/-/vue-family-tree-0.1.8.tgz",
"integrity": "sha512-Ge3jsnA9auPTYPTDJkxGyiOR0A9hfgGr+DkfCi7DOuIkmBhdjuz+fuREEiRlLwmAo4fY3h9EKACQXfoyos8mrA==",
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11"
}
},
"node_modules/vue-family-tree/node_modules/@vue/compiler-sfc": {
"version": "2.7.16",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz",
"integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==",
"dependencies": {
"@babel/parser": "^7.23.5",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
},
"optionalDependencies": {
"prettier": "^1.18.2 || ^2.0.0"
}
},
"node_modules/vue-family-tree/node_modules/vue": {
"version": "2.7.16",
"resolved": "https://registry.npmmirror.com/vue/-/vue-2.7.16.tgz",
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
"deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
"dependencies": {
"@vue/compiler-sfc": "2.7.16",
"csstype": "^3.1.0"
}
},
"node_modules/vue-hot-reload-api": {
"version": "2.3.4",
"resolved": "https://registry.npmmirror.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
@ -12672,18 +12702,19 @@
}
},
"node_modules/vue-observe-visibility": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz",
"integrity": "sha512-xo0CEVdkjSjhJoDdLSvoZoQrw/H2BlzB5jrCBKGZNXN2zdZgMuZ9BKrxXDjNP2AxlcCoKc8OahI3F3r3JGLv2Q==",
"license": "MIT"
"version": "2.0.0-alpha.1",
"resolved": "https://registry.npmmirror.com/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz",
"integrity": "sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==",
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-resize": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-0.4.5.tgz",
"integrity": "sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==",
"license": "MIT",
"version": "2.0.0-alpha.1",
"resolved": "https://registry.npmmirror.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz",
"integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==",
"peerDependencies": {
"vue": "^2.3.0"
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
@ -12727,17 +12758,16 @@
"license": "MIT"
},
"node_modules/vue-virtual-scroller": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-1.1.2.tgz",
"integrity": "sha512-SkUyc7QHCJFB5h1Fya7LxVizlVzOZZuFVipBGHYoTK8dwLs08bIz/tclvRApYhksaJIm/nn51inzO2UjpGJPMQ==",
"license": "MIT",
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmmirror.com/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.8.tgz",
"integrity": "sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==",
"dependencies": {
"scrollparent": "^2.0.1",
"vue-observe-visibility": "^0.4.4",
"vue-resize": "^0.4.5"
"mitt": "^2.1.0",
"vue-observe-visibility": "^2.0.0-alpha.1",
"vue-resize": "^2.0.0-alpha.1"
},
"peerDependencies": {
"vue": "^2.6.11"
"vue": "^3.2.0"
}
},
"node_modules/watchpack": {

@ -13,8 +13,9 @@
"element-plus": "^2.8.6",
"vite": "^5.4.10",
"vue": "^3.2.13",
"vue-family-tree": "^0.1.8",
"vue-router": "^4.4.5",
"vue-virtual-scroller": "^1.1.2"
"vue-virtual-scroller": "^2.0.0-beta.8"
},
"devDependencies": {
"@babel/core": "^7.12.16",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@ -1,17 +1,13 @@
<!DOCTYPE html>
<html lang="">
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<meta charset="UTF-8">
<link rel="icon" href="./favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>恋爱森林</title>
</head>
<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>
<!-- built files will be auto injected -->
<script type="module" src="/src/main.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 14 KiB

@ -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/familyTree.vue'
import familyTree from '@/views/space/familyTree1.vue'
import {createRouter ,createWebHashHistory} from 'vue-router'

@ -15,7 +15,6 @@
<style scoped>
.recommend-container {
width: 100%;
height: 100%;
min-height: calc(100vh - 60px);
background-image: url('../../assets/pictures/recommendbg.png');
background-size: cover;

@ -1,150 +1,299 @@
<template>
<div class="background">
<div class="tree">
<TreeChart :json="treeData">
<template v-slot:node="{ node }">
<div class="node-info">
<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 v-if="node.expanded" class="children">
<!-- 子节点展示 -->
<TreeChart :json="node.children" v-if="node.children && node.children.length"/>
</div>
<div class="family-tree-container" ref="treeContainer">
<!-- 顶部操作栏 -->
<div class="operation-bar">
<el-button type="primary" @click="showAddDialog('root')"></el-button>
<el-button type="success" @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>
<!-- 节点操作弹窗 -->
<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.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>
</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>
</template>
<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 {
name: 'FamilyTree',
components: {
TreeChart
VueFamilyTree
},
data() {
return {
treeContainer: null,
treeData: {
name: '爸爸',
image_url: require('@/assets/pictures/space/Inbase.png'),
expanded: true, //
id: '1',
name: '张三',
relation: 'grandfather',
avatar: 'http://example.com/avatar1.png',
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"
}
{
id: '2',
name: '父亲',
relation: 'father',
avatar: 'url2',
children: [
{
id: '3',
name: '我',
relation: 'self',
avatar: 'url3',
isUser: true,
children: []
}
]
}
]
},
dialogVisible: false,
dialogTitle: '',
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: {
toggleNode(node) {
node.expanded = !node.expanded; // /
async initTreeData() {
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>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
.family-tree-container {
padding: 20px;
position: relative;
}
.background {
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;
height: 100vh;
width: 100%;
.operation-bar {
margin-bottom: 20px;
}
.tree {
display: flex;
position: absolute;
background: rgba(255, 255, 255, 0.8);
width: 1100px;
height: 700px;
border-radius: 30px;
margin-top: 30px;
margin-left: 240px;
padding-top: 100px;
justify-content: center;
align-items: center;
flex-shrink: 0;
.tree-wrapper {
min-height: 600px;
background: #f5f7fa;
padding: 20px;
border-radius: 8px;
}
.node-info {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.avatar-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.node-image {
width: 40px;
height: 40px;
border-radius: 50%;
margin-bottom: 10px;
.avatar {
width: 100px;
height: 100px;
display: block;
}
.info {
font-size: 14px;
.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;
}
.children {
margin-top: 20px;
padding-left: 20px;
display: flex;
flex-direction: column;
.context-menu .el-button {
display: block;
width: 100%;
text-align: left;
padding: 8px 20px;
}
</style>
</style>

@ -0,0 +1,337 @@
<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="name">{{ node.name }}</div>
<button @click="toggleNode(node)">
{{ node.expanded ? '隐藏下一代' : '显示下一代' }}
</button>
</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';
export default {
components: {
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;
},
//
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 {
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%;
}
.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;
}
.operation-bar {
margin-bottom: 20px;
text-align: center;
}
.node-info {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
cursor: pointer;
padding: 10px;
border-radius: 8px;
transition: all 0.3s;
}
.node-info:hover {
background: rgba(0, 0, 0, 0.1);
}
.is-current-user {
background: rgba(64, 158, 255, 0.1);
border: 2px solid #409EFF;
}
.node-image {
width: 40px;
height: 40px;
border-radius: 50%;
margin-bottom: 10px;
}
.info {
font-size: 14px;
}
.children {
margin-top: 20px;
padding-left: 20px;
display: flex;
flex-direction: column;
}
.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;
}
.avatar {
width: 100px;
height: 100px;
display: block;
}
</style>

@ -5,28 +5,28 @@
<!-- 头像 -->
<div class="avatar-section">
<div class="avatar-container">
<img :src="user.avatar || defaultAvatar" alt="Avatar" class="avatar">
<img :src="userInfo.avatar || defaultAvatar" alt="Avatar" class="avatar">
<!-- 更多信息按钮 -->
<button @click="showMoreInfo" class="showMoreInfo-btn">
<img src="../../assets/pictures/space/more.png" alt="">
<img src="../../assets/pictures/space/more.png" alt="" style="width: 80px;">
</button>
<!-- 族谱树按钮 -->
<button @click="showFamilyTree" class="showFamilytree-btn">
<img src="../../assets/pictures/space/tree.png" alt="">
<img src="../../assets/pictures/space/tree.png" alt="" style="width: 80px;">
</button>
</div>
</div>
<!-- 基本信息 -->
<div class="user-info">
<h2>{{ userInfo.name || "加载中..." }}</h2>
<h2 style="margin-bottom: 30px;">{{ userInfo.name || "加载中..." }}</h2>
<div class="info-container">
<p>
<img src="../../assets/pictures/space/map.png" style="width: 19px;">
<p style="margin-right: 20px;">
<img src="../../assets/pictures/space/map.png" style="width: 19px; margin-right: 5px;">
{{ userInfo.location || "加载中..." }}
</p>
<p>
<img src="../../assets/pictures/space/calendar.png" style="width: 17px;">
<img src="../../assets/pictures/space/calendar.png" style="width: 17px; margin-right: 5px">
{{ userInfo.joinedDate || "加载中..." }}
</p>
</div>
@ -180,7 +180,7 @@
</template>
<script>
import axios from 'axios';
import axios from '@/utils/axiosConfig'
export default {
name:'spaceIndex',
data() {
@ -188,11 +188,9 @@
likeCount: 0,
isLiked:false,
commentCount:0,
user: {
avatar: "", // URL
},
userInfo: {
name: "", //
avatar:"",
location: "", //
joinedDate: "", //
interests: "", //
@ -236,7 +234,7 @@
// }
//
this.user.avatar = response.data.avatar;
this.userInfo.avatar = response.data.avatar;
this.userInfo.name = response.data.name;
this.userInfo.location = response.data.location;
this.userInfo.joinedDate = response.data.joinedDate;
@ -245,7 +243,7 @@
this.userInfo.career = response.data.career;
} catch (error) {
console.error("获取用户信息失败:", error);
alert("无法加载用户信息,请稍后重试!");
// alert("");
}
},
selectTab(tab) {
@ -277,18 +275,20 @@
display: flex;
justify-content: space-between;
background-image: url("../../assets/pictures/space/Inbase2.png");
background-size: 100%;
height: 100vh;
padding: 20px;
min-height: calc(100vh - 100px);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.profile-card {
background-color: #FFFFFF;
padding: 20px;
padding: 25px;
border-radius: 30px;
margin-top: 70px;
margin-left: 40px;
width: 410px;
height: 540px;
margin-left: 80px;
width: 430px;
height: 580px;
border-top: 10px solid #9EAFFD;
position: relative; /* 为伪元素定位 */
}

Loading…
Cancel
Save