如何在你的项目中接入在线excel,这一篇文章就够了~
最近公司项目中遇到了需要使用在线excel的场景,于是商讨方案后决定使用葡萄城的spread作为技术开发栈,用过之后发现还是很强大的(据说实现了excel90%的功能),就是有点小贵,适合企业使用。这里将介绍完整的使用方法,便于需要使用的小伙伴~
使用总结:
- 数据加载快,万条数据无压力,页面不卡顿
- 相比传统table适合更复杂数据交互
- 节省了原本的导入,导出excel的资源
- …
eg图片:
起步
在拿到测试开发授权的excel后,首先将其全局引入我们的项目中,在main.js中进行全局注册。
未购买的小伙伴可以有30天的试用期进行测试使用,但是只能使用loaclhost,无线进行线上测试哦
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 注册葡萄城spread
/* eslint-disable */
import '@grapecity/spread-sheets-resources-zh'
import GC from '@grapecity/spread-sheets'
GC.Spread.Common.CultureManager.culture('zh-cn')
import '@grapecity/spread-sheets-vue'
import '@grapecity/spread-sheets-charts'
import * as Excel from '@grapecity/spread-excelio'
GC.Spread.Sheets.LicenseKey = Excel.LicenseKey = '此处为授权密匙,未购买部署授权则不需填写'
/* eslint-ensable */
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
复制代码
结构
在我们的项目中使用时,将spread的引入主要拆为了3大块
- spread.mixin.js
- page.vue(使用页面)
- header.vue(表头页面)
spread.mixin.js : 通过mixin混入用来集合spread的方法,便于文件内的调用
page.vue: spread的使用页面,也是业务逻辑的呈现页面。
header.vue: 表头渲染页面,包括按钮,搜索,选择等组件的封装。
page.vue
- dom模块
<template>
<div v-loading="loading" class="spread-box">
<!-- header组件 -->
<gd-spread-header :buttons="buttons" :percentage="percentage" />
<!-- spread渲染模块 -->
<gc-spread-sheets class="spread-content" @workbookInitialized="initSpread">
<gc-worksheet :data-source="dataSource" :auto-generate-columns="autoGenerateColumns">
<gc-column
v-for="(item, index) in sheets[activeSheetIndex].columns"
:key="index"
:data-field="item.dataField"
:header-text="item.displayName"
:formatter="item.formatter"
:width="item.width"
:cell-type="item.cellType && item.cellType()"
/>
</gc-worksheet>
</gc-spread-sheets>
</div>
</template>
复制代码
- js模块
首先进行需要模块的引入与注册
import GC from '@grapecity/spread-sheets'
// spread混入引入
import { spreadMixins } from '@/mixins'
// 项目内字典表(可自行设计)
import { MEASURE_UNIT_DICT } from '@/common/dict.const'
// 头部组件引入
import { GdSpreadHeader } from '@common/components/index'
// api 引入
import { getDataApi } from '@api/index'
export default {
components: {
GdSpreadHeader
},
mixins: [spreadMixins]
}
复制代码
数据渲染,组件渲染
data: function () {
return {
// 表头按钮配置
buttons: [
{
id: 'import',
icon: 'el-icon-download',
text: '导出excel',
click: () => {
this.handleExportExcel()
}
},
{
id: 'save',
text: '保存',
click: () => {
this.handleSaveData()
}
},
{
id: 'insertRow',
text: '添加行',
nativeType: 'insertRow',
click: val => {
this.handleInsertRow({ type: 'row', val: val })
}
}
],
// 表单配置
sheets: [
{
// 数据源
dataSource: [],
name: 'ALLData',
// header 多级表头,数组深度为表头层级,用来配置多级表头
header: [
[
// addSpan 合并位置(单元格起始行,单元格起始列,向下合并的行数,向右合并的列数 )
// setValue 设置文本位置(单元格起始行,单元格起始列,文本值)
{ addSpan: [0, 0, 2, 1], setValue: [0, 0, '编码'] },
{ addSpan: [0, 1, 2, 1], setValue: [0, 1, '层级码'] },
{ addSpan: [0, 2, 2, 1], setValue: [0, 2, '名称'] },
{ addSpan: [0, 3, 2, 1], setValue: [0, 3, '计量单位'] },
{
addSpan: [0, 4, 1, 3],
setValue: [0, 4, '工程量1']
},
{ addSpan: [0, 7, 1, 3], setValue: [0, 7, '工程量2'] },
{ addSpan: [0, 10, 1, 2], setValue: [0, 10, '变化量'] }
]
],
// 表头配置
columns: [
{
dataField: 'code', // 编码
displayName: '编码', // 对应值
width: '120', // 宽度 *|自定义 *为自适应宽度
align: 'left', // 对齐方式left|center|right
formatter: '0.00' // 数据格式化
},
{
dataField: 'detailCode',
displayName: '层级码',
formatter: '0000',
width: '100',
align: 'left'
},
{
dataField: 'name',
displayName: '名称',
width: '120',
align: 'left'
},
{
dataField: 'unit',
displayName: '计量单位',
width: '100',
align: 'center',
cellType: () => {
// 此处为项目内部封装字典表转化
let cellType = new GC.Spread.Sheets.CellTypes.ComboBox()
cellType.items(this.lookUp[MEASURE_UNIT_DICT])
return cellType
}
},
{
dataField: 'amount',
displayName: '数量',
formatter: '0.00',
width: '100',
align: 'right'
},
{
dataField: 'unitMoney',
displayName: '单价(元)',
width: '120',
align: 'right'
},
{
dataField: 'sumMoney',
displayName: '合价(元)',
width: '100',
align: 'right'
},
{
dataField: 'amount',
displayName: '数量',
width: '100',
align: 'right'
},
{
dataField: 'unitMoney',
displayName: '单价',
width: '120',
align: 'right'
},
{
dataField: 'sumMoney',
displayName: '合价',
width: '100',
align: 'right'
},
{
dataField: 'finalAmount',
displayName: '数量',
width: '100',
align: 'right'
},
{
dataField: 'finalPrice',
displayName: '合价(元)',
width: '100',
align: 'right'
}
]
}
]
}
},
复制代码
- function 执行
<script>
methods: {
// spread组件初次加载
initSpread: function (spread) {
// 定义spread
this.spread = spread
// 限制滚动区域(true/false)
spread.options.scrollbarMaxAlign = true
// 屏蔽sheet增加按钮
spread.options.newTabVisible = this.newTabVisible
// 配置多级表头
this.handleSetTableHeader()
// 配置表单保护
this.handleSetProtectedArea()
// 格式化数据
this.handleFormatterData({ startLen: 4, endLen: 12, unit: 3 })
// 获取字典
this.getLookUp()
// 设置对齐方式
this.handleSetAlign()
// 配置层级码格式校验
this.handleDetailCodeCheck({ position: 1 })
// ... 其他集成方法
// 请求数据
this.loadData('init')
},
// 获取初始数据
loadData(type) {
this.loading = true
const params = {
currentPage: this.currentPage,
pageSize: this.pageSize
}
getDataApi(params)
.then(res => {
// 总页数
this.totalPages = res.pages
// 总数据条数
this.total = res.total
// 执行获取数据后的内容
this.handleExitOption({ type: type, data: res.records })
// 无数据时进度条加载为100%
if (res.records.length == 0) {
this.percentage = 100
} else {
this.percentage = parseInt((this.currentPage / this.totalPages) * 100)
}
})
.catch(() => {
this.loading = false
})
},
// 配置执行
handleExitOption({ type, data }) {
// 配置数据源
let sheet = this.spread.getSheet(this.activeSheetIndex)
this.dataSource = data
// suspendPaint 挂起绘制,加速进程,避免卡顿
sheet.suspendPaint()
// 拼接新数据
this.activeRow = sheet.getRowCount()
sheet.addRows(this.activeRow, data.length)
let datasource = sheet.getDataSource()
data.map((item, index) => {
datasource[this.activeRow + index] = item
})
// 设置边线
this.handleSetBorder()
// 配置树形结构
this.handleSetTreeData(data)
// 结束绘制,加速进程,避免卡顿
sheet.resumePaint()
// 关闭loading
this.loading = false
// 开启加载
this.isAddData = true
// 清除变化
sheet.clearPendingChanges()
// 大于1页时开启加载,加载后续数据
if (type === 'init' && this.totalPages > 1) {
this.addAll()
}
},
// 提交变更数据(增删改脏数据)
submitDirtyData() {
const params = this.dirtyData
Object.assign(params, { sectionId: this.id })
replydoExcelData(params).then(() => {
this.$message.success('保存成功')
})
},
// 获取字典表数据(项目内部方法)
getLookUp() {
let lookUpData = JSON.parse(sessionStorage.getItem('LOOKUPS'))[MEASURE_UNIT_DICT]
lookUpData.map(item => {
item.text = item.name
item.value = item.code
})
this.lookUp[MEASURE_UNIT_DICT] = lookUpData
},
// 配置表单保护
handleSetProtectedArea() {
this.handleGetSheet().options.protectionOptions = this.protectedOption
this.handleGetSheet().options.isProtected = this.isProtected
// 设置默认style
let defaultStyle = new GC.Spread.Sheets.Style()
defaultStyle.locked = true
defaultStyle.foreColor = this.protectedTextColor
this.handleGetSheet().setDefaultStyle(defaultStyle, GC.Spread.Sheets.SheetArea.viewport)
// 可编辑列-style
let styleNo = new GC.Spread.Sheets.Style()
styleNo.foreColor = this.defaultTextColor
styleNo.locked = false
// 要保护的行列
for (let i = 7; i < 10; i++) {
this.handleGetSheet().setStyle(-1, i, styleNo)
}
},
// 循环加载所有数据
addAll() {
this.currentPage++
if (this.isGetNextData && this.currentPage <= this.totalPages) {
const params = {
currentPage: this.currentPage,
pageSize: this.pageSize
}
this.isGetNextData = false
getDataApi(params).then(res => {
this.totalPages = res.pages
this.total = res.total
this.percentage = parseInt((this.currentPage / this.totalPages) * 100)
this.allData = this.allData.concat(res.records)
this.handleExitOption({ type: 'add', data: res.records })
this.isGetNextData = true
this.addAll()
})
}
}
}
}
</script>
复制代码
header.vue (头部渲染组件)
这里二次包装了,按钮,选择,搜索,删上传等常用组件(element-ui),便于项目快速开发。
<template>
<div>
<div class="spread-header">
<!-- left -->
<div class="spread-header-left">
<div v-for="(item, index) in buttons" :key="index">
<!-- 选择器 -->
<div v-if="item.nativeType && item.nativeType === 'select'" class="spread-el-button">
<el-select
v-model="item.contractId"
size="small"
:placeholder="item.placeholder"
@change="selectChange"
>
<el-option
v-for="(childItem, childIndex) in item.list"
:key="'select' + childIndex"
:label="childItem.contractName"
:value="childItem.id"
/>
</el-select>
</div>
<!-- 普通按钮 -->
<el-button
v-if="!item.nativeType"
:id="item.id"
class="spread-el-button"
size="small"
:icon="item.icon"
:type="item.type && item.type"
:loading="item.loading"
@click="item.click && item.click()"
>
{{ item.text }}
</el-button>
<!-- 上传按钮 -->
<el-upload
v-if="item.nativeType && item.nativeType === 'upload'"
ref="upload"
class="spread-el-button"
action=""
:on-change="item.beforeUpload"
:file-list="fileList"
:auto-upload="false"
:show-file-list="false"
>
<el-button slot="trigger" size="small" type="primary">{{ item.text }}</el-button>
</el-upload>
<!-- 插入行 -->
<div class="spread-el-button">
<el-input
v-if="item.nativeType && item.nativeType === 'insertRow'"
v-model="insertRowVal"
placeholder="请输入"
size="small"
style="width: 160px"
>
<el-button slot="append" @click="item.click && item.click(insertRowVal)">
{{ item.text }}
</el-button>
</el-input>
</div>
</div>
</div>
<!-- right -->
<div class="spread-header-right">
<div v-for="(item, index) in queryFields" :key="'fileds' + index">
<el-date-picker
v-model="item.value"
size="small"
type="date"
:placeholder="item.placeholder"
/>
<i class="el-icon-search spread-el-icon" @click="handleQuery(item.value)"></i>
</div>
</div>
</div>
<div class="spread-progress">
<el-progress :percentage="percentage" />
</div>
</div>
</template>
<script>
export default {
props: {
buttons: {
type: Array,
default: () => []
},
queryFields: {
type: Array,
default: () => []
},
fileList: {
type: Array,
default: () => []
},
percentage: {
type: Number,
default: () => 0
}
},
data() {
return {
insertRowVal: '' // 插入行数
}
},
watch: {
// 行数规则
insertRowVal(val) {
val = parseInt(val)
this.insertRowVal = val
let reg = /^[1-9]\d*$/
if (!reg.test(val)) {
this.insertRowVal = ''
} else if (val > 999) {
this.insertRowVal = val.toString().substring(0, 3)
}
}
},
methods: {
selectChange(e) {
this.$emit('selectChange', e)
},
handleQuery(e) {
this.$emit('handleQuery', e)
}
}
}
</script>
<style lang="scss" scoped>
.spread-header {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 15px;
.spread-header-left {
display: flex;
flex-direction: row;
}
.spread-header-right {
display: flex;
flex-direction: row;
.spread-header-right_search {
display: flex;
align-items: center;
}
}
}
.spread-el-button {
margin-right: 10px;
}
.spread-el-icon {
margin-left: 5px;
// 全局换肤主题色
@include color('header-bg-color');
}
</style>
复制代码
spread.mixin.js
这里进行常用sprad方法的包装与介绍
配置多级表头
可进行多级表头的配置,合并等操作,只需设置表头合并位置
handleSetTableHeader() {
let spreadNS = GC.Spread.Sheets
let header = this.sheets[this.activeSheetIndex].header
this.handleGetSheet().setRowCount(header.length + 1, spreadNS.SheetArea.colHeader)
this.handleGetSheet().setColumnCount(1, spreadNS.SheetArea.rowHeader)
header.map(item => {
item.map(items => {
this.handleGetSheet().addSpan(
items.addSpan[0],
items.addSpan[1],
items.addSpan[2],
items.addSpan[3],
GC.Spread.Sheets.SheetArea.colHeader
)
this.handleGetSheet().setValue(
items.setValue[0],
items.setValue[1],
items.setValue[2],
GC.Spread.Sheets.SheetArea.colHeader
)
})
})
}
复制代码
配置树形结构
将数据进行树形层级结构配置,可进行收缩与展开,你可以使用分组列来展示有分层结构的数据,使数据呈现树形结构。
内部集成了字典转化,与数据状态添加等操作。
handleSetTreeData(data) {
this.handleGetSheet().suspendPaint()
let d = data
let style = new GC.Spread.Sheets.Style();
style.backColor = 'yellow';
style.foreColor = 'red';
for (let r = 0; r < d.length; r++) {
let level = d[r].level
this.handleGetSheet().getCell(r + this.activeRow, 0).textIndent(level)
// 转化字典
d[r].measureUnit = this.handleBook(d[r].measureUnit)
// 添加新增数据状态
if (d[r].detailStatus == '1') {
this.handleGetSheet().setStyle(r + this.activeRow, -1, style, GC.Spread.Sheets.SheetArea.viewport);
}
}
this.handleGetSheet().outlineColumn.options({ columnIndex: 0 })
// 隐藏左侧状态栏
this.handleGetSheet().showRowOutline(false)
this.handleGetSheet().outlineColumn.options({
columnIndex: 0,
expandIndicator: require('@/assets/spread/add-circle.png'),
collapseIndicator: require('@/assets/spread/del-circle.png')
})
this.handleGetSheet().resumePaint()
}
复制代码
获取表单json数据结构
进行json数据的获取可让我们进行复制,导出等一系列操作
handleSerialization() {
const serializationOption = {
includeBindingSource: this.includeBindingSource, // 在将工作簿转换为json时包含绑定源,默认值为false
saveAsView: this.saveAsView, // 将工作簿转换为json时,包含格式化字符串的结果,默认值为false
includeAutoMergedCells: this.includeAutoMergedCells, // 将工作簿转换为json时,将自动合并的单元格包含为实际合并的单元格
ignoreFormula: this.ignoreFormula, // 忽略公式,默认为true
ignoreStyle: this.ignoreStyle, // 将工作簿转换为json时忽略样式,默认值为false
rowHeadersAsFrozenColumns: this.rowHeadersAsFrozenColumns, // 将工作簿转换为json时,将行标头视为冻结列,默认值为false
columnHeadersAsFrozenRows: this.columnHeadersAsFrozenRows // 将工作簿转换为json时,将列标头视为冻结行,默认值为false
}
let jsonStr = this.spread.toJSON(serializationOption)
this.$message.success('导出json成功')
console.log('导出json数据-表头', jsonStr.sheets.Sheet1.columns)
console.log('导出json数据-数据源', jsonStr.sheets.Sheet1.data.dataTable)
}
复制代码
导出excel
将页面内excel导出到本地
handleExportExcel() {
// 导出解开表单保护
this.handleGetSheet().options.isProtected = false;
const serializationOption = {
includeBindingSource: this.includeBindingSource, // 在将工作簿转换为json时包含绑定源,默认值为false
saveAsView: this.saveAsView, // 将工作簿转换为json时,包含格式化字符串的结果,默认值为false
includeAutoMergedCells: this.includeAutoMergedCells, // 将工作簿转换为json时,将自动合并的单元格包含为实际合并的单元格
ignoreFormula: this.ignoreFormula, // 忽略公式,默认为true
ignoreStyle: this.ignoreStyle, // 将工作簿转换为json时忽略样式,默认值为false
rowHeadersAsFrozenColumns: this.rowHeadersAsFrozenColumns, // 将工作簿转换为json时,将行标头视为冻结列,默认值为false
columnHeadersAsFrozenRows: this.columnHeadersAsFrozenRows // 将工作簿转换为json时,将列标头视为冻结行,默认值为false
}
const excelIo = new IO()
let fileName = this.fileName
if (fileName === undefined) {
fileName = (new Date()).getTime() + '.xlsx'
}
const password = this.password
if (fileName.substr(-5, 5) !== '.xlsx') {
fileName += '.xlsx'
}
let json = this.spread.toJSON(serializationOption)
// 导出后重新保护表单
this.handleGetSheet().options.isProtected = true
// here is excel IO API
excelIo.save(
json,
function (blob) {
FaverSaver.saveAs(blob, fileName)
},
function (e) {
// process error
console.log(e)
},
{
password: password
}
)
}
复制代码
导入excel
将本地excel导入页面
handleImportExcel(excelFile) {
const excelIo = new IO()
const password = this.password
// here is excel IO API
excelIo.open(
excelFile,
function (json) {
let workbookObj = json
this.spread.fromJSON(json)
this.spread.fromJSON(workbookObj)
},
function (e) {
// process error
console.log(e.errorMessage)
},
{
password: password
}
)
}
复制代码
获取变更数据
此方法可获取删除,修改,新增数据,便于后台交互
此处将脏数据放于数组中,add-新增数据,update-变更数据,delete-删除数据
handleSaveData() {
this.dirtyData = {
add: [],
update: [],
delete: []
}
let editRows = this.handleGetSheet().getDirtyRows()
let insertRows = this.handleGetSheet().getInsertRows()
let deleteRows = this.handleGetSheet().getDeletedRows()
// 处理数据
editRows.map(item => {
this.dirtyData.update.push(item.item)
})
insertRows.map(item => {
this.dirtyData.add.push(item.item)
})
deleteRows.map(item => {
// 与后端确认,目前所有页面的spreadJS删除均传入id
this.dirtyData.delete.push(item.originalItem.id)
})
// 转化单位字典
for (let itemArr in this.dirtyData) {
if (itemArr !== 'delete') {
this.dirtyData[itemArr].map(item => {
// 更改sectionId
item.contractSectionId = this.id || this.contractSectionId
// 单位匹配
this.lookUp[MEASURE_UNIT_DICT].map(items => {
if (items.name === item.measureUnit) {
item.measureUnit = items.value
return item.measureUnit
}
})
})
}
}
// 提交数据
// ...
}
复制代码
监听返回底部
监听excel到达最后一行数据,便于进行数据懒加载、拼接数据等操作
handleWatchGetBottom() {
const _this = this
function bottomF() {
let row = _this.handleGetSheet().getViewportBottomRow(1)
let rowCount = _this.handleGetSheet().getRowCount()
if ((row === rowCount - 1) && _this.isAddData === true) {
console.log('滚动到底部了', row, _this.currentPage, _this.pageSize)
if (_this.currentPage >= _this.pageSize) {
// 判断是否到达底部
_this.$message.warning('已无更多数据')
} else if (_this.currentPage < _this.pageSize) {
_this.isAddData = false
// 定位行
_this.activeRow = row
// 当前页数自增
_this.currentPage++
// 执行新数据获取,拼接更多数据
_this.loadData('add')
}
}
}
// debounce 为节流函数
this.handleGetSheet().bind(GC.Spread.Sheets.Events.TopRowChanged, debounce(bottomF, 500))
},
复制代码
配置公式
进行自动计算等操作,此处为使用示例
/**
*
* @param {*} data 数据
* @param {*} activeRow 追加数据最后一行索引
* @param {*} toArr 要配置公式的列
* @param {*} fromArr 要配置公式的数据源
*/
handleSetFormula({ data, activeRow, toArr, fromArr }) {
this.handleGetSheet().suspendPaint()
data.map((_, index) => {
const indexs = activeRow + index + 1
this.handleGetSheet().setFormula(
activeRow + index,
toArr[0],
`=SUM(${fromArr[0]}${indexs}-${fromArr[1]}${indexs})`
)
this.handleGetSheet().setFormula(
activeRow + index,
toArr[1],
`=SUM(${fromArr[2]}${indexs}-${fromArr[3]}${indexs})`
)
})
this.handleGetSheet().resumePaint()
// 清除公式变化数据
this.handleGetSheet().clearPendingChanges({
clearType: 1,
row: -1,
rowCount: -1,
col: -1,
colCount: -1
})
}
复制代码
设置边线
用来给表单,单元格设置自定义边线(示例)
all:true 上下左右都添加
-1,-1 代表全表添加
let lineStyle = GC.Spread.Sheets.LineStyle.thin
let lineBorder = new GC.Spread.Sheets.LineBorder('#cccccc', lineStyle)
let sheetArea = GC.Spread.Sheets.SheetArea.viewport
this.handleGetSheet().getRange(-1, -1, 0, 0).setBorder(lineBorder, { all: true }, sheetArea)
复制代码
对齐方式
设置单元格内文本对齐方式
left|center|right
this.handleGetSheet().getCell(-1, 0).hAlign(GC.Spread.Sheets.HorizontalAlign.left)
复制代码
刷新spread
在页面视口大小发生改变时,可调取进行页面适配
this.spread.refresh()
复制代码
最后
本篇对常用的spread功能进行了总结,后续还会进行更多使用场景的更新迭代~
coding加油~