整理 | 一一
出品 | AI科技大本营(ID:rgznai100)
如何挑战百万年薪的人工智能!
https://edu.csdn.net/topic/ai30?utm_source=csdn_bw
无疑,如今频上热搜 AI 写词作曲给人们带来了意想不到的新鲜感。
去年的中国好声音上,清华大学博士生宿涵直接将 AI 写的歌改编后唱了出来,引起网友一片赞叹。然而,近期国内一所高校公布了研究“AI+音乐”领域博士生的招收指标,引发了网友们不同意见的讨论。
3 月 1 日,中央音乐学院官网悄然发布了将在 2019 年招收全国首个“音乐AI”方向博士生的消息,该研究方向的全称是“音乐人工智能与音乐信息科技”。
链接:http://www.ccom.edu.cn/xwyhd/xsjd/2019s/201903/t20190301_53856.html
以往我们听到更多是“AI音乐”这样的名词,但不知道称“音乐AI”是不是为了契合中央音乐学院本身以音乐为主体的风格,AI 放在音乐之后看上去似乎更强调其作为一种探索音乐的工具属性。
不管怎样,中央音乐学院为国内高校招收这一方向的博士生应该是开创了先河。
中央音乐学院表示,他们将着力培养音乐与理工科交叉融合的复合型拔尖创新人才,助力音乐与科技的融合。他们还给出了招生条件,须是计算机、智能和电子信息类,显然懂技术,有科研能力是必须的。当然,要研究的是音乐,没有音乐艺术细胞也是不行的,不会乐器也没关系,最低要求是要会唱歌。
套用一句俗语就是:搞这一行的,讲究一个吹拉弹唱,代码写的溜。
这一点从其面试要求和建议阅读书目里也有所体现。
既然“音乐AI”要艺术和科技兼修,那教你的导师自然也得两样都精通,为了解决这一问题,中央音乐学院采取的是双导师培养制(音乐导师+科技导师),他们还公布了三位导师的信息:
俞峰,中央音乐学院院长、教授、博导
孙茂松,清华大学教授、博导,清华大学人工智能研究院常务副院长,其主要研究领域为自然语言处理、人工智能、机器学习和计算教育学。2017 年领衔研制出“九歌”人工智能古诗写作系统。
吴玺宏,北京大学教授、博导,教育部新世纪优秀人才,致力于机器听觉计算理论、语音信息处理、自然语言理解以及音乐智能等领域的研究。在智能音乐创作、编配领域颇有成就。
高校想要申请一个博士点,从申请到最终确认招收学生需要一定准备时间。根据 AI科技大本营了解,去年 5 月,中央音乐学院与美国印第安纳大学信息计算与工程学院签署“音乐人工智能”实验室合作协议,同年 9 月才对外公布了这一消息。
而对于两所大学之间的“音乐人工智能”实验室合作协议,中央美术学院当时在新闻稿中称合作意义重大,为中央音乐学院更深入参与、引导音乐人工智能领域的发展呈现新的可能,也是中央音乐学院在产学研用和社会服务方面的重要战略布局。
相比国内高校,如清华大学、北京大学在“AI+音乐”领域提前进行了深入研究,但目前也没有设置相关博士点。显然,中央音乐学院在硬件配置上的布局更具前瞻性。
设置“音乐AI”博士点有无必要?
中央音乐学院在发布这一消息后,引发了网友们对设置“AI+音乐”博士点有无必要的讨论。
从知乎的评论来看,大部分网友持支持观点,但他们指出了该领域研究面临的一些挑战。
除了算法模型训练等技术问题,另一更重要的挑战是缺乏音乐数据,知乎网友@吴俣提到,相比语言模型较易获取的大量文本数据,音乐数据,尤其一些比较有特色音乐资源往往很难获得,因此无法改善训练质量,这给研究工作带来了极大挑战。所以目前更重要的是构建音乐库数据。
还有一点是像 AI 作曲这种艺术类作品,很难有量化标准去评判质量好坏。
(https://www.zhihu.com/question/314142299/answer/612575470)
另有网友@张晖表达了悲观的看法:尽管“AI+音乐”研究很流行,但音乐界和 AI 界的研究人员不能有效沟通,这有点像目前学界做自然语言理解的研究者和语言学的研究者存有的隔阂,跨界研究总是会面临各种可能意想不到的状况,这些都是科研时所要面临的挑战。
此外,基于 AI 技术发展的不成熟,也有人质疑该研究方向只是打着 AI 旗号的噱头研究,设置博士点毫无必要。
哪些学府在招收“AI+音乐”方向博士生?
虽然在国内,中央音乐学院可能开设了第一个“AI+音乐”方向的博士点,不过在国际上,对该方向专门设置博士点及专门的研究实验室的高校并不少。
斯坦福大学音乐学院在博士方向就有计算机音乐理论与声学原理博士(Ph.D. in Computer-Based Music Theory and Acoustics),在 CCRMA 实验室进行学术研究。硕士则有音乐与科学技术硕士(M.A. in Music, Science, and Technology (MST))研究方向。
美国卡耐基梅隆大学的 Computer Music Group 是一个专门针对音乐做 AI 和机器学习等技术研究的机构。
此外,还有英国的爱丁堡大学有属于音乐学院的 Music Technology 学位,而 MIT、日本的京都大学等高校也有实验室用 AI 做音乐分析。
“AI+音乐”的研究目前已有从小众研究转向热门研究的趋势,尤其在国际计算机音乐会议(ICMC)、音频与音乐技术会议(ISMIR)等专门的学术会议上,近年来已有领域专家进行集中探讨。
“AI+音乐”前景如何?
目前,像国内有清华大学孵化的相关创业团队,科技巨头如 Google、微软小冰团队,以及平安科技深度学习团队也有专门的研究项目。但总体而言,无论是科研还是商业化探索,“AI+音乐”产业发展还在酝酿初期。
不过一旦技术研究成熟,它带来的经济价值将不可估量。试想一下,AI 辅助人类作曲家提供更多灵感,创造更多变化多端的乐曲,同时形成的大量音乐素材库,也能解决解决平台方的曲目内容产出,带动版权管理的新模式。
另外从“AI+”领域发展的角度看,既然设置及“音乐AI”博士点,是为了让高校结合业界需求进行专注研究,那未来在其它的跨界艺术研究领域,是不是也会设置个像“美术AI”这样的博士点?
人工智能的现状及今后发展趋势如何?
https://edu.csdn.net/topic/ai30?utm_source=csdn_bw
(本文为AI科技大本营整理文章,转载请微信联系 1092722531)
群招募
扫码添加小助手微信,回复:公司+研究方向(学校+研究方向),邀你加入技术交流群。技术群审核较严,敬请谅解。
推荐阅读:
❤点击“阅读原文”,查看历史精彩文章。
- Hey, 我是 沉浸式趣谈
- 本文首发于【沉浸式趣谈】,我的个人博客 yaolifeng.com 也同步更新。
- 转载请在文章开头注明出处和版权信息。
- 如果本文对您有所帮助,请 点赞、评论、转发,支持一下,谢谢!
- 该平台创作会佛系一点,更多文章在我的个人博客上更新,欢迎访问我的个人博客。
做前端的经常碰到这种需求:用户哗啦一下传个 Excel 上来,你得在网页上给它弄个像模像样的预览?有时候还要编辑,还挺折腾人的。
我踩了不少坑,也试了市面上挺多库,今天就聊聊几个比较主流的选择,特别是最后那个,我个人是强推!
在线预览 Demo
第一个选手:老牌劲旅 xlsx
提起处理 Excel,xlsx 这库估计是绕不过去的。GitHub 上 35k 的 star,简直是元老级别的存在了。
安装?老规矩:
bash代码解读复制代码npm install xlsx
用起来嘛,也挺直接。看段代码感受下:
vue代码解读复制代码import { ref } from 'vue'; import * as XLSX from 'xlsx'; // 读取Excel文件 const readExcel = event => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = e => { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); // 获取第一个工作表 const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; // 转换为JSON const jsonData = XLSX.utils.sheet_to_json(firstSheet); console.log('喏,JSON 数据到手:', jsonData); }; reader.readAsArrayBuffer(file); };
上面就是读个文件,拿到第一个 sheet 转成 JSON。很简单粗暴,对吧?
搞个带文件选择器的预览 Demo 也不复杂:
vue代码解读复制代码
import { ref } from 'vue'; import * as XLSX from 'xlsx'; const data = ref([]); const columns = ref([]); const handleFile = (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { // 修改变量名避免与外部响应式变量冲突 const fileData = new Uint8Array(event.target?.result as ArrayBuffer); const workbook = XLSX.read(fileData, { type: 'array' }); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; // 使用 header: 1 来获取原始数组格式 const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); if (jsonData.length > 0) { // 第一行作为列标题 columns.value = jsonData[0] as string[]; // 其余行作为数据 data.value = jsonData.slice(1); console.log('数据已加载:', { 列数: columns.value.length, 行数: data.value.length }); } } catch (error) { console.error('Excel解析失败:', error); alert('文件解析失败,请检查文件格式'); } }; reader.readAsArrayBuffer(file); };
{{ column.title }} {{ row[column.title] }}
xlsx 这家伙吧,优点很明显: 轻、快!核心库体积不大,解析速度嗖嗖的,兼容性也不错,老格式新格式基本都能吃。社区也活跃,遇到问题谷歌一下大多有解。
但缺点也得说说: 它的 API 设计感觉有点…嗯…老派?或者说比较底层,不太直观。想拿到的数据结构,经常得自己再加工一道(就像上面 Demo 里那样)。而且,如果你想连着样式一起搞,比如单元格颜色、字体啥的,那 xlsx 就有点力不从心了,样式处理能力基本等于没有。
我个人觉得,如果你的需求就是简单读写数据,不关心样式,那 xlsx 绝对够用,效率杠杠的。但凡需求复杂一点,比如要高度还原 Excel 样式,或者处理复杂公式,那用它就有点“小马拉大车”的感觉了。
第二个选手:重量级嘉宾 Handsontable
聊完基础款,我们来看个重量级的:Handsontable。这家伙最大的卖点,就是直接给你一个长得、用起来都跟 Excel 贼像的在线表格!
安装要多装个 Vue 的适配包:
bash 代码解读复制代码npm install handsontable
npm install @handsontable/vue3 # Vue3 专用包
别忘了还有 CSS:
javascript 代码解读复制代码import 'handsontable/dist/handsontable.full.css';
基础用法,它是在一个 DOM 容器里初始化:
vue代码解读复制代码import { onMounted } from 'vue'; import Handsontable from 'handsontable'; import 'handsontable/dist/handsontable.full.css'; onMounted(() => { // 初始化表格 const container = document.getElementById('excel-preview'); const hot = new Handsontable(container, { data: [ ['姓名', '年龄', '城市'], ['张三', 28, '北京'], ['李四', 32, '上海'], ['王五', 25, '广州'], ], rowHeaders: true, colHeaders: true, contextMenu: true, licenseKey: 'non-commercial-and-evaluation', // 注意:商用要钱!这很关键! }); });
搞个可编辑的 Demo 看看?这才是它的强项:
vue代码解读复制代码
import { ref, computed, onMounted } from 'vue'; import { HotTable } from '@handsontable/vue3'; import { registerAllModules } from 'handsontable/registry'; import 'handsontable/dist/handsontable.full.css'; import * as XLSX from 'xlsx'; import Handsontable from 'handsontable'; // 注册所有模块 registerAllModules(); // 表头定义 const headers = ['ID', '姓名', '部门', '职位', '入职日期', '薪资', '绩效评分', '状态']; // 嵌套表头 const nestedHeaders = [['员工基本信息', '', '', '', '员工绩效数据', '', '', ''], headers]; // 列宽设置 const colWidths = [60, 100, 100, 120, 120, 100, 100, 120]; // 列定义 const columnDefinitions = [ { data: 'id', type: 'numeric', readOnly: true }, { data: 'name', type: 'text' }, { data: 'department', type: 'dropdown', source: ['销售', '市场', '技术', '人事', '财务'], }, { data: 'position', type: 'text' }, { data: 'joinDate', type: 'date', dateFormat: 'YYYY-MM-DD', correctFormat: true, }, { data: 'salary', type: 'numeric', numericFormat: { pattern: '¥ 0,0.00', culture: 'zh-CN', }, }, { data: 'performance', type: 'numeric', numericFormat: { pattern: '0.0', }, }, { data: 'status', type: 'dropdown', source: ['在职', '离职', '休假'], }, ]; // 右键菜单选项 const contextMenuOptions = { items: { row_above: { name: '上方插入行' }, row_below: { name: '下方插入行' }, remove_row: { name: '删除行' }, separator1: Handsontable.plugins.ContextMenu.SEPARATOR, copy: { name: '复制' }, cut: { name: '剪切' }, separator2: Handsontable.plugins.ContextMenu.SEPARATOR, columns_resize: { name: '调整列宽' }, alignment: { name: '对齐' }, }, }; // 初始数据 const initialData = [ { id: 1, name: '张三', department: '销售', position: '销售经理', joinDate: '2022-01-15', salary: 15000, performance: 4.5, status: '在职', }, { id: 2, name: '李四', department: '技术', position: '高级开发', joinDate: '2021-05-20', salary: 18000, performance: 4.7, status: '在职', }, { id: 3, name: '王五', department: '市场', position: '市场专员', joinDate: '2022-03-10', salary: 12000, performance: 3.8, status: '在职', }, { id: 4, name: '赵六', department: '技术', position: '开发工程师', joinDate: '2020-11-05', salary: 16500, performance: 4.2, status: '在职', }, { id: 5, name: '钱七', department: '销售', position: '销售代表', joinDate: '2022-07-18', salary: 10000, performance: 3.5, status: '休假', }, { id: 6, name: '孙八', department: '市场', position: '市场总监', joinDate: '2019-02-28', salary: 25000, performance: 4.8, status: '在职', }, { id: 7, name: '周九', department: '技术', position: '测试工程师', joinDate: '2021-09-15', salary: 14000, performance: 4.0, status: '在职', }, { id: 8, name: '吴十', department: '销售', position: '销售代表', joinDate: '2022-04-01', salary: 11000, performance: 3.6, status: '离职', }, ]; // 表格引用 const hotTableRef = ref(null); const data = ref([...initialData]); const selectedDepartment = ref('all'); // 过滤后的数据 const filteredData = computed(() => { if (selectedDepartment.value === 'all') { return data.value; } return data.value.filter(item => item.department === selectedDepartment.value); }); // 数据统计 const totalEmployees = computed(() => data.value.filter(emp => emp.status === '在职' || emp.status === '休假').length); const averagePerformance = computed(() => { const activeEmployees = data.value.filter(emp => emp.status === '在职'); if (activeEmployees.length === 0) return 0; const sum = activeEmployees.reduce((acc, emp) => acc + emp.performance, 0); return (sum / activeEmployees.length).toFixed(1); }); const totalSalary = computed(() => { const activeEmployees = data.value.filter(emp => emp.status === '在职' || emp.status === '休假'); const sum = activeEmployees.reduce((acc, emp) => acc + emp.salary, 0); return `¥ ${sum.toLocaleString('zh-CN')}`; }); // 单元格渲染器 - 条件格式 const cellsRenderer = (row, col, prop) => { const cellProperties = {}; // 绩效评分条件格式 if (prop === 'performance') { const value = filteredData.value[row]?.performance; if (value >= 4.5) { cellProperties.className = 'bg-green'; } else if (value >= 4.0) { cellProperties.className = 'bg-light-green'; } else if (value < 3.5) { cellProperties.className = 'bg-red'; } } // 状态条件格式 if (prop === 'status') { const status = filteredData.value[row]?.status; if (status === '在职') { cellProperties.className = 'status-active'; } else if (status === '离职') { cellProperties.className = 'status-inactive'; } else if (status === '休假') { cellProperties.className = 'status-vacation'; } } return cellProperties; }; // 数据验证 const beforeChangeHandler = (changes, source) => { if (source === 'edit') { for (let i = 0; i < changes.length; i++) { const [row, prop, oldValue, newValue] = changes[i]; // 薪资验证:不能小于0 if (prop === 'salary' && newValue < 0) { changes[i][3] = oldValue; } // 绩效验证:范围1-5 if (prop === 'performance') { if (newValue < 1) changes[i][3] = 1; if (newValue > 5) changes[i][3] = 5; } } } return true; }; // 在数据更改后的处理 const afterChangeHandler = (changes, source) => { if (!changes) return; setTimeout(() => { if (hotTableRef.value?.hotInstance) { hotTableRef.value.hotInstance.render(); } }, 0); }; // 应用过滤器 const applyFilters = () => { if (hotTableRef.value?.hotInstance) { hotTableRef.value.hotInstance.render(); } }; // 添加新行 const addNewRow = () => { const newId = Math.max(...data.value.map(item => item.id), 0) + 1; data.value.push({ id: newId, name: '', department: '', position: '', joinDate: new Date().toISOString().split('T')[0], salary: 0, performance: 3.0, status: '在职', }); if (hotTableRef.value?.hotInstance) { setTimeout(() => { hotTableRef.value.hotInstance.render(); }, 0); } }; // 保存数据 const saveData = () => { // 这里可以添加API保存逻辑 alert('数据已保存'); }; // 导出为Excel const exportToExcel = () => { const currentData = data.value; const ws = XLSX.utils.json_to_sheet(currentData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '员工数据'); XLSX.writeFile(wb, '员工数据报表.xlsx'); }; // 确保组件挂载后正确渲染 onMounted(() => { setTimeout(() => { if (hotTableRef.value?.hotInstance) { hotTableRef.value.hotInstance.render(); } }, 100); }); .handsontable-container { padding: 20px; font-family: Arial, sans-serif; } .toolbar { display: flex; justify-content: space-between; margin-bottom: 20px; align-items: center; } .filter-section { display: flex; align-items: center; gap: 10px; } .toolbar-actions { display: flex; gap: 10px; } button { padding: 8px 16px; background-color: #4285f4; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; } button:hover { background-color: #3367d6; } select { padding: 6px; border-radius: 4px; border: 1px solid #ccc; } .summary-section { margin-top: 20px; padding: 15px; background-color: #f9f9f9; border-radius: 6px; } .summary-items { display: flex; gap: 30px; margin-top: 10px; } .summary-item { padding: 10px; background-color: white; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } /* 条件格式样式 */ .bg-green { background-color: rgba(76, 175, 80, 0.3) !important; } .bg-light-green { background-color: rgba(139, 195, 74, 0.2) !important; } .bg-red { background-color: rgba(244, 67, 54, 0.2) !important; } .status-active { font-weight: bold; color: #2e7d32; } .status-inactive { font-weight: bold; color: #d32f2f; } .status-vacation { font-weight: bold; color: #f57c00; }Handsontable 数据分析工具
ref="hotTableRef" :data="filteredData" :colHeaders="headers" :rowHeaders="true" :width="'100%'" :height="500" :contextMenu="contextMenuOptions" :columns="columnDefinitions" :nestedHeaders="nestedHeaders" :manualColumnResize="true" :manualRowResize="true" :colWidths="colWidths" :beforeChange="beforeChangeHandler" :afterChange="afterChangeHandler" :cells="cellsRenderer" licenseKey="non-commercial-and-evaluation" > 数据统计
员工总数: {{ totalEmployees }}平均绩效分: {{ averagePerformance }}总薪资支出: {{ totalSalary }}
Handsontable 的牛逼之处: 界面无敌!
用户体验几乎无缝对接 Excel,什么排序、筛选、合并单元格、公式计算、右键菜单、拖拽调整行列,花里胡哨的功能一大堆。
定制性也强,事件钩子多得很。官方还贴心地提供了 Vue、React 这些框架的集成包。
但(总有个但是,对吧?):
贵! 商用许可不便宜,对不少项目来说是个门槛。虽然有非商用许可,但你懂的。
重! 功能全的代价就是体积大,加载可能慢一丢丢,尤其对性能敏感的页面。
大数据量有压力: 行列一多,性能可能会有点吃紧。
学习曲线: 配置项多如牛毛,想玩溜需要花点时间看文档。
我个人感觉,Handsontable 就像是你去了一家装修豪华、菜品精致的高档餐厅,体验一级棒,但结账时钱包会疼。
如果项目预算充足,而且用户强烈要求“就要 Excel 那样的体验”,那它确实是王炸。
压轴出场:我的心头好 ExcelJS
前面说了两个,一个轻快但简陋,一个豪华但贵重。
那有没有折中点的,功能强又免费的?
ExcelJS 登场!这家伙给我的感觉就是:现代化、全能型选手,而且 API 设计得相当舒服。
老规矩,安装
bash代码解读复制代码npm install exceljs
基本用法,注意它用了 async/await,很现代:
vue代码解读复制代码import { ref } from 'vue'; import ExcelJS from 'exceljs'; const readExcel = async event => { const file = event.target.files[0]; if (!file) return; // 最好加个 try...catch try { const workbook = new ExcelJS.Workbook(); const arrayBuffer = await file.arrayBuffer(); // 直接读 ArrayBuffer,省事儿 await workbook.xlsx.load(arrayBuffer); const worksheet = workbook.getWorksheet(1); // 获取第一个 worksheet const data = []; worksheet.eachRow((row, rowNumber) => { const rowData = []; row.eachCell((cell, colNumber) => { rowData.push(cell.value); }); // 它的 API 遍历起来就挺顺手 data.push(rowData); }); console.log(data); return data; // 返回解析好的数据 } catch (error) { console.error('用 ExcelJS 解析失败了,检查下文件?', error); alert('文件好像有点问题,解析不了哦'); } };
来个带劲的 Demo:把 Excel 样式也给你扒下来!
vue代码解读复制代码
import ExcelJS from 'exceljs'; // 高级数据类型 interface AdvancedData { id: number; name: string; department: string; salary: number; joinDate: Date; performance: number; } // 生成示例数据 const generateData = () => { const data: AdvancedData[] = []; for (let i = 1; i <= 5; i++) { data.push({ id: i, name: `员工${i}`, department: ['技术部', '市场部', '财务部'][i % 3], salary: 10000 + i * 1000, joinDate: new Date(2020 + i, i % 12, i), performance: Math.random() * 100, }); } return data; }; const exportAdvancedExcel = async () => { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('员工报表'); // 设置文档属性 workbook.creator = '企业管理系统'; workbook.lastModifiedBy = '管理员'; workbook.created = new Date(); // 设置页面布局 worksheet.pageSetup = { orientation: 'landscape', margins: { left: 0.7, right: 0.7, top: 0.75, bottom: 0.75 }, }; // 创建自定义样式 const headerStyle = { font: { bold: true, color: { argb: 'FFFFFFFF' } }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4F81BD' } }, border: { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' }, }, alignment: { vertical: 'middle', horizontal: 'center' }, }; const moneyFormat = '"¥"#,##0.00'; const dateFormat = 'yyyy-mm-dd'; const percentFormat = '0.00%'; // 合并标题行 worksheet.mergeCells('A1:F1'); const titleCell = worksheet.getCell('A1'); titleCell.value = '2023年度员工数据报表'; titleCell.style = { font: { size: 18, bold: true, color: { argb: 'FF2E75B5' } }, alignment: { vertical: 'middle', horizontal: 'center' }, }; // 设置列定义 worksheet.columns = [ { header: '工号', key: 'id', width: 10 }, { header: '姓名', key: 'name', width: 15 }, { header: '部门', key: 'department', width: 15 }, { header: '薪资', key: 'salary', width: 15, style: { numFmt: moneyFormat }, }, { header: '入职日期', key: 'joinDate', width: 15, style: { numFmt: dateFormat }, }, { header: '绩效', key: 'performance', width: 15, style: { numFmt: percentFormat }, }, ]; // 应用表头样式 worksheet.getRow(2).eachCell(cell => { cell.style = headerStyle; }); // 添加数据 const data = generateData(); worksheet.addRows(data); // 添加公式行 const totalRow = worksheet.addRow({ id: '总计', salary: { formula: 'SUM(D3:D7)' }, performance: { formula: 'AVERAGE(F3:F7)' }, }); // 设置总计行样式 totalRow.eachCell(cell => { cell.style = { font: { bold: true }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFCE4D6' } }, }; }); // 添加条件格式 worksheet.addConditionalFormatting({ ref: 'F3:F7', rules: [ { type: 'cellIs', operator: 'greaterThan', formulae: [0.8], style: { fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFC6EFCE' } } }, }, ], }); // 生成Blob并下载 const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); // 使用原生API下载 const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = '员工报表.xlsx'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); };
为啥我偏爱 ExcelJS?
API 友好: Promise 风格,链式调用,写起来舒服,代码也更易读。感觉就是为现代 JS 开发设计的。
功能全面: 不仅仅是读写数据,样式、公式、合并单元格、图片、表单控件… 它支持的 Excel 特性相当多。特别是读取和修改样式,这对于需要“还原”Excel 样貌的场景太重要了!
免费开源! 这点太香了,没有商业使用的后顾之忧。
文档清晰: 官方文档写得挺明白,示例也足。
当然,没啥是完美的:
体积比 xlsx 大点: 但功能也强得多嘛,可以接受。
复杂公式支持可能有限: 极其复杂的嵌套公式或者宏,可能还是搞不定(不过大部分场景够用了)。
超大文件性能: 几十上百兆的 Excel,解析起来可能会慢,或者内存占用高点(老实说,哪个库处理这种文件不头疼呢)。
我之前用 xlsx 时,老是要自己写一堆转换逻辑,数据结构处理起来烦得很。换了 ExcelJS 后,感觉世界清净了不少。尤其是它能把单元格的背景色、字体、边框这些信息都读出来,这对做预览太有用了!
实战中怎么选?或者…全都要?
其实吧,这三个库也不是非得“你死我活”。在真实项目中,完全可以根据情况搭配使用:
简单快速的导入导出: 用户上传个模板,或者导出一份简单数据,用 xlsx 就行,轻快好省。
需要精确保留样式或复杂解析: 用户传了个带格式的报表,你想尽可能还原预览,那 ExcelJS 就是主力。
需要在线编辑、强交互: 如果你做的不是预览,而是个在线的类 Excel 编辑器,那砸钱上 Handsontable 可能是最接近目标的(如果预算允许的话)。
我甚至见过有项目是这样搞的:先用 xlsx 快速读取基本数据和 Sheet 名称做个“秒开”预览,然后后台或者异步再用 ExcelJS 做详细的、带样式的解析。
这样既快,又能保证最终效果。
下面这个(伪)代码片段,大概是这个思路:
vue代码解读复制代码
import { ref } from 'vue'; import * as XLSX from 'xlsx'; // 用于快速预览 & 导出 import ExcelJS from 'exceljs'; // 用于详细解析 import { HotTable } from '@handsontable/vue3'; // 用于展示 & 编辑 import 'handsontable/dist/handsontable.full.css'; const data = ref([]); const isLoading = ref(false); const sheetNames = ref([]); const activeSheet = ref(0); const hotTableRef = ref(null); // 快速预览(可选,或者直接用 detailedParse) const quickPreview = file => { isLoading.value = true; const reader = new FileReader(); reader.onload = e => { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); sheetNames.value = workbook.SheetNames; const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 }); data.value = jsonData; activeSheet.value = 0; } catch (error) { console.error('预览失败:', error); alert('文件预览失败'); } finally { isLoading.value = false; } }; reader.readAsArrayBuffer(file); }; // 使用ExcelJS详细解析 const detailedParse = async file => { isLoading.value = true; try { const workbook = new ExcelJS.Workbook(); const arrayBuffer = await file.arrayBuffer(); await workbook.xlsx.load(arrayBuffer); // 也许这里还可以把 ExcelJS 解析到的样式信息存起来,以后可能用得到 // 比如,导出时尝试用 ExcelJS 写回样式?那就更高级了 const names = workbook.worksheets.map(sheet => sheet.name); sheetNames.value = names; // 解析第一个 sheet parseWorksheet(workbook.worksheets[0]); activeSheet.value = 0; } catch (error) { console.error('解析失败:', error); alert('文件解析失败'); } finally { isLoading.value = false; } }; // 解析某个 worksheet 并更新 Handsontable 数据 const parseWorksheet = worksheet => { const sheetData = []; worksheet.eachRow((row, rowNumber) => { const rowData = []; row.eachCell((cell, colNumber) => { let value = cell.value; // 处理日期等特殊类型 if (value instanceof Date) { value = value.toLocaleDateString(); } rowData.push(value); }); sheetData.push(rowData); }); // 这里的 data 结构要适配 Handsontable,通常是二维数组 data.value = sheetData; }; // 切换 Sheet (需要重新调用 parseWorksheet) const handleSheetChange = async index => { activeSheet.value = index; // 重新加载并解析对应 Sheet 的数据... 这需要保存 workbook 实例 // 或者在 detailedParse 时就把所有 sheet 数据都解析缓存起来?看内存消耗 }; // 导出 (简单起见,用 xlsx 快速导出当前 Handsontable 的数据) const exportToExcel = () => { const ws = XLSX.utils.aoa_to_sheet(data.value); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Sheet1'); XLSX.writeFile(wb, '导出数据.xlsx'); // 如果想导出带样式的,那得用 ExcelJS 来写,会复杂不少 }; .excel-viewer { margin: 20px; } .controls { margin-bottom: 15px; } .sheet-tabs { display: flex; margin-bottom: 10px; } .sheet-tabs button { padding: 5px 10px; margin-right: 5px; border: 1px solid #ccc; background: #f5f5f5; cursor: pointer; } .sheet-tabs button.active { background: #e0e0e0; border-bottom: 2px solid #1890ff; }detailedParse(e.target.files[0])" accept=".xlsx,.xls" />加载中...v-if="data.length > 0" ref="hotTableRef" :data="data" :rowHeaders="true" :colHeaders="true" :width="'100%'" :height="400" licenseKey="non-commercial-and-evaluation" >
总结一下我的个人看法:
折腾下来,这几个库真是各有千秋:
xlsx (SheetJS): 老司机,适合追求极致性能和体积的简单场景。代码写得少,跑得快,但不怎么讲究“内饰”(样式)。
Handsontable: 豪华座驾,提供近乎完美的 Excel 编辑体验。功能强大没得说,但得看你口袋里的银子够不够。
ExcelJS: 可靠的全能伙伴。API 现代,功能均衡,对样式支持好,关键还免费!能帮你解决绝大多数问题。
说真的,没有银弹。选哪个,最终还是看你的具体需求和项目限制。
但如果非要我推荐一个,我绝对站 ExcelJS。
在功能、易用性和成本(免费!)之间,它平衡得太好了。
对于大部分需要精细处理 Excel 文件(尤其是带样式预览)的场景,它就是那个最香的选择!
好了,就叨叨这么多,希望能帮到你!赶紧去试试吧!
评论记录:
回复评论: