Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
下图是一个产品开发中非常常见的大屏展示界面示例。 通过 Vue 提供的 Vuex ,上方三个仪表板以及下方的表格组件共享同一个数据源,已经实现了数据改变后同步响应更新。
“很棒的大屏展示功能,能支持 Excel 数据的导入导出吗,表格数据可以实时编辑更新吗?”
如果你已经开发软件很长时间,可能不止一次地从最终客户或者产品经理那里听到过这个灵魂拷问。对于非技术人群来说,觉得要求 Excel 导入 /导出 /展示是一个非常正常且容易实现的需求。
但实际上,这个问题常常让前端开发人员感到恐惧。处理 Excel 文件需要大量工作。 这个问题通过前端表格可以变得简单,将电子表格嵌入 Web 应用程序。同时和其他的组件进行交互。 这篇博客将研究如何使用现有的这个大屏展示 Vue 应用作为基础,使用前端电子表格对其进行增强。
本文假定你已经了解 HTML 、CSS 和 JavaScript 。以及 Vue 的基础应用。如果你有使用过 Vuex ,当然会更容易理解,如果还没有,也不用担心。VueX 在这个项目中的应用很简单。
关于 VueX ,可以在 Vue 官网了解更多信息
本文将分为下面的几个部分
包含 Vuex 的原始应用 如上图看到的,将要使用的 Vue 应用程序是一个简单的大屏展示界面,带有几个汇总信息仪表板和一个数据表。
可以通过下面的附件获取这个 Vue 应用项目代码,然后运行“npm install”以及 “npm run serve”即可启动应用 。 附件下载地址: https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MjI1NzA1fDNkMDNjNjQ2fDE2NjAxMTUxMjF8NjI2NzZ8OTk3MTg%3D
原始的 Vue 应用代码结构如下:
Vuex store 代码如下,初始状态只有一个设置为 recentSales 的值,表示近期销售记录 :
const store = new Vuex.Store({
state: {
recentSales
}
});
通过 recentSales 这一个数据,如何生成三个统计表和一个表格?打开 Dashboard.vue 组件。在其中,可以看到基于 Vuex 存储中的数据生成了几个计算属性:
<template>
<div style="background-color: #ddd">
<NavBar title="销售仪表板"/>
<div class="container">
<div class="row">
<TotalSales :total="totalSales"/>
<SalesByCountry :salesData="countrySales"/>
<SalesByPerson :salesData="personSales"/>
<SalesTableBySpreadjs :tableData="salesTableData"/>
<SalesTable :tableData="salesTableData"/>
</div>
</div>
</div>
</template>
<script>
import NavBar from "./NavBar";
import TotalSales from "./TotalSales";
import SalesByCountry from "./SalesByCountry";
import SalesByPerson from "./SalesByPerson";
import SalesTable from "./SalesTable";
import SalesTableBySpreadjs from "./SalesTableBySpreadjs";
import { groupBySum } from "../util/util";
export default {
components: { NavBar, SalesByCountry, SalesByPerson, SalesTable, TotalSales ,SalesTableBySpreadjs},
computed: {
totalSales() {
const total = this.$store.state.recentSales.reduce(
(acc, sale) => (acc += sale.value),
0
);
return parseInt(total);
},
countrySales() {
const items = this.$store.state.recentSales;
const groups = groupBySum(items, "country", "value");
return groups;
},
personSales() {
const items = this.$store.state.recentSales;
const groups = groupBySum(items, "soldBy", "value");
return groups;
},
salesTableData() {
return this.$store.state.recentSales;
}
}
};
</script>
因此 recentSales 这个单个数据集目前能为这个大屏展示的几个仪表板和表格提供一致数据。由于数据位于 Vuex store 中,那么如果数据更新,所有仪表板面板都会自动更新。 当我们用可以编辑的电子表格替换现有的表格来进行编辑时,这种特性将派上用场。
我们要用前端电子表格替换这个 html 表格,在 component 文件夹新建一个 vue 文件,命名为 SalesTableBySpreadjs.vue ,然后在其中添加一个 template:
<template>
<TablePanel title="近期销售额">
<gc-spread-sheets
:hostClass="hostClass"
@workbookInitialized="workbookInit"
style="height: 300px"
>
<gc-worksheet
:dataSource="tableData"
:autoGenerateColumns="autoGenerateColumns"
>
<gc-column
:width="50"
:dataField="'id'"
:headerText="'ID'"
:visible="visible"
:resizable="resizable"
>
</gc-column>
<gc-column
:width="300"
:dataField="'client'"
:headerText="'Client'"
:visible="visible"
:resizable="resizable"
>
</gc-column>
<gc-column
:width="350"
:headerText="'Description'"
:dataField="'description'"
:visible="visible"
:resizable="resizable"
>
</gc-column>
<gc-column
:width="100"
:dataField="'value'"
:headerText="'Value'"
:visible="visible"
:formatter="priceFormatter"
:resizable="resizable"
>
</gc-column>
<gc-column
:width="100"
:dataField="'itemCount'"
:headerText="'Quantity'"
:visible="visible"
:resizable="resizable"
>
</gc-column>
<gc-column
:width="100"
:dataField="'soldBy'"
:headerText="'Sold By'"
:visible="visible"
:resizable="resizable"
></gc-column>
<gc-column
:width="100"
:dataField="'country'"
:headerText="'Country'"
:visible="visible"
:resizable="resizable"
></gc-column>
</gc-worksheet>
</gc-spread-sheets>
</TablePanel>
</template>
其中,gc-spread-sheets 元素创建了一个电子表格并定义了如何显示数据列。gc-column 中的 dataField 属性告诉该列应该显示底层数据集的哪个属性。
接下来是 js 部分:
import "@grapecity/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css";
_// SpreadJS imports_
import "@grapecity/spread-sheets-vue";
import Excel from "@grapecity/spread-excelio";
import TablePanel from "./TablePanel";
export default {
components: { TablePanel },
props: ["tableData"],
data(){
return {
hostClass:'spreadsheet',
autoGenerateColumns:true,
width:200,
visible:true,
resizable:true,
priceFormatter:"$ #.00"
}
},
methods: {
workbookInit: function(_spread_) {
this._spread = spread;
}
}
};
只需很少的代码即可完成。其中的几个数据属性和方法,是绑定到纯前端电子表格组件的配置选项,workbookInit 方法是 SpreadJS 在初始化工作表时调用的回调。
回到 Dashboard.vue 文件,加入刚刚创建的 SalesTableBySpreadjs 组件。 然后重新运行,即可显示电子表格数据:
<template>
<div style="background-color: #ddd">
<NavBar title="销售仪表板"/>
<div class="container">
<div class="row">
<TotalSales :total="totalSales"/>
<SalesByCountry :salesData="countrySales"/>
<SalesByPerson :salesData="personSales"/>
<SalesTableBySpreadjs :tableData="salesTableData"/>
<SalesTable :tableData="salesTableData"/>
</div>
</div>
</div>
</template>
<script>
import NavBar from "./NavBar";
import TotalSales from "./TotalSales";
import SalesByCountry from "./SalesByCountry";
import SalesByPerson from "./SalesByPerson";
import SalesTable from "./SalesTable";
import SalesTableBySpreadjs from "./SalesTableBySpreadjs";
import { groupBySum } from "../util/util";
export default {
components: { NavBar, SalesByCountry, SalesByPerson, SalesTable, TotalSales ,SalesTableBySpreadjs},
computed: {
totalSales() {
const total = this.$store.state.recentSales.reduce(
(acc, sale) => (acc += sale.value),
0
);
return parseInt(total);
},
countrySales() {
const items = this.$store.state.recentSales;
const groups = groupBySum(items, "country", "value");
return groups;
},
personSales() {
const items = this.$store.state.recentSales;
const groups = groupBySum(items, "soldBy", "value");
return groups;
},
salesTableData() {
return this.$store.state.recentSales;
}
}
};
</script>
现在我们已经用一个完整的电子表格替换了原来的 html table ,接下来可以对电子表格中的金额列中显示的金额进行编辑。比如将第 6 行的金额从 35,000 美元更改为 3500 美元,可以看到上面三个仪表板显示的内容同时也进行了更新。
原因是 SpreadJS 被编辑后同步更新了它的数据源=>VUEX store 中的 recentSales 。
到这里我们已经有了一个可以随着数据变化而实时更新的增强型仪表板。下一步我们可以通过导出导入 Excel 数据的功能来做进一步增强。
将 Excel 导出功能添加到工作表很容易。首先,在仪表板中添加一个导出按钮。把它放在表格面板的底部,在 gc-spread-sheets 结束标记之后:
</gc-spread-sheets>
<div class="row my-3">
<div class="col-sm-4">
<button class="btn btn-primary mr-3" @click="exportSheet">
导出文件
</button>
</div>
</div>
</TablePanel>
</template>
接下来添加点击时触发的 exportSheet 方法,从名为 file-saver 的 NPM 包中导入一个函数:
import { saveAs } from 'file-saver';
然后将 exportSheet 添加到组件的方法对象中:
exportSheet: function () {
const spread = this._spread;
const fileName = "SalesData.xlsx";
//const sheet = spread.getSheet(0);
const excelIO = new IO();
const json = JSON.stringify(
spread.toJSON({
includeBindingSource: true,
columnHeadersAsFrozenRows: true,
})
);
excelIO.save(
json,
function (blob) {
saveAs(blob, fileName);
},
function (e) {
console.log(e);
}
);
},
运行测试点击按钮,即可直接获取到导出的 excel 文件。 需要注意的是,我们设置了两个序列化选项:includeBindingSource 和 columnHeadersAsFrozenRows 。以确保绑定到工作表的数据被正确导出,且工作表包含列标题,。
在 template 中,添加以下代码添加一个 file 类型的 input 用于导入文件:
<div class="col-sm-8">
<button class="btn btn-primary float-end mx-2">导入文件</button>
<input
type="file"
class="fileSelect float-end mt-1"
@change="fileChange($event)"
/>
</div>
然后将 fileChange 方法添加到组件的 method 对象中:
fileChange: function (e) {
if (this._spread) {
const fileDom = e.target || e.srcElement;
const excelIO = new IO();
//const spread = this._spread;
const store = this.$store;
excelIO.open(fileDom.files[0], (data) => {
const newSalesData = extractSheetData(data);
store.commit("updateRecentSales", newSalesData);
});
}
},
选择文件后,使用 SpreadJS 中的 ExcelIO 导入它。获取其中的 json 数据。传入自定义的函数 extractSheetData ,从中提取需要的数据,然后将其提交回 Vuex store,来更新 recentSales 数据。
extractSheetData 函数可以在 src/util.util.js 文件中找到。extractSheetData 函数假定导入工作表中的数据与原始数据集具有相同的列。如果有人上传的电子表格不符合此要求,将无法解析。这个应该是大多数客户可以接受的限制。数据不符时,也可以尝试给客户一个提示信息。
另外,还需要在 main.js 中为 Vuex store 添加 updateRecentSales 来更新数据, 修改后的 store 如下:
const store = new Vuex.Store({
state: {
recentSales
},
mutations: {
updateRecentSales (state,param) {
let sales=state.recentSales;
let arr=sales.map(function(o){return o.id});
param.forEach((newsale)=>{
if(arr.indexOf(newsale.id)>0){
console.log("update");
state.recentSales[arr.indexOf(newsale.id)]=newsale;
}
else{
console.log("add");
state.recentSales.push(newsale);
}
});
console.log(state.recentSales);
}
},
actions: {
updateRecentSales ({commit},param) {
commit('updateRecentSales',param)
}
}
});
可以看到,Vuex store 调用 commit 后,会触发 updateRecentSales 方法对 recentSales 进行更新,id 相同时进行更新, 有新的 id 时进行新增。 最后,SpreadJS 工作表和所有仪表板面板都会同步更新以反映新数据。
Vue 、Vuex 和 SpreadJS 的配合使用让这个应用的增强开发变的非常方便。借助 Vue 的模板和数据绑定、Vuex 的管理共享状态,响应式数据存储和纯前端的交互式电子表格,可以在很短内创建复杂的企业 JavaScript 应用程序。
大家如果感兴趣可以访问更多在线实例: https://demo.grapecity.com.cn/spreadjs/gc-sjs-samples/index.html