入职前端工作到现在差不多有一年半的时间了,和朋友偶然聊天的时候被问到,能不能用所学的前端知识做一个家族关系的族谱,可以使家族关系更加简单明了。当时听完这个需求,觉得可能还是蛮简单的,后来动手做的时候,发现族谱的连线,是需要根据返回的数据动态生成的,这就是我这个小前端,有点头秃了🤡。
解决技术困难
当时阻碍我前进的就是如何实现族谱的连线以及根据数据渲染它们的对应关系,后来在逛博客的过程中,发现了antdesign的charts图表组件。利用这个组件,如果可以进行一些改造,可能就可以实现族谱的关系图。
开始动手
首先需要安装ant-design/charts
,具体安装过程请参考官方文档。
安装完成以后,就要根据数据渲染出想要的视觉效果,由于svg图相关的知识比较薄弱,所以实现的视觉可能有点丑陋,大家将就着看看,我写这篇文章的目的就是总结自己的技术探索历程,可能这篇文章下周就修改了。
先看视觉效果。
代码:
import React, { useEffect, useState } from "react";
import "./Treechart.css";
import { Modal } from "antd";
import { OrganizationGraph } from "@ant-design/charts";
import * as _ from "lodash"
// const data = {
// id: "joel",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// style: {
// fill: "#00CED1",
// width: "200",
// },
// img: "/api/img/%E8%80%81%E4%BA%BA.jpeg",
// },
// style: {
// width: 110,
// height: 40,
// // stroke: "#87ceeb",
// fill: " #FFC0CB",
// radius: "8",
// // textAlign: "center",
// },
// children: [
// {
// id: "c1",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// children: [
// {
// id: "c1-1",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// },
// {
// id: "c1-2",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// children: [
// {
// id: "c1-2-1",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// },
// {
// id: "c1-2-2",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://lwfcll.oss-cn-hangzhou.aliyuncs.com/img/1630238333160.png",
// },
// },
// ],
// },
// ],
// },
// {
// id: "c2",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// },
// {
// id: "c3",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// children: [
// {
// id: "c3-1",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// },
// {
// id: "c3-2",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://lwfcll.oss-cn-hangzhou.aliyuncs.com/img/%E4%B8%AD%E5%B9%B4%E5%A4%AB%E5%A6%872.jpeg",
// },
// children: [
// {
// id: "c3-2-1",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// },
// {
// id: "c3-2-2",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// },
// {
// id: "c3-2-3",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// },
// ],
// },
// {
// id: "c3-3",
// value: {
// text: "李伟峰",
// value: "陈兰兰",
// // 建议使用 bae64 数据
// icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
// },
// },
// ],
// },
// ],
// };
const colorArr = ["#00CED1", "#FFA07A", "#87CEFA", "#BA55D3", "#00FA9A"];
export default function TreeChart() {
/**
* 遍历树的方法
*/
// const traverseTree = (data, fn) => {
// if (typeof fn !== "function") {
// return;
// }
// if (fn(data) === false) {
// return false;
// }
// if (data && data.children) {
// for (let i = data.children.length - 1; i >= 0; i--) {
// if (!traverseTree(data.children[i], fn)) return false;
// }
// }
// return true;
// };
// traverseTree(data, (d) => {
// d.leftIcon = {
// style: {
// fill: "#e6fffb",
// stroke: "#e6fffb",
// },
// img:
// "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Q_FQT6nwEC8AAAAAAAAAAABkARQnAQ",
// };
// d.rightIcon = {
// style: {
// fill: "#e6fffb",
// stroke: "#e6fffb",
// },
// img:
// "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Q_FQT6nwEC8AAAAAAAAAAABkARQnAQ",
// };
// return true;
// });
const [data, setData] = useState({})
const randomNum = (minNum: number, maxNum: number) => {
switch (arguments.length) {
case 1:
return parseInt(String(Math.random() * minNum + 1), 10);
break;
case 2:
return parseInt(String(Math.random() * (maxNum - minNum + 1) + minNum), 10);
break;
default:
return 0;
break;
}
};
const fetchTableData = () => {
fetch("http://localhost:9091/genealogy/info/selectUserPage", {
method: "POST",
body: JSON.stringify({
size: 20,
current: 1,
}),
headers: {
"content-type": "application/json",
},
}).then((res) => {
res.json().then((data) => {
setData(arrayToTree(data.data)[0])
console.log(456, arrayToTree(data.data)[0])
});
});
};
const arrayToTree = (items: []) => {
const result: any = []; // 存放结果集
const itemMap: any = {};
let newItem: any = _.cloneDeep(items);
newItem = _.map(newItem, (item: any) => {
let compain = newItem.findIndex((i: any) => item?.companionId === i.id)
let value = ''
if (compain !== -1) {
value = newItem[compain]?.name;
newItem.splice(compain, 1)
}
return {
...item,
value
}
})
items = newItem.filter((item: any) => item?.id)
console.log(123, items)
items.sort((x: any, y: any) => {
return x.id - y.id
})
console.log(items)
//
for (const item: any of items) {
const id = item.id;
const parentId = item.parentId;
if (!itemMap[id]) {
itemMap[id] = {
children: [],
}
}
itemMap[id] = {
...item,
id: String(item?.id),
value: {
text: item?.name,
value: item?.value,
// 建议使用 bae64 数据
icon: "https://avatars.githubusercontent.com/u/31396322?v=4",
style: {
fill: "#00CED1",
width: "200",
},
},
children: itemMap[id]['children']
}
const treeItem = itemMap[id];
if (parentId === 0) {
result.push(treeItem);
} else {
if (!itemMap[parentId]) {
itemMap[parentId] = {
children: [],
}
}
itemMap[parentId].children.push(treeItem)
}
}
return result;
}
useEffect(() => {
fetchTableData()
}, [])
return (
<div className="content">
<OrganizationGraph
width={1000}
height={1000}
data={data}
nodeCfg={{
// type:'circle',
padding: 0,
size: [150, 40],
style: (node) => {
const num = randomNum(0, 4);
console.log('node', node)
return {
fill: colorArr[num],
};
},
label: {
style: (node: any, group, type: any) => {
const styles = {
icon: {
width: 40,
height: 40,
x: 0,
y: 0,
},
value: {
fill: "#fff",
x: 100,
// y: 4,
},
text: {
fill: "#fff",
x: 100,
y: node?.value?.value ? 4 : 16,
},
};
return styles[type];
},
},
}}
/>
</div>
);
}
编辑页面的视觉效果:
代码:
import React, { useEffect, useState } from "react";
import {
Table,
Tag,
Space,
Modal,
Button,
Form,
message,
Input,
Tooltip,
DatePicker,
Select
} from "antd";
import { PlusCircleTwoTone, EditTwoTone } from "@ant-design/icons";
import moment from 'moment';
const { Option } = Select;
export default function EditTree() {
const [tabelData, setTableData] = useState([]);
const [newPersonVisible, setNewPersonVisible] = useState(false);
const [modalOkLoading, setModalOkLoading] = useState(false);
const [currentPerson, setCurrentPerson] = useState({});
const [form] = Form.useForm();
useEffect(() => {
fetchTableData();
}, []);
const fetchTableData = () => {
fetch("http://localhost:9091/genealogy/info/selectUserPage", {
method: "POST",
body: JSON.stringify({
size: 20,
current: 1,
}),
headers: {
"content-type": "application/json",
},
}).then((res) => {
res.json().then((data) => {
setTableData(data.data);
});
});
};
const columns = [
{
title: "姓名",
dataIndex: "name",
key: "name",
render: (text) => <a>{text}</a>,
},
{
title: "年龄",
dataIndex: "date",
key: "date",
render:(record:any)=>{
let birthday = moment(record).year();
let now = moment().year();
return now - birthday
}
},
{
title: "创建日期",
dataIndex: "createTime",
key: "createTime",
},
{
title: "子女",
key: "childrenName",
dataIndex: "childrenName",
align: "center",
render: (child) => (
<>
{child && child.length > 0 ? (
child.map((item, index) => {
let color = index > 0 ? "geekblue" : "green";
return (
<Tag color={color} key={item}>
{item}
</Tag>
);
})
) : (
<a>暂无子女</a>
)}
</>
),
},
{
title: "操作",
key: "action",
render: (text, record) => (
<Space size="middle">
<Tooltip title={"新增子女"} placement="top">
<PlusCircleTwoTone
onClick={() => {
setNewPersonVisible(true);
setCurrentPerson(record);
}}
/>
</Tooltip>
<Tooltip title={"编辑个人信息"} placement="top">
<EditTwoTone />
</Tooltip>
</Space>
),
},
];
const handlModalOk = () => {
const { getFieldsValue,resetFields } = form;
let result = getFieldsValue();
if(result?.type === 'child') {
result = {
...result,
parentId: currentPerson?.id,
date: moment(result?.date).format('X') + '000'
};
delete result?.type
}else {
result = {
...result,
companionId: currentPerson?.id,
date: moment(result?.date).format('X') + '000'
};
delete result?.type
}
setModalOkLoading(true);
fetch("http://localhost:9091/genealogy/info/saveUser", {
method: "POST",
body: JSON.stringify(result),
headers: {
"content-type": "application/json",
},
}).then((res) =>
res.json().then((data) => {
if (data.isSuccess) {
setModalOkLoading(false);
setNewPersonVisible(false);
message.success("新建成功");
fetchTableData();
resetFields()
} else {
message.error(data.msg);
setModalOkLoading(false);
}
})
);
};
return (
<div>
<Table columns={columns} rowKey={(record)=> record?.id} dataSource={tabelData} />
<Modal
title={`新建${currentPerson.name}信息`}
visible={newPersonVisible}
onCancel={() => {
setNewPersonVisible(false);
}}
onOk={handlModalOk}
cancelText="取消"
okText="确定"
centered
confirmLoading={modalOkLoading}
>
<Form form={form}>
<Form.Item label="姓名" name={"name"} required>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item label="生日" name={"date"} required>
<DatePicker
style={{ width: '100%' }}
placeholder={'请选择出生日期'}
/>
</Form.Item>
<Form.Item label="类型" name={"type"} required>
<Select
style={{ width: '100%' }}
placeholder="请选择新增的是伴侣还是子女"
>
<Option value="child">子女</Option>
<Option value="companion">伴侣</Option>
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
}