spacedesk 是一款免费的多屏扩展工具，采用主从架构实现屏幕共享与扩展，能够将安卓手机、平板、iOS 设备甚至网页端设备，转变为 Windows 电脑的副屏，实现多屏协同办公或娱乐。其核心优势如下：零硬件成本：无需额外购置显示器，可盘活闲置的手机、平板设备；灵活连接方式：支持无线局域网（WiFi）、USB 有线连接，满足不同场景的延迟需求；多模式适配：可实现扩展显示（副屏作为独立工作区）、复制显示（副屏同步主屏内容），还支持屏幕方向自定义（横屏 / 竖屏），适配编程、文档阅读等不同使用场景；触控支持：安卓端可直接通过触屏或手写笔操控电脑内容，替代鼠标操作。目前该工具仅支持 Windows 系统作为主设备（主机端），客户端可覆盖安卓、iOS 及网页端，暂不支持 Mac 系统作为主机。二、Windows 主机端安装安装前提系统要求：Windows 10（Build 14393 及以上）、Windows 8.1/8/7（Windows 7/8 版本为非官方正式支持，需自行承担风险）；硬件要求：需配备 Nvidia/AMD/Intel 等主流图形适配器，支持 WDDM 1.1 及以上驱动，DirectX 9 及以上版本；网络要求：若使用无线连接，需保证电脑与客户端设备处于同一局域网。安装步骤下载安装包：访问 spacedesk 官方下载页面，获取对应 Windows 版本的驱动程序（安装包为 spacedesk Driver Software）；运行安装向导：双击安装包，按照提示点击 “下一步” 完成安装，过程中若弹出 Windows 防火墙权限请求，务必选择 “允许访问”，否则会导致后续设备无法连接；确认安装成功：安装完成后，电脑右下角任务栏会出现 spacedesk 绿色图标，图标显示绿色对勾即代表服务启动成功，右键点击图标可打开「spacedesk driver Console」进行后续设置。汉化方式，以下为汉化后的页面，汉化版本较低，一些功能未完整展示，可满足使用局域网连接三、安卓端客户端安装安装前提系统要求：安卓 4.1 及以上版本；权限要求：若通过非官方渠道安装，需开启 “未知来源应用安装” 权限（路径：设置→安全→允许未知来源）。安装渠道与步骤渠道 1：官方渠道（推荐）访问 spacedesk 安卓端专属下载页：https://www.spacedesk.net/android，点击「Download spacedesk Android client」获取官方 APK 安装包（无需依赖 Google Play）；将 APK 传输至安卓设备，双击安装即可。渠道 2：Google Play应用商店搜索spacedesk VIEWER for Android，下载安装即可四、使用方法前期准备确保 Windows 主机端服务已启动（任务栏图标为绿色对勾），且安卓设备与电脑处于同一 WiFi 网络；若使用 USB 连接，需提前为安卓设备开启 “开发者选项” 和 “USB 调试” 功能。连接步骤方式 1：无线连接（便捷首选）打开安卓端 spacedesk 应用，APP 会自动扫描局域网内的 Windows 主机；在扫描列表中点击对应电脑的 IP 地址或主机名，首次连接需在电脑端确认授权，授权后即可完成连接；若未扫描到主机，可点击 APP 右上角 “+” 按钮，手动输入电脑的 IP 地址（可在电脑端「spacedesk driver Console」中查询）进行连接。方式 2：USB 有线连接（低延迟首选）用数据线将安卓设备与电脑连接，确保安卓端已开启 “USB 调试”（如果识别不到可以关闭USB调试，再重新打开，spacedesk会识别到）；在电脑端「spacedesk driver Console」中启用 “USB (Android)” 选项；打开安卓端 spacedesk 应用，APP 会自动识别 USB 连接，点击连接即可建立低延迟通信。多屏模式设置连接成功后，Windows 会将安卓设备识别为第二显示器，可通过以下步骤调整显示模式：在电脑桌面右键，选择「显示设置」；在 “多显示器” 选项中选择模式：扩展这些显示器：副屏作为独立工作区，可将窗口拖拽至安卓设备屏幕，实现多任务并行（如主屏写文档、副屏查资料）；复制这些显示器：副屏同步主屏内容，适合会议演示场景；可拖拽显示器示意图调整物理位置，还能在 “显示方向” 中设置副屏为横屏或竖屏，适配不同使用需求。画质与性能优化在安卓端 spacedesk 应用的「setting」-「quality/performance」中，可根据网络情况调整参数：追求清晰度：提高画质分辨率，降低压缩率；追求流畅度：降低分辨率，提升压缩率，同时关闭安卓设备动态壁纸、后台冗余应用，减少系统负担。五、下载链接下载文件
效果图：代码实现：import React, { Component } from 'react'; import * as echarts from 'echarts'; import moment from 'moment' const { registerReactDom, logoutReactDom } = scriptUtil; let style = document.createElement('style'); style.innerHTML = ` .echarts-container { width: 100%; height: calc(100% - 80px); display: flex; justify-content: flex-start; flex-wrap: wrap; overflow: auto; // background:rgb(80, 202, 117); } .echarts-container li { list-style: none; margin-right: 20px; margin-bottom: 20px; border: 1px solid #000; } .echarts-container li:nth-child(3n){ margin-right: 0; } .echarts-container li:last-child{ margin-bottom: 0; } .no-data{ width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 20px; color: #000; } `; document.head.appendChild(style); class CustomComp extends Component { state = { data: {}, xData: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], chartInstances: {}, currentHighlightIndex: null, chartType: 'bar' // 图表类型状态 } componentDidMount = () => { registerReactDom(this, this.props); } componentWillUnmount() { logoutReactDom(this, this.props); } componentDidUpdate(prevProps, prevState) { if (prevState.currentHighlightIndex !== this.state.currentHighlightIndex) { this.highlightCharts(this.state.currentHighlightIndex); } if (prevState.chartType !== this.state.chartType) { this.updateAllCharts(); } } // 数据初始化 init = (data) => { let newData = _.groupBy(data, 'pn'); // 按pn分组 /* 示例数据： {"B80005210603181143": [{dataDate:"2025-01", valSUM: 124829},...], "B80005210603195057": [{dataDate:"2025-01", valSUM: 124829},...]} */ // console.log(newData, 'newData'); this.setState({ data: newData }) Object.keys(newData).forEach((key) => { let obj = newData[key]; let yData = []; obj.forEach((item) => { yData.push(item.valSUM); }) this.chartInit('analysis' + key, yData, obj[0].deviceName, key); }) } // 图表初始化 chartInit = (id, yData, name, key) => { let { xData, chartType } = this.state; let myChart = echarts.init(document.getElementById(id)); var option = { backgroundColor: 'transparent', tooltip: { trigger: 'axis', axisPointer: { type: 'cross', // type: 'none', crossStyle: { color: '#000' } }, // 添加事件监听器 showContent: true, formatter: params => { let index = params[0].dataIndex; let month = xData[index]; let value = params[0].value.toFixed(2); let seriesName = params[0].seriesName; this.setState({ currentHighlightIndex: index }); // return params[0].seriesName + ' ' + xData[index] + '月: ' + params[0].value.toFixed(2) + 'kw'; return ` ${seriesName} ${month}月 ${value} kw `; }, // 允许使用 HTML 内容 useHTML: true, // 可选：设置 tooltip 的样式 extraCssText: 'background-color: #37586d; color: #fff; padding: 0px 0px; border-radius: 2px; line-height: 1.5;border:none;' }, grid: { top: '10%', right: '3%', left: '12%', bottom: '8%' }, // 工具配置 toolbox: { feature: { dataView: { show: false, readOnly: false }, magicType: { show: true, type: ['line', 'bar'] }, restore: { show: false }, saveAsImage: { show: false } }, iconStyle: { borderColor: "#000", // 图标默认颜色 }, emphasis: { iconStyle: { borderColor: "#000", // 图标hover颜色 }, }, }, legend: { data: [name], type: 'scroll', icon: 'circle', top: 5, right: 150, textStyle: { color: '#000', margin: '10', }, }, xAxis: [ { type: 'category', data: xData, axisPointer: { type: 'shadow' }, axisLabel: { margin: 10, color: '#000', textStyle: { fontSize: 16 }, }, } ], yAxis: [ { type: 'value', name: "kw", nameTextStyle: {//y轴上方单位的颜色 color: '#000' }, splitLine: { show: false }, axisLabel: { formatter: '{value}', color: '#000', fontSize: 16 }, axisLine: { // 添加y轴轴线 show: true, lineStyle: { color: '#000' // 轴线颜色 } }, axisTick: { // 添加y轴刻度 show: true, lineStyle: { color: '#000' // 刻度颜色 } } } ], series: [ { name: name, type: chartType, // barWidth: '15%', // itemStyle:{ // color: '#DAC8A3' // }, data: yData }, ] }; myChart.setOption(option, true) // 保存图表实例 this.setState(prevState => ({ chartInstances: { ...prevState.chartInstances, [key]: myChart } })); // 添加事件监听器 myChart.on('mouseover', (params) => { this.setState({ currentHighlightIndex: params.dataIndex }); }); myChart.on('mouseout', () => { this.setState({ currentHighlightIndex: null }); }); // 图表类型切换 myChart.on('magictypechanged', (params) => { this.setState({ chartType: params.currentType }); }); } // 高亮图表 highlightCharts(index) { const { chartInstances } = this.state; Object.keys(chartInstances).forEach(key => { let chart = chartInstances[key]; // 隐藏所有tooltip // chart.dispatchAction({ // type: 'hideTip' // }); // // 取消高亮所有柱子 // chart.dispatchAction({ // type: 'downplay' // }); // 包含清除tooltip、柱子高亮、坐标轴指示器 chart.dispatchAction({ type: 'updateAxisPointer', xAxisIndex: 0, // 对应你的 x 轴索引 yAxisIndex: 0, // 对应你的 y 轴索引 tooltip: { show: false // 强制隐藏指示器 } }); // 完全重置图表状态，如果有图表类型切换，会被重置 // chart.dispatchAction({ // type: 'restore' // }); }); // 高亮图表 if (index !== null) { Object.keys(chartInstances).forEach(key => { let chart = chartInstances[key]; // 触发tooltip显示 chart.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: index }); // 高亮柱子 chart.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: index }); }); } } // 切换图表类型 updateAllCharts = () => { const { chartInstances, chartType } = this.state; Object.keys(chartInstances).forEach(key => { let chart = chartInstances[key]; let option = chart.getOption(); option.series[0].type = chartType; chart.setOption(option); }); }; render() { const { data } = this.state; return ( {Object.keys(data).length > 0 ? Object.keys(data).map((key) => { return ( ) }) : 未查询到数据 } ); } } export default CustomComp;
效果图：visualMap：min 和 max：设置数据映射的最小值和最大值。inRange：定义一个映射的范围，可以设置颜色、大小等属性。outOfRange：定义超出范围的映射属性。orient：设置组件的方向，可以是 ‘horizontal’ 或 ‘vertical’。left、right、top、bottom：设置组件的位置。splitNumber：设置分段型 visualMap 的分段数。pieces：设置分段型 visualMap 的各段范围和样式。fetColumnData = (dataX, dataY, unit) => { const chart = echarts.init(this.chartRef.current); const color = ['rgb(39, 42, 233)', 'rgb(15, 216, 206)'] const color1 = [ 'rgba(83, 246, 211, 0.57)', 'rgba(255, 165, 0, 0.57)', 'rgba(92, 222, 109, 0.57)', 'rgba(0, 255, 0, 0.57)', 'rgba(255, 0, 0, 0.57)', 'rgba(66, 168, 218, 0.57)', 'rgba(240, 106, 106, 0.57)', 'rgba(230, 122, 240, 0.57)', 'rgba(255, 255, 0, 0.57)', 'rgba(0, 0, 255, 0.57)' ] let pieces = []; for (let i = 1; i < dataY.length; i++) { let startColor = color1[0]; if (dataY[i] > dataY[i - 1]) { startColor = color1[1]; } pieces.push({ gte: i - 1, lte: i, color: startColor }); } var hasDataOption = { backgroundColor: 'rgba(20, 58, 110,0)', color: ['#3cefff'], grid: { top: '20%', bottom: '3%', left: '3%', right: '5%', containLabel: true, }, tooltip: { trigger: 'axis', formatter: function (params) { return ` ${params[0]["name"]} 月费用：${params[0]["value"]} `; }, extraCssText: 'background: #fff; border-radius: 0;box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);color: #333;', }, xAxis: [ { name: '月', nameTextStyle: { color: '#fff' }, type: 'category', data: dataX, axisTick: { alignWithLabel: true, }, nameTextStyle: { color: '#82b0ec', }, axisLine: { lineStyle: { color: '#82b0ec', }, }, axisLabel: { formatter: '{value}', textStyle: { color: '99b3d6', fontSize: '16' }, }, }, ], yAxis: [ { name: unit, nameTextStyle: { color: '#fff' }, type: 'value', axisLabel: { textStyle: { fontSize: '13', color: '#99b3d6', }, formatter: '{value}', }, splitLine: { show: true, lineStyle: { type: 'dashed', color: 'RGBA(3, 75, 97, 0.8)', }, }, axisLine: { show: false, }, }, ], visualMap: { type: 'piecewise', show: false, dimension: 0, seriesIndex: 0, pieces: pieces }, series: [ { data: dataY, name: '', // smooth: false, type: "line", lineStyle: { width: 1, color: function (params) { let color = color1[0]; // 默认为下降或相同颜色 if (params.dataIndex > 0 && dataY[params.dataIndex] > dataY[params.dataIndex - 1]) { color = color1[1]; // 如果比前一个数据大，则为上升颜色 } return color; } }, areaStyle: {}, // areaStyle: { // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ // offset: 0, // color: color1[1][0] // 开始颜色，红色渐变到结束颜色透明度为0的黑色渐变效果。可以根据需要调整颜色和透明度。 // }, { // offset: 1, // color: color1[1][1] // 结束颜色，完全透明。可以根据需要调整颜色和透明度。 // }]) // }, showSymbol: true, // 隐藏折线图上的圆点 symbol: 'circle', symbolSize: 7, } ], }; chart.setOption(hasDataOption) } html：
思路：使用nodejs搭建服务端，接收到客户端发送的问题后返回加载响应，然后调用ai接口，等待ai接口响应后将响应结果通过sse推送给客户端；使用react搭建页面，主要由问题输入框、发送按钮和问答信息展示模块步骤：创建node项目，安装相关依赖项npm init -y npm install express openai body-parser cors创建server.js文件，配置服务const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); const OpenAI = require("openai"); const app = express(); const port = 3001; app.use(cors()); app.use(bodyParser.json()); // deepseek 配置 // const openai = new OpenAI({ // baseURL: 'https://api.deepseek.com/v1', // apiKey: '你的key' // }); // 存储 SSE 客户端连接 const clients = new Map(); // SSE 端点 app.get('/stream', (req, res) => { const clientId = Date.now().toString(); res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // 存储连接并发送客户端 ID clients.set(clientId, res); res.write(`event: clientId\ndata: ${clientId}\n\n`); // 心跳保持连接 const heartbeat = setInterval(() => res.write(': heartbeat\n\n'), 30000); req.on('close', () => { clearInterval(heartbeat); clients.delete(clientId); }); }); // 消息处理端点 app.post('/chat', async (req, res) => { const { clientId, message } = req.body; // 立即返回等待提示 res.status(200).send('AI 正在思考中...'); // 模拟 AI 接口调用 setTimeout(async () => { try { const aiResponse = `AI回复：${Math.random() * 100}`; // 替换为真实 AI 响应 const clientRes = clients.get(clientId); if (clientRes) { clientRes.write(`event: response\n`); clientRes.write(`data: ${JSON.stringify({ text: aiResponse })}\n\n`); } } catch (error) { const clientRes = clients.get(clientId); clientRes?.write(`event: error\ndata: ${error.message}\n\n`); } }, 2000); }); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); }); 搭建前端项目npx create-react-app ai-chat cd ai-chat npm install axios在src文件下，创建Chat.js文件，搭建简单聊天页面import React, { useState, useEffect } from 'react'; import axios from 'axios'; function Chat() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [clientId, setClientId] = useState(null); useEffect(() => { const es = new EventSource('http://localhost:3001/stream'); es.addEventListener('clientId', (e) => { setClientId(e.data); }); es.addEventListener('response', (e) => { const data = JSON.parse(e.data); setMessages(prev => [...prev, { text: data.text, type: 'Ai' }]); }); es.addEventListener('error', (e) => { console.error('SSE Error:', e.data); }); return () => { es.close(); }; }, []); const sendMessage = async () => { if (!input || !clientId) return; // 添加用户消息 setMessages(prev => [...prev, { text: input, type: 'user' }]); try { const response = await axios.post('http://localhost:3001/chat', { clientId: clientId, message: input }, { headers: { 'Content-Type': 'application/json', }, }); console.log('Server response:', response.data); // 假设服务器返回的消息格式为 setMessages(prev => [...prev, { text: response.data, type: 'Ai' }]); console.log('Messages:', messages); setInput(''); } catch (error) { console.error('Error while sending message:', error); } }; return ( setInput(e.target.value)} placeholder="输入你的消息..." /> 发送 {messages.map((message, index) => ( {message.type}: {message.text} ))} ); } export default Chat;修改App.js文件，引入搭建好的聊天页面import './App.css'; import Chat from './Chat'; function App() { return ( <Chat /> ); } export default App;在server.js同级目录下，启动服务node server.js在ai-chat文件夹下，启动前端项目npm start在浏览器中访问：http://localhost:3000 即可看到简单的聊天界面，发送消息后能收到服务端使用sse推送的消息，替换为真实的ai请求即可与AI聊天
// 矩形透明蒙版和准星线只能同时存在一种显示矩形透明蒙版xAxis: { ... crosshair: true }, plotOptions: { column: { pointWidth: 30, pointPadding: 0.3, borderWidth: 0, states: { hover: { enabled: true, opacity: 0.7 // 设置悬停时的透明度 } } } }, series: [{ name: '销量', data: data, states: { hover: { enabled: true, color: 'rgba(255, 165, 0, 0.5)' // 悬停时的颜色，使用 rgba 实现透明效果 } } }],显示十字准星线xAxis: { ..., crosshair: { width: 2, color: 'red', // 交叉线颜色 dashStyle: 'ShortDash', // 交叉线样式 zIndex: 999 } }, yAxis: { ..., crosshair: { width: 2, color: 'red', // 交叉线颜色 dashStyle: 'ShortDash', // 交叉线样式 zIndex: 999 } },配置自动轮播let cur = 0 setInterval(() => { // 每次清除鼠标移入效果，防止用户鼠标移入导致样式错乱 if (cur > 0) { hChart.series[0].data[cur - 1].onMouseOut(); } hChart.series[0].data[cur].onMouseOver(); cur++ if(cur==data.length)cur=0 }, 2000);
组成标题（Title）：图表标题，包含标题和副标题（subTitle）坐标轴（Axis）：包含x轴（xAxis）和y轴（yAxis）图例（Legend）：用不同形状、颜色、文字等 标示不同数据列，通过点击标示可以显示或隐藏该数据列版权信息（Credits）：包含链接的文字，通过指定credits.enabled=false即可不显示该信息数据提示框（Tooltip）：鼠标悬停在某点上时，以框的形式提示该点的数据工具菜单（Exporting）：通过引入exporting.js即可增加图表导出为常见文件功能数据列（Series）：一个或多个数据系列配置基础配置：chartchart: { type: 'line', // 图表类型，如 'line', 'column', 'bar', 'pie', 'scatter' 等 backgroundColor: null, // 背景色，可以设置为颜色（如 '#FFFFFF'）或 'none' borderColor: '#A0A0A0', // 图表边框颜色 borderWidth: 2, // 图表边框宽度 borderRadius: 0, // 边框圆角 height: 400, // 图表高度（像素） width: 600, // 图表宽度（像素） margin: [50, 50, 50, 50], // 图表的内边距 [上, 右, 下, 左] marginTop: null, // 上部边距 marginRight: null, // 右部边距 marginBottom: null, // 下部边距 marginLeft: null, // 左部边距 spacing: [10, 10, 15, 10], // 图表的外边距 [上, 右, 下, 左] spacingTop: null, // 顶部外边距 spacingRight: null, // 右侧外边距 spacingBottom: null, // 底部外边距 spacingLeft: null, // 左侧外边距 plotBackgroundColor: null, // 绘图区的背景颜色 plotBorderColor: '#A0A0A0', // 绘图区的边框颜色 plotBorderWidth: 0, // 绘图区的边框宽度 plotShadow: false, // 是否为绘图区添加阴影 panning: false, // 是否允许平移图表 pinchType: 'x', // 手势缩放类型，'x', 'y', 'xy', 'none' selectionMarkerFill: 'rgba(69,114,167,0.25)', // 选择区域的填充颜色 events: { // 事件处理 load: function() {}, // 图表加载后触发的事件 click: function() {} // 图表点击时触发的事件 }, options3d: { // 配置3D效果 enabled: true, // 是否启用 3D，值为 true 或 false alpha: 0, // 视角的倾斜角度 beta: 15, // 视角的旋转角度 depth: 30, // 深度值，表示 3D 视觉效果的强度 frame: { back: { size: 1, color: 'rgba(0,0,0,0.05)' }, // 后面，薄且透明的黑色 front: { size: 1, color: 'rgba(0,0,0,0.1)' }, // 前面，稍厚的黑色 right: { size: 1, color: 'rgba(0,0,0,0.1)' }, // 右侧，稍厚的黑色 left: { size: 1, color: 'rgba(0,0,0,0.1)' }, // 左侧，稍厚的黑色 bottom: { size: 1, color: 'rgba(0,0,0,0.1)' }, // 底面，稍厚的黑色 } } }标题：Title/subtitletitle: { // title，subtitle text: '默认标题', // 图表的主标题文本 align: 'center', // 标题的对齐方式，可以是 'left'、'center' 或 'right' verticalAlign: 'top', // 标题的垂直对齐方式，可以是 'top'、'middle' 或 'bottom' x: 0, // 标题的水平偏移，单位为像素 y: 0, // 标题的垂直偏移，单位为像素 style: { // 标题的CSS样式 color: '#333333', // 文字颜色 fontSize: '18px', // 字体大小 fontWeight: 'bold', // 字体粗细，可以是 'normal'、'bold'、'lighter' 等 fontFamily: 'Verdana, sans-serif', // 字体系列 textOutline: 'none' // 文字轮廓，设置为 'none' 可以去掉轮廓 }, useHTML: false, // 如果设置为 true，将使用 HTML 渲染标题，允许更多样式 floating: false, // 如果设置为 true，将允许标题浮动在图表上，而不是依赖于图表边界 backgroundColor: null, // 标题背景色 borderColor: null, // 标题边框颜色 borderWidth: 0, // 标题边框宽度 borderRadius: 0, // 边框圆角 shadow: false // 是否为标题添加阴影 }坐标轴：AxisxAxis: { title: { text: '月份', align: 'middle', // 标题对齐方式，可选 'high'、'middle'、'low' offset: 0, // 标题与轴的距离 rotation: 0, // 标题旋转角度 style: { // 标题样式 color: '#333333', fontSize: '12px' } }, categories: ['一月', '二月', '三月', '四月', '五月'], labels: { step: 2, // 每隔两个标签显示一个 rotation: -45, // 倾斜 style: { color: '#333333' } }, gridLineWidth: 1, // 网格线 lineWidth: 4, // 基线 lineColor: '#3ef7ff', min: 0, max: 4, tickInterval: 1, crosshair: { // 准星线 width: 1, color: 'green', // 交叉线颜色 dashStyle: 'ShortDash', // 交叉线样式 zIndex: 999 } } yAxis: { title: { text: '销售额' }, labels: { enabled: true, // 是否显示标签 formatter: function() { return this.value + '元'; // 自定义标签格式 }, style: { color: '#333333' } }, gridLineWidth: 0, // 纵向网格线 min: 0, tickInterval: 50, // 设置刻度间隔 plotLines: [{ // 绘制参考线 value: 300, // 参考值 color: 'red', // 参考线颜色 width: 2, // 参考线宽度 zIndex: 5 // 参考线层级 }] }图例：Legendlegend: { enabled: true, // 是否显示图例 align: 'center', // 图例的水平对齐方式 ('left', 'center', 'right') verticalAlign: 'top', // 图例的垂直对齐方式 ('top', 'middle', 'bottom') layout: 'horizontal', // 图例的布局方式 ('horizontal', 'vertical') floating: false, // 是否使图例浮动 borderWidth: 1, // 图例边框宽度 borderColor: '#999999', // 图例边框颜色 backgroundColor: null, // 图例背景颜色 shadow: true, // 是否开启阴影效果 itemStyle: { // 图例项的CSS样式 color: '#333333', // 文本颜色 fontSize: '12px', // 字体大小 fontWeight: 'normal' // 字体粗细 }, itemHiddenStyle: { // 隐藏项的CSS样式 color: '#cccccc' // 隐藏项的文本颜色 }, itemHoverStyle: { color: '#FF0000', // 鼠标悬停时字体颜色 textDecoration: 'underline', // 鼠标悬停时下划线 cursor: 'pointer' // 鼠标悬停时光标样式 }, itemMarginBottom: 5, // 每个图例项之间的下边距 symbolWidth: 16, // 图例符号的宽度 symbolHeight: 16, // 图例符号的高度 verticalItemWidth: 16, // 纵向图例项的宽度 useHTML: false, // 是否使用 HTML 渲染图例 floating: false, // 是否浮动 itemDistance: 10 // 项之间的距离 } // 配置图例项宽高 .highcharts-legend-item { height: 30px; /* 设置高度 */ width: 100px; /* 设置宽度 */ }版权信息：Creditscredits: { enabled: true, // 是否显示版权信息 href: 'https://www.highcharts.com', // 点击版权信息时跳转的 URL position: { // 版权信息的位置设置 align: 'right', // 水平对齐方式 ('left', 'center', 'right') verticalAlign: 'bottom', // 垂直对齐方式 ('top', 'middle', 'bottom') x: -10, // 水平偏移，单位为像素 y: -5 // 垂直偏移，单位为像素 }, style: { // 版权信息的 CSS 样式 color: '#999999', // 字体颜色 fontSize: '10px', // 字体大小 cursor: 'pointer' // 鼠标样式 }, text: 'Highcharts.com' // 自定义版权文本 }数据提示框：Tooltip// 数据提示框展示需要在 plotOptions 中将鼠标跟踪开启：enableMouseTracking: true, // 启用鼠标跟踪 tooltip: { enabled: true, // 启用 tooltip formatter: function() { return '类别: ' + this.x + ' 值: ' + this.y; }, useHTML: true, // 使用 HTML 渲染 style: { // 设置样式 color: '#FFFFFF', fontSize: '12px', padding: 10, borderRadius: 5, backgroundColor: '#333333' }, shared: true, // 共享 tooltip crosshairs: true, // 显示十字线 borderColor: '#A0A0A0', // 设置边框 borderWidth: 1 }工具菜单：Exportingexporting: { enabled: true, // 启用导出功能 filename: 'my_chart', // 设置导出文件名 type: 'image/png', // 设置导出类型 scale: 2, // 导出图像的缩放倍数 buttons: { // 自定义上下文按钮菜单 contextButton: { menuItems: [ 'printChart', // 打印图表 'separator', // 分隔线 'downloadPNG', // 导出为 PNG 'downloadJPEG', // 导出为 JPEG 'downloadPDF', // 导出为 PDF 'downloadSVG', // 导出为 SVG { text: '自定义选项', // 自定义菜单项 onclick: function () { alert('自定义功能执行'); // 这里可以执行自定义代码 } } ] } } }数据列：Seriesseries: [{ type: 'line', // 系列类型，例如 'line', 'column', 'pie' 等 name: '系列名称', // 系列的名称，用于图例和 tooltip data: [1, 3, 2, 4], // 系列的数据点，可以是数字或对象形式 // 对象形式： // [ // { y: 34.4, name: '一月' }, // { y: 90.3, name: '二月' }, // { y: 123.4, name: '三月' } // ] color: '#FF0000', // 系列的颜色 lineWidth: 2, // 线图的线宽 marker: { // 数据点标记的配置 enabled: true, // 是否显示标记 radius: 4, // 标记的半径 states: { // 标记的状态配置 hover: { enabled: true, radiusPlus: 2 // 悬停时标记的半径增加值 } } }, dataLabels: { // 数据标签配置 enabled: true, // 是否启用数据标签 format: '{y}', // 标签格式 style: { // 标签样式 color: '#333333', fontSize: '12px' } }, tooltip: { // Tooltip 配置 pointFormat: '{point.y}' // Tooltip 格式 }, enableMouseTracking: true, // 是否启用鼠标跟踪 // 面积图渐变配置 /* fillColor: { linearGradient: { // 使用线性渐变 x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [ [0, Highcharts.getOptions().colors[0]], // 上方颜色 [1, Highcharts.Color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')] // 下方颜色透明 ] }*/ }]通用系列配置：plotOptionsplotOptions: { series: { // 通用的系列配置 enableMouseTracking: true, // 启用鼠标跟踪 animation: true, // 是否启用动画 marker: { // 数据点标记的样式配置 enabled: true, // 是否显示标记 radius: 4, // 标记的半径 states: { // 标记的状态配置 hover: { enabled: true, // 鼠标悬停时是否启用 radiusPlus: 2 // 悬停时标记的增加半径 } } }, dataLabels: { // 数据标签的配置 enabled: false, // 是否显示数据标签 format: '{y}', // 数据标签的格式 style: { // 数据标签的样式 color: '#000000', fontSize: '12px' } }, tooltip: { // Tooltip 配置 (也可以在全局 tooltip 中直接配置) pointFormat: '{series.name}: {point.y}', // Tooltip 格式 shared: true // 是否共享 tooltip } }, column: { // 针对柱状图的特定配置 maxPointWidth: 30, // 最大宽度 pointPadding: 0.2, // 点之间的填充 groupPadding: 0.1 // 组之间的填充 }, line: { // 针对线图的特定配置 marker: { // 线图数据点标记的配置 enabled: true, radius: 3 } }, pie: { // 针对饼图的特定配置 allowPointSelect: true, // 是否允许选择某个扇形 cursor: 'pointer', // 鼠标光标样式 dataLabels: { // 饼图数据标签的配置 enabled: true, format: '{point.name}: {point.y}%' // 数据标签的格式 } } }参考材料：highCharts中文教程highCharts官网
集合_.reduce()：对数组或集合进行归约操作。它从数组的第一个元素开始，依次对每个元素执行一个用户提供的函数，并将结果累积到一个累加器上，最终返回累加器的值_.reduce(collection, iteratee, [accumulator], [initAccum]) /** * collection: 要遍历的集合，可以是数组、对象等。 * iteratee: 用于每个元素的回调函数，可以接收四个参数：accumulator（累加器），value（当前元素），index（当前索引），collection（集合本身）。 * accumulator: （可选）初始值，如果未提供，则将集合的第一个元素作为初始值。 * initAccum: （可选）布尔值，指示是否将第一个元素作为初始值，默认为 false。 */ let sum = _.reduce([1, 2, 3, 4], (accumulator, value)=> { return accumulator + value; }, 0); console.log(sum); // 输出: 10_.groupBy()：将集合（数组或对象）的元素按特定条件分组。它会创建一个对象，其中每个键代表分组的条件，值为满足该条件的元素数组_.groupBy(collection, iteratee) /** * collection: 要进行分组的集合，可以是数组或对象。 * iteratee: 分组的条件，可以是属性名、函数或者字符串，如果是字符串，表示根据该属性的值进行分组。 */ const users = [ { 'user': 'Alice', 'age': 25, 'active': true }, { 'user': 'Bob', 'age': 30, 'active': false }, { 'user': 'Charlie', 'age': 25, 'active': true }, { 'user': 'David', 'age': 30, 'active': true } ]; // 按年龄分组 const groupedByAge = _.groupBy(users, 'age'); console.log(groupedByAge); /* 输出: { '25': [ { 'user': 'Alice', 'age': 25, 'active': true }, { 'user': 'Charlie', 'age': 25, 'active': true } ], '30': [ { 'user': 'Bob', 'age': 30, 'active': false }, { 'user': 'David', 'age': 30, 'active': true } ] } */_.countBy()：对集合中的元素进行计数。它会创建一个对象，其中每个键表示某个特定条件的结果，值为该条件对应的元素数量_.countBy(collection, iteratee) /** * collection: 要进行计数的集合，可以是数组或对象。 * iteratee: 计数的条件，可以是属性名、函数或者字符串。如果是字符串，表示根据该属性的值进行计数。 / const users = [ { 'user': 'Alice', 'age': 25, 'active': true }, { 'user': 'Bob', 'age': 30, 'active': false }, { 'user': 'Charlie', 'age': 25, 'active': true }, { 'user': 'David', 'age': 30, 'active': true } ]; // 按年龄计数 const countByAge = _.countBy(users, 'age'); console.log(countByAge); /* 输出: { '25': 2, '30': 2 } */_.filter() ：用于从集合（数组或对象）中筛选出符合指定条件的元素。它返回一个新数组，包含所有满足条件的元素_.filter(collection, predicate) /** * collection: 要进行过滤的集合，可以是数组或对象。 * predicate: 筛选条件，可以是函数、属性名或其他条件。如果是函数，它会对每个元素进行测试，返回 true 或 false 来决定该元素是否包含在结果中 / const users = [ { 'user': 'Alice', 'age': 25, 'active': true }, { 'user': 'Bob', 'age': 30, 'active': false }, { 'user': 'Charlie', 'age': 25, 'active': true }, { 'user': 'David', 'age': 30, 'active': true } ]; // 通过活跃状态过滤用户 const activeUsers = _.filter(users, { 'active': true }); console.log(activeUsers); /* 输出: [ { 'user': 'Alice', 'age': 25, 'active': true }, { 'user': 'Charlie', 'age': 25, 'active': true }, { 'user': 'David', 'age': 30, 'active': true } ]数组_.chunk()：将数组（array）拆分成多个 size 长度的区块，并将这些区块组成一个新数组。 如果array 无法被分割成全部等长的区块，那么最后剩余的元素将组成一个区块_.chunk(array, [size=1]) _.chunk(['a', 'b', 'c', 'd'], 2); // => [['a', 'b'], ['c', 'd']]函数_.once()：创建一个只能调用 func 一次的函数。 重复调用返回第一次调用的结果。 func 调用时， this 绑定到创建的函数，并传入对应参数var initialize = _.once(createApplication); initialize(); initialize(); // `initialize` 只能调用 `createApplication` 一次。其它_.cloneDeep()：深拷贝var objects = [{ 'a': 1 }, { 'b': 2 }]; var deep = _.cloneDeep(objects); console.log(deep[0] === objects[0]); // => false
改变原数组的方法push：向数组末尾添加一个或多个元素。pop：从数组末尾删除一个元素。shift：从数组开头删除一个元素。unshift：向数组开头添加一个或多个元素。splice：可以在任意位置添加或删除元素。reverse：反转数组中元素的顺序。sort：对数组的元素进行排序。fill：用指定的值填充数组的某部分。不改变原数组的方法slice：返回数组的某个部分，生成一个新数组。concat：合并两个或多个数组，返回一个新数组。join：将数组的所有元素连接成一个字符串。map：对数组的每个元素执行给定的函数，返回一个新数组。forEach：对数组的每个元素执行给定的函数，没有返回值。reduce：对数组的每个元素执行给定的函数，最终计算出一个值。filter：筛选数组中的元素，返回满足条件的元素的新数组。find：返回数组中第一个满足条件的元素。includes：判断数组是否包含某个元素。some：判断数组中至少有一个元素满足条件。every：判断数组中的所有元素是否都满足条件。push()：向数组末尾添加一个或多个元素，返回新数组的长度const arr = [1, 2, 3]; arr.push(4); console.log(arr); // [1, 2, 3, 4]pop()：移除数组末尾的元素，并返回该元素const arr = [1, 2, 3]; const lastElement = arr.pop(); console.log(lastElement); // 3 console.log(arr); // [1, 2]shift()：移除数组开头的元素，并返回该元素const arr = [1, 2, 3]; const firstElement = arr.shift(); console.log(firstElement); // 1 console.log(arr); // [2, 3]unshift()：向数组开头添加一个或多个元素，返回新数组的长度const arr = [2, 3]; arr.unshift(1); console.log(arr); // [1, 2, 3]splice()：从数组中添加或删除元素，返回被删除的元素。可以添加多个元素到指定位置const arr = [1, 2, 3, 4]; const removed = arr.splice(1, 2, 5, 6); // 从索引1开始删除2个元素，并插入5和6 console.log(arr); // [1, 5, 6, 4] console.log(removed); // [2, 3]slice()：返回数组的一个片段，复制数组的一部分，并返回新数组const arr = [1, 2, 3, 4]; const sliced = arr.slice(1, 3); // 从索引1到索引3（不包括3） console.log(sliced); // [2, 3]concat()：用于合并两个或多个数组，返回一个新数组const arr1 = [1, 2]; const arr2 = [3, 4]; const combined = arr1.concat(arr2); console.log(combined); // [1, 2, 3, 4]join()：将数组的所有元素连接成一个字符串，返回连接后的字符串const arr = ['Hello', 'World']; const str = arr.join(' '); // 用空格连接 console.log(str); // "Hello World"reverse()：反转数组中的元素顺序const arr = [1, 2, 3]; arr.reverse(); console.log(arr); // [3, 2, 1]sort()：对数组元素进行排序const arr = [3, 1, 4, 2]; arr.sort(); // 默认按字符排序 console.log(arr); // [1, 2, 3, 4] arr.sort((a,b)=>b-a); // 逆序排序 console.log(arr); // [4, 3, 2, 1]map()：用于对数组中的每个元素执行一个函数，并返回一个新数组const numbers = [1, 2, 3]; const doubled = numbers.map(num => num * 2); console.log(doubled); // [2, 4, 6]forEach()：遍历数组中的每个元素，并对每个元素执行一个指定的函数，不返回新数组const colors = ['red', 'green', 'blue']; colors.forEach(color => console.log(color));reduce()：用于对数组进行累积操作，它可以用来求和、求积、统计元素出现次数、数组去重等/** * 参数一： callback 函数（执行数组中每个值的函数，包含四个参数）： * prev 必需 （上一次调用回调返回的值，或者是提供的初始值（initialValue）） * cur 必需（数组中当前被处理的元素） * index 可选 （当前元素在数组中的索引） * arr 可选 （调用 reduce 的数组） * 参数二：initialValue 可选 （表示初始值，作为第一次调用 callback 的第一个参数。） * 提供初始值，cur 从数组第一项开始，若不提供初始值，则 cur 从第二项开始执行，对应的第一次 prev 是数组第一项的值 */ // arr.reduce(callback,[initialValue]) array.reduce((prev, cur, index, arr)=> { /***/ }, initialValue) // 例子 let list = [1,2,3,4]; list = list.reduce((pre,cur)=>{ pre.push(cur+1); return pre; },list); console.log(list); // [1, 2, 3, 4, 2, 3, 4, 5]fill()：用于用静态值填充数组的整个或一部分，用于需要创建一个填充特定值的数组时/** * value：要填充的值。 * start（可选）：填充开始的索引（默认是 0）。 * end（可选）：填充结束的索引（默认是数组的长度）。 */ // 创建一个长度为 5 的数组，全部填充为 0 let array1 = new Array(5).fill(0); console.log(array1); // 输出：[0, 0, 0, 0, 0] // 填充部分数组 let array2 = [1, 2, 3, 4, 5]; array2.fill(0, 1, 3); // 从索引 1 到 2 填充为 0 console.log(array2); // 输出：[1, 0, 0, 4, 5] // 使用非基础数据类型 let array3 = new Array(3).fill({name: 'Fitten'}); console.log(array3); // 输出：[ {name: 'Fitten'}, {name: 'Fitten'}, {name: 'Fitten'} ] // 如果修改其中一个对象，其他对象也会受到影响（可以改为使用map处理） array3[0].name = 'Code Assistant'; console.log(array3); // 输出：[ {name: 'Code Assistant'}, {name: 'Code Assistant'}, {name: 'Code Assistant'} ]filter()：创建一个新数组，包含所有通过测试的元素const numbers = [1, 2, 3, 4, 5]; const evens = numbers.filter(num => num % 2 === 0); console.log(evens); // [2, 4]find()：返回数组中满足提供的测试函数的第一个元素的值const numbers = [1, 2, 3, 4, 5]; const found = numbers.find(num => num > 3); console.log(found); // 4includes()：判断一个数组是否包含某个指定的值，如果包含，则返回 true，否则返回 falseconst fruits = ['apple', 'banana', 'mango']; // 检查数组中是否包含特定的元素 const hasBanana = fruits.includes('banana'); console.log(hasBanana); // true const hasOrange = fruits.includes('orange'); console.log(hasOrange); // falsesome()：测试数组中是否至少有一个元素通过了指定的测试const numbers = [1, 2, 3, 4]; const hasEven = numbers.some(num => num % 2 === 0); console.log(hasEven); // trueevery()：测试数组中的所有元素是否都通过了指定的测试const numbers = [2, 4, 6]; const allEven = numbers.every(num => num % 2 === 0); console.log(allEven); // true
文件导入（读取）// 组件 <Upload fileList={this.state.fileList} beforeUpload={this.fileBeforeUpload} accept=".xlsx">导入文件</Upload> // 上传组件中添加 beforeUpload 使用xlsx读取上传文件数据，写入数据库 fileBeforeUpload = file => { // xlsx 读取文件内容 const reader = new FileReader(); reader.onload = e => { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const rows = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); let newData = [] for (let i = 1; i < rows.length; i++) { newData.push({ id: this.state.tabData.length + 1, equipmentName: rows[i][0], manufacturer: rows[i][1], model: rows[i][2], mn: rows[i][3], status: rows[i][4], productionDate: rows[i][5], useYear: rows[i][6], }) } this.setState({ tabData: [...this.state.tabData, ...newData], fileList: [] }, () => { scriptUtil.showMessage("数据写入成功", 'success'); }); } catch (error) { scriptUtil.showMessage("数据写入失败，请检查文件格式或内容", 'error'); } }; reader.onerror = () => { console.error("文件读取失败"); scriptUtil.showMessage("文件读取失败，请检查文件是否正确", 'error'); }; reader.readAsArrayBuffer(file); return false; }文件导出一、二维数组格式数据导出：aoa_to_sheet[['姓名', '年龄', '城市'],['张三', 28, '北京'],['李四', 34, '上海'],['王五', 22, '广州']]const data = [ ['姓名', '年龄', '城市'], ['张三', 28, '北京'], ['李四', 32, '上海'], ['王五', 25, '广州'] ]; const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.aoa_to_sheet(data); XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); XLSX.writeFile(workbook, 'output.xlsx');二、对象数组格式数据导出：json_to_sheet[{ 姓名: '张三', 年龄: 28, 城市: '北京' },{ 姓名: '李四', 年龄: 34, 城市: '上海' },{ 姓名: '王五', 年龄: 22, 城市: '广州' }]const data = [ { 姓名: '张三', 年龄: 28, 城市: '北京' }, { 姓名: '李四', 年龄: 32, 城市: '上海' }, { 姓名: '王五', 年龄: 25, 城市: '广州' } ]; const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.json_to_sheet(data); XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); XLSX.writeFile(workbook, 'output.xlsx');三、table表格直接导出：table_to_sheet页面展示什么样导出就是什么样，注：勾选列、操作列也会被导出可以在写入sheet前调用deleteRow(deleteRowIndex);deleteCell(deleteColumnIndex)删除不需要的行和列// html原生table直接通过id导出 姓名 年龄 城市 张三 28 北京 李四 32 上海 王五 25 广州 // 导出方法 handleExport() { const table = document.getElementById('myTable'); const worksheet = XLSX.utils.table_to_sheet(table); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); XLSX.writeFile(workbook, 'output.xlsx'); }); // 封装后的表格（antd），通过class获取到表头与表格，创建一个新的table，将表头与表格合并后得到一个table再导出 /** * 导出表格到excel * @param {string} tableClass 表格的class名称 * @param {string} fileName 导出的文件名 * @param {number[]} deleteRowIndices 要删除的行索引，从0开始，如果为[]则不删除任何行，删除单行：[0]，删除多行：[0, 1, 2] * @param {number[]} deleteColumnIndices 要删除的列索引，从0开始，如果为[]则不删除任何列，删除单列：[0]，删除多列：[0, 1, 2] * */ exportTableToExcel = (tableClass, fileName, deleteRowIndices = [], deleteColumnIndices = []) => { const table = document.getElementsByClassName(tableClass); const headerHtml = table[0].outerHTML; const bodyHtml = table[1].outerHTML; const fullTable = document.createElement('table'); const thead = document.createElement('thead'); thead.innerHTML = headerHtml; fullTable.appendChild(thead); const tbody = document.createElement('tbody'); tbody.innerHTML = bodyHtml; fullTable.appendChild(tbody); // 删除指定行 deleteRowIndices.sort((a, b) => a - b); if (deleteRowIndices.length > 0) { // 从后往前删除，以避免因删除行导致索引变化 for (let i = deleteRowIndices.length - 1; i >= 0; i--) { const deleteRowIndex = deleteRowIndices[i]; if (deleteRowIndex >= 0 && deleteRowIndex < fullTable.rows.length) { fullTable.deleteRow(deleteRowIndex); } } } // 删除指定列 deleteColumnIndices.sort((a, b) => a - b); if (deleteColumnIndices.length > 0) { for (let i = 0; i < fullTable.rows.length; i++) { const cells = fullTable.rows[i].cells; // 从后往前删除列 for (let j = deleteColumnIndices.length - 1; j >= 0; j--) { const deleteColumnIndex = deleteColumnIndices[j]; if (deleteColumnIndex >= 0 && deleteColumnIndex < cells.length) { fullTable.rows[i].deleteCell(deleteColumnIndex); } } } } const workbook = XLSX.utils.table_to_book(fullTable, { sheet: "Sheet1" }); XLSX.writeFile(workbook, `${fileName}.xlsx`); }; // 调用实例 // 删除第2行和第3列 exportTableToExcel('myTableClass', 'myFileName', [1], [2]); // 只删除第2行 exportTableToExcel('myTableClass', 'myFileName', [1], []); // 只删除第3列 exportTableToExcel('myTableClass', 'myFileName', [], [2]); // 删除多行多列 exportTableToExcel('ant-table-fixed', 'table_to_sheet', [2, 1], [8, 0]);四、复杂表格结构导出分组表头或表格内有单元格合并等（使用table_to_sheet方式导出更方便）// 导出逻辑 exportToExcel = () => { let { rangePickerValue: dates, tabData, selsectValue, selectList } = this.state; const diff = dates[1].diff(dates[0], 'days'); let selectPreject = "项目名称"; // 报表第一行 let title = "报表数据起止时间：" + dates[0].format('YYYY-MM-DD') + "至" + dates[1].format('YYYY-MM-DD'); // 报表第二行 let sheetTitle = ["设备名称", "总电量", "", "", "", ""]; // 报表第三行 let subTitle = ["", "总", "尖", "峰", "平", "谷"]; // 报表第四行 let keys = []; // 存放日期 ["09-13","09-12",...] let colIdx = 6 // 表格列索引值 let merge = [ // 表格合并规则数组，一个对象代表一个合并规则，s:开始行和列（从0开始），e:结束行和列，r:行索引值，c:列索引值 { s: { r: 0, c: 0 }, e: { r: 0, c: 15 } }, { s: { r: 1, c: 0 }, e: { r: 1, c: 15 } }, { s: { r: 2, c: 0 }, e: { r: 3, c: 0 } }, { s: { r: 2, c: 1 }, e: { r: 2, c: 5 } } ] // "_" lodash库 _.range(diff + 1).forEach((day) => { let date = dates[1].clone().subtract(day, 'days').format('MM-DD'); sheetTitle.push(date, "", "", "", ""); subTitle.push("总", "尖", "峰", "平", "谷"); merge.push({ s: { r: 2, c: colIdx }, e: { r: 2, c: colIdx + 4 } }) // 动态添加单元格合并规则 colIdx += 5 keys.push(date); }) let sheetData = tabData.reduce((acc, cur) => { let sumArr = ["sum", "spike", "peak", "flat", "valley"].map((type) => getValue(cur[type]) ); let rowData = _.reduce(keys, (subPre, key) => { ["sum", "spike", "peak", "flat", "valley"].forEach((type) => { const vItem = getValue(cur[type + key]); subPre.push(vItem); }); return subPre; },[cur.equipmentName, ...sumArr]); acc.push(rowData); return acc; }, [[selectPreject], [title], sheetTitle, subTitle] ) let worksheet = XLSX.utils.aoa_to_sheet(sheetData); let workbook = XLSX.utils.book_new(); worksheet["!merges"] = merge; // 添加单元格合并规则 XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); XLSX.writeFile(workbook, selectPreject + '_报表导出.xlsx'); }
使用技术：vue3.2+openlayers8.2.0实现功能：​ 点击绘制图形按钮，从缓存中获取图形绘制到地图上（实际项目中可替换成请求接口获取图形数据绘制到地图）​ 点击多边形、圆形、三角形...后可在地图上绘制对应图形（实际项目中可替换成下拉框+数字input框），绘制完的图形默认打开编辑绘制​ 点击清除绘制按钮，会将地图矢量绘制层矢量数据源全部清除​ 点击关闭绘制按钮，如果当前处于编辑状态，则关闭编辑状态​ 点击编辑绘制按钮，可以选择当前地图上显示的图形，然后编辑图形对象 <el-button class="btn-search" @click="drawSketch" type="success">绘制图形</el-button> <el-button class="btn-search" @click="handleAddInteraction('Polygon')" type="primary">多边形</el-button> <el-button class="btn-search" @click="handleAddInteraction('Circle')" type="primary">圆形</el-button> <el-button class="btn-search" @click="handleAddInteraction('LineString')" type="primary">线条</el-button> <el-button class="btn-search" @click="handleAddInteraction('Circle', 3)" type="primary">三角形</el-button> <el-button class="btn-search" @click="handleAddInteraction('Circle', 6)" type="primary">六边形</el-button> <el-button class="btn-search" @click="clearDraw" type="danger">清除绘制</el-button> <el-button class="btn-search" @click="closeDraw" v-show="isOpenModify == 1" type="danger">关闭绘制</el-button> <el-button class="btn-search" @click="openDraw" v-show="isOpenModify == 0" type="danger">编辑绘制</el-button>