h5,原生html,echarts关系网实现
1.缩放和移动功能,配置roam:true
2.他关系网的缩放和移动只能按住具体的关系网矩形才能实现,这体验不好,为了解决这个问题,我添加了两个透明的节点一个在左上角,一个在右下角,这样就能让关系网矩形做到最大,来解决这个问题。
3.设置层级来让不同的层级显示不同颜色的线。
4.关系网矩形具体是指这个红色部分。我的画布设置的宽高满屏,但echarts具体画关系网所用到的画布是根据关系网的大小决定的,具体的功能操作也只在这部分,这就造成了2的问题。
<!DOCTYPE html>
<html lang="en" style="height: 100%"><head><meta charset="UTF-8"><!-- <meta name="viewport" content="width=device-width, initial-scale=1.0">--><meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>关系网</title><style>#main {height: 100%;width: 100%;}.refresh{position: fixed;right: 25px;bottom: 25px;color: #000;padding: 10px 12px;border-radius: 100px;background: #FFFFFF;box-shadow: 0 4px 10px 0 rgba(179, 179, 179, 0.3);}.refresh-class{width: 24px;height: 24px;}.refresh-text{font-family: Source Han Sans;font-size: 10px;font-weight: 500;line-height: normal;text-align: center;letter-spacing: normal;color: #383838;}.loading {position: absolute;left: 0;top: 0;width: 100%;height: 100vh;background: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;color: white;}/* 如果你想要一个旋转动画 */.loading::after {content: "";width: 30px;height: 30px;border: 3px solid rgba(255, 255, 255, 0.3);border-radius: 50%;border-top-color: white;animation: spin 1s linear infinite;}@keyframes spin {to { transform: rotate(360deg); }}</style>
</head><body style="height: 100%; margin: 0">
<div id="main"></div>
<div id="loading" class="loading"></div>
<div class="refresh" id="refresh"><img src="./image/refresh.png" alt="" class="refresh-class"><div class="refresh-text">刷新</div>
</div><script src="./static/echarts.min.js"></script>
<script src="./static/dayjs.js"></script>
<script src="./static/md5.js"></script>
<script src="./static/wechat.env.js"></script>
<script>const salt = '81ad0be7fd53914f8cf8193c1886f635';const urlParams = new URLSearchParams(window.location.search)const token = urlParams.get('token') // 回调codeconst id = urlParams.get('id') // 回调codeconst defaultAvatar = `${sqbApiHost}/weapp/wechat/default-avatar.png`function initParams() {const salt = '81ad0be7fd53914f8cf8193c1886f635'const timestamp = dayjs().unix();const sign = hex_md5(timestamp + salt);const channel = 3;const reqConfig = {timestamp,sign,channel,};return reqConfig;}// post请求方法function postRequest(url, data, additionalHeaders = {}) {const headers = {'Content-Type': 'application/json',...additionalHeaders // 允许传入额外的头部信息};const reqUrl = sqbApiHost + urlconst reqData = {...initParams(),...data,}return fetch(reqUrl, {method: 'POST', // 指定请求方法为POSTheaders: headers,body: JSON.stringify(reqData) // 将JavaScript对象转换为JSON字符串}).then(response => {if (!response.ok) { // 检查响应是否成功throw new Error('Network response was not ok ' + response.statusText);}return response.json(); // 假设服务器返回的是JSON格式的数据}).catch(error => {console.error('There has been a problem with your fetch operation:', error);throw error; // 重新抛出错误以便调用者处理});}
</script>
<script>function assignNodeLevels(nodes = [], links = []) {// 创建一个映射以便快速通过id查找节点const nodeMap = new Map();nodes.forEach(node => {nodeMap.set(node.id, node);});// 找出根节点(没有入边的节点)const rootNodes = nodes.filter(node => {return !links.some(link => link.target === node.id);});// 如果没有根节点,可能有环,选择第一个节点作为根if (rootNodes.length === 0 && nodes.length > 0) {rootNodes.push(nodes[0]);}// 为每个根节点分配层级并开始遍历rootNodes.forEach(rootNode => {rootNode.category = 0;traverseGraph(rootNode.id, 0);});// 遍历图的辅助函数function traverseGraph(currentNodeId, currentCategory) {// 找出所有以当前节点为起点的连接const outgoingLinks = links.filter(link => link.source === currentNodeId);// 遍历这些连接outgoingLinks.forEach(link => {const targetNode = nodeMap.get(link.target);// 如果目标节点还没有层级,或者当前路径提供了更大的层级if (targetNode.category === undefined || currentCategory + 1 > targetNode.category) {targetNode.category = currentCategory + 1;traverseGraph(link.target, targetNode.category);}});}// 处理可能存在的孤立节点(没有连接的节点)nodes.forEach(node => {if (node.category === undefined) {node.category = -1; // 表示孤立节点}});return nodes;}async function formatterNodes(nodes = []) {// 使用 Promise.all + map 确保所有异步任务完成const processedNodes = await Promise.all(nodes.map(async (it) => {// 同步处理 name 和 symbolit.name = it.name.toString();if (!it?.symbol) {it.symbol = defaultAvatar;}// 特殊处理 category = 0 的节点if (it.category === 0) {it.x = window.innerWidth / 2;it.y = window.innerHeight / 2;it.fixed = true;// it.itemStyle = {// borderColor: '#FFD700',// borderWidth: 3,// borderType: 'solid'// };}// 异步获取头像并修改 symbolconst avatar = await getImgData(it.symbol);it.symbol = `image://${avatar}`;it.symbolSize= getValueByLevelLinear(it.category)return it; // 返回处理后的节点}));return processedNodes;}function convertToStringTypes(links = []) {return links?.map(link => ({source: String(link?.source),target: String(link?.target)}));}function getMaxCategory(nodes) {if (!nodes.length) return 0; // 空数组返回 0return Math.max(...nodes.map(node => node.category));}function createLevelArray(maxLevel) {return Array.from({ length: maxLevel + 1 }, (_, index) => ({name: String.fromCharCode(65 + index) // A, B, C...}));}function getValueByLevelLinear(level) {const max = 50;const min = 20;const step = 10; // 每增加一级,减少 5let value = max - level * step;return Math.max(min, value); // 确保不低于 20}
</script>
<script>var chartDom = document.getElementById('main');var myChart = echarts.init(chartDom);var option;var oldOption;function getImgData(imgSrc) {var fun = function (resolve) {const canvas = document.createElement('canvas');const context = canvas.getContext('2d');const img = new Image();img.crossOrigin = ''; // 如果需要跨域访问图片,请确保服务器支持CORSimg.onload = function () {// 计算圆直径(取原图宽高中较小的一个值以保证圆形完全填充)const diameter = Math.min(img.width, img.height);const radius = diameter / 2;// 设置画布大小为正方形,边长等于直径canvas.width = diameter;canvas.height = diameter;// 清除画布并开始绘制context.clearRect(0, 0, diameter, diameter);context.save();context.beginPath();context.arc(radius, radius, radius, 0, 2 * Math.PI); // 画出圆context.clip(); // 裁剪上面的圆形// 确保图像在圆形区域内居中显示const x = (diameter - img.width) / 2;const y = (diameter - img.height) / 2;context.drawImage(img, x, y, img.width, img.height, 0, 0, diameter, diameter);// 添加边框const borderWidth = 4; // 边框宽度,可以根据需求调整context.lineWidth = borderWidth;context.strokeStyle = '#5994ff'; // 边框颜色context.beginPath();context.arc(radius, radius, radius - borderWidth / 2, 0, 2 * Math.PI); // 绘制边框的位置需考虑边框宽度context.stroke();context.restore(); // 还原状态resolve(canvas.toDataURL('image/png', 1));};img.src = imgSrc;};var promise = new Promise(fun);return promise;}function calculateGridPositions(rows, cols, spacing = 100) {const positions = [];for (let i = 0; i < rows; i++) {for (let j = 0; j < cols; j++) {positions.push({x: j * spacing - (cols * spacing) / 2, // 水平居中y: i * spacing - (rows * spacing) / 2, // 垂直居中fixed: true});}}return positions;}let nodeData = [];let linkData = [];// 请求接口获取数据function getData(params) {postRequest('/api/net_disk_file/network', {token: token,id: +id,}).then(async res=>{// console.log('res', res);nodeData = await formatterNodes(assignNodeLevels(res.data.nodes, res.data.links))const fixedList = [{id: -1,name: "-1",user_name: "-1",symbol: "",isParent: false,x: -99999 * window.innerWidth,y: -99999 * window.innerHeight,fixed: true,symbolSize: 50,itemStyle: { opacity: 0 },},{id: -2,name: "-2",user_name: "-2",symbol: "",isParent: false,x: 99999 * window.innerWidth,y: 99999 * window.innerHeight,fixed: true,symbolSize: 50,itemStyle: { opacity: 0 },}]nodeData = [...nodeData, ...fixedList]console.log('nodeData', nodeData);linkData = convertToStringTypes(res.data.links)const maxLevel = getMaxCategory(nodeData);const categories = createLevelArray(maxLevel);initEchart(nodeData, linkData, categories);}).catch(err=>{console.log('err', err);}).finally(()=>{})}function initEchart(nodeData, linkData, categories) {option = {// title: {// text: '关系网'// },tooltip: {showContent: false},animationDurationUpdate: 1500,animationEasingUpdate: 'quinticInOut',series: [{type: 'graph',legendHoverLink: false,layout: 'force',symbolSize: 30,roam: true,label: {show: true,position: 'bottom', // 名称显示在头像下方color: '#333',formatter: function (params) {return params.data.user_name;},},edgeSymbolSize: [4, 10],edgeLabel: {fontSize: 20},focusNodeAdjacency: false,force: {repulsion: 1000, // 减小节点间斥力(值越小节点越紧凑)edgeLength: 50, // 缩短边的理想长度(值越小连线越短)gravity: 0.2, // 适当增加向心力(值越大节点越集中)friction: 0.6, // 增加摩擦系数(降低节点运动速度)},data: nodeData,links: linkData,scaleLimit: {min: 0.3, // 最小缩小到30%max: 3 // 最大放大到300%},categories: categories,emphasis: {focus: 'adjacency',lineStyle: {width: 10}},lineStyle: {color: 'source',opacity: 0.9,width: 2,curveness: 0.3}}]};oldOption = option;option && myChart.setOption(option);// 初始化图表后执行myChart.dispatchAction({type: 'downplay', // 先取消所有高亮seriesIndex: 0});// myChart.dispatchAction({// type: 'highlight', // 高亮第一个节点// seriesIndex: 0,// dataIndex: 0,// itemStyle: { // 覆盖高亮样式// borderColor: '#00FF00', // 绿色边框// borderWidth: 4// }// });document.getElementById('loading').style.display = 'none';document.getElementById('refresh').style.display = 'block';}document.addEventListener('DOMContentLoaded', function() {document.getElementById('loading').style.display = 'flex';document.getElementById('refresh').style.display = 'none';getData();});document.body.addEventListener('touchmove', function (e) {e.preventDefault();}, {passive: false});document.getElementById('refresh').addEventListener('click', function(params) {myChart.dispatchAction({ type: 'restore' });})
</script>
</body></html>