Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

下图是一个产品开发中非常常见的大屏展示界面示例。

通过Vue提供的Vuex,上方三个仪表板以及下方的表格组件共享同一个数据源,已经实现了数据改变后同步响应更新。

(数据响应-销售仪表板)

很棒的大屏展示功能,能支持Excel数据的导入导出吗,表格数据可以实时编辑更新吗?

如果您已经开发软件很长时间,您可能不止一次地从最终客户或者产品经理那里听到过这个灵魂拷问。对于非技术人群来说,觉得要求 Excel 导入/导出/展示是一个非常正常且容易实现的需求。

但实际上,这个问题常常让前端开发人员感到恐惧。处理 Excel 文件需要大量工作。

这个问题通过SpreadJS可以变得简单,将电子表格嵌入Web 应用程序。同时和其他的组件进行交互。 这篇博客将研究如何使用现有的这个大屏展示 Vue 应用作为基础,使用 SpreadJS 对其进行增强。

本文假定您已经了解 HTML、CSS 和 JavaScript。以及Vue的基础应用。如果您有使用过Vuex ,当然会更容易理解,如果还没有,也不用担心。VueX在这个项目中的应用很简单。

关于VueX,可以在Vue官网了解更多信息

本文将分为下面的几个部分

  • Vuex的原始应用
  • 给应用添加实时编辑功能
  • 添加 Excel 数据导入功能
  • 添加导出为Excel功能

包含Vuex的原始应用

如上图看到的,将要使用的 Vue 应用程序是一个简单的大屏展示界面,带有几个汇总信息仪表板和一个数据表。

可以通过下面的附件获取这个Vue应用项目代码,然后运行“npm install”以及 “npm run serve”即可启动应用 。

附件下载地址:

https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MjI1NzA1fDNkMDNjNjQ2fDE2NjAxMTUxMjF8NjI2NzZ8OTk3MTg%3D

原始的Vue 应用代码结构如下:

  • Vuex 和 Vue 应用程序都定义在main.js中。
  • 有几个单文件 Vue 组件,位于该components文件夹中。

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中,那么如果数据更新,所有仪表板面板都会自动更新。

当我们用可以编辑的电子表格替换现有的表格来进行编辑时,这种特性将派上用场。

将前端电子表格添加到您的 Vue 应用程序

我们要用前端电子表格替换这个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文件

将 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。以确保绑定到工作表的数据被正确导出,且工作表包含列标题,。

Excel 数据导入

在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 工作表和所有仪表板面板都会同步更新以反映新数据。

最终的项目下载地址:

https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MjI1NzA2fDcxYzkxZjZmfDE2NjAxMTUxMjF8NjI2NzZ8OTk3MTg%3D

Vue、Vuex 和 SpreadJS 的配合使用让这个应用的增强开发变的非常方便。借助 Vue 的模板和数据绑定、Vuex 的管理共享状态,响应式数据存储和 SpreadJS 的交互式电子表格,可以在很短内创建复杂的企业 JavaScript 应用程序。

大家如果感兴趣可以访问更多在线实例:

https://demo.grapecity.com.cn/spreadjs/gc-sjs-samples/index.html