vue3 + Luckysheet的使用
- 模板页面功能:
- 导入excel
- 提交模板(一次性提交完整json)
- 填报页面功能:
- 导出excel
- 自动保存(修改单元格触发)
- 模板json与已填单元格数据整合作为填报页面数据。
- 设置冻结
- 设置可编辑区
- 设置数据验证
- 单元格内容填充(替换填报json)与单元格值填充(只保存值 + 模板json组合)
- 问题修复
- 修改 luckysheet 源代码。问题修复:设置单元格格式为数字的千分位格式时,数据验证,type设置为数字时,验证不通过
- 带千分位符格式的数字,使用序列填充后,后面填充的值会是字符串,且数据不对,及不会自动触发公式的计算。如出现898,9.010这样的数据,所以,直接将源码中的填充方式默认修改为复制,其它提供的一律砍掉。(目前也用不上,临时修改)
官方提供dropType的类型
注:修改完luckysheet源码后,需要重新打包再引用到项目中。
一、 Luckysheet的介绍
Luckysheet,一款纯前端类似excel的在线表格,功能强大、配置简单、完全开源。
二、Luckysheet简单使用
luckysheet源码地址:https://gitee.com/mengshukeji/Luckysheet/
- 下载源码,查看package.json文件,打包生成dist文件夹
npm run build
- 将打包生成的dist文件,重命名为luckysheet_dist,放在 根目录/public文件夹下
- 在index.html中引入luckysheet
<link rel='stylesheet' href='/luckysheet_dist/plugins/css/pluginsCss.css' />
<link rel='stylesheet' href='/luckysheet_dist/plugins/plugins.css' />
<link rel='stylesheet' href='/luckysheet_dist/css/luckysheet.css' />
<link rel='stylesheet' href='/luckysheet_dist/assets/iconfont/iconfont.css' />
<script src="/luckysheet_dist/plugins/js/plugin.js"></script>
<script src="/luckysheet_dist/luckysheet.umd.js"></script>
2.1 填报页面:配置文件
// src/utils/fillConfig.js文件
import { updateTemplate } from "@/api/api.js";
/** luckysheet 职能预算填报 start */
/** 整体配置 start */
//配置项 作用于整个表格。特别的,单个sheet的配置项需要在options.data数组中
export const options = {
container: "luckysheet", // 设定DOM容器的id:luckysheet为容器id
title: "职能预算", // 设置表格名称
lang: "zh", // 设定表格语言
// 更多其它设置...
data: [],
column: 18, // 空表格默认的列数量
row: 36, //空表格默认的行数据量
showtoolbar: false, //是否显示工具栏
// 强制刷新公式,初始化时显示正确,但是实际值还是上一个结果
forceCalculation: true, //强制刷新公式。!!!提醒:公式较多时会有性能问题,慎用!(影响初始化时的速度...)
showtoolbarConfig: {
//自定义配置工具栏
undoRedo: false, // 撤销重做,注意撤销重做是两个按钮,由这一个配置决定显示还是隐藏
paintFormat: false, //格式刷
currencyFormat: false, //货币格式
percentageFormat: false, //百分比格式
numberDecrease: false, // '减少小数位数'
numberIncrease: false, // '增加小数位数'
moreFormats: false, // '更多格式'
font: false, // '字体'
fontSize: false, // '字号大小'
bold: false, // '粗体(Ctrl + B)'
italic: false, // '斜体(Ctrl + I)'
strikethrough: false, //删除线(Alt + Shift + 5)
underline: false, // 下划线(Alt + Shift + 6)
textColor: false, //文本颜色
fillColor: false, // 单元格颜色
border: false, // 边框
mergeCell: false, //合并单元格
horizontalAlignMode: false, //水平对齐方式
verticalAlignMode: false, //垂直对齐方式
textWrapMode: false, //换行方式
textRotateMode: false, //文本旋转方式
image: false, //插入图片
link: false, //插入链接
chart: false, //图表(图标隐藏,但是如果配置了chart插件,右击仍然可以新建图表)
postil: false, //批注
pivotTable: false, //数据透视表
function: false, //公式
frozenMode: false, //冻结方式
sortAndFilter: false, //排序和筛选
conditionalFormat: false, //条件格式
dataVerification: false, //数据验证
splitColumn: false, //分列
screenshot: false, //截图
findAndReplace: false, //查找替换
protection: false, // 工作表保护
print: false, //打印
},
showinfobar: false, //是否显示顶部信息栏
showsheetbar: true, //是否显示底部sheet页按钮
showsheetbarConfig: {
//自定义配置底部sheet页按钮
add: false, //新增sheet
menu: false, //sheet管理菜单
sheet: true, //sheet页显示
},
showstatisticBar: false, //是否显示底部计数栏
showstatisticBarConfig: {
//自定义配置底部计数栏
count: false, //计数栏
view: false, //打印视图
zoom: true, //缩放
},
enableAddRow: false, //允许添加行
enableAddBackTop: false, //允许回到顶部
userInfo: false, //右上角的用户信息展示
userMenuItem: [], //点击右上角的用户信息弹出的菜单
myFolderUrl: "", //左上角 < 返回按钮的链接
functionButton: "", //右上角功能按钮
cellRightClickConfig: {
//自定义配置单元格右击菜单
copy: false, //复制
copyAs: false, //复制为
paste: false, //粘贴
insertRow: false, //插入行
insertColumn: false, //插入列
deleteRow: false, //删除选中行
deleteColumn: false, //删除选中列
deleteCell: false, //删除单元格
hideRow: false, //隐藏选中行和显示选中行
hideColumn: false, //隐藏选中列和显示选中列
rowHeight: false, //行高
columnWidth: false, //列宽
clear: false, //清除内容
matrix: false, //矩阵操作选区
sort: false, //排序选区
filter: false, //筛选选区
chart: false, //图表生成
image: false, //插入图片
link: false, //插入链接
data: false, //数据验证
cellFormat: false, //设置单元格格式
},
sheetRightClickConfig: {
//自定义配置sheet页右击菜单
delete: false, //删除
copy: false, //复制
rename: false, //重命名
color: false, //更改颜色
hide: false, //隐藏,取消隐藏
move: false, //向左移,向右移
},
rowHeaderWidth: 46, //行标题区域的宽度,如果设置为0,则表示隐藏行标题
columnHeaderHeight: 20, //列标题区域的高度,如果设置为0,则表示隐藏列标题
sheetFormulaBar: true, //是否显示公示栏
defaultFontSize: 11, //初始化默认字体大小
limitSheetNameLength: true, //工作表重命名等场景下是否限制工作表名称的长度
defaultSheetNameMaxLength: 31, //默认允许的工作表名最大长度
// pager: { //分页器按钮设置
// pageIndex: 1, //当前的页码
// pageSize: 10, //每页显示多少行数据
// total: 50, //数据总行数
// selectOption: [10, 20], //允许设置每页行数的选项
// },
hook: {
//钩子函数
/** 单元格 start */
/**进入单元格编辑模式之前触发。在选中了某个单元格且在非编辑状态下,通常有以下三种常规方法触发进入编辑模式 1.双击单元格 2.敲Enter键 3.使用API:enterEditMode
* 参数:{ Array }[range]: 当前选区范围
*/
cellEditBefore: (range) => {
console.log("进入单元格编辑模式之前触发:cellEditBefore");
console.log(`当前选区范围:`, range);
},
/**更新这个单元格值之前触发,return false 则不执行后续的更新。在编辑状态下修改了单元格之后,退出编辑模式并进行数据更新之前触发这个钩子
* 参数:
* {Number}[r]:单元格所在行数
* {Number}[c]:单元格所在列数
* {Object | String | Number}[value]:要修改的单元格内容
* {Boolean}[isRefresh]:是否刷新整个表格
*/
cellUpdateBefore: (r, c, value, isRefresh) => {
console.log("更新这个单元格值之前触发:cellUpdateBefore");
console.log(
`单元格所在行:${r};单元格所在列:${c};是否刷新整个表格:${isRefresh};要修改的单元格内容: `,
value
);
},
/** 更新这个单元格后触发 */
cellUpdated: (r, c, oldValue, newValue, isRefresh) => {
console.log("cellUpdated:", r, c, oldValue, newValue);
// 获取单元格的值
const value = luckysheet.getCellValue(r, c, { type: "v" });
console.log("更新后的值:", value, "对值进行数据验证");
},
/**单元格渲染前触发 return false 则不渲染该单元格
* 参数:
* {Object}[cell]:单元格对象
* {Object}[position]:
* {Number}[r]:单元格所在行号
* {Number}[c]:单元格所在列号
* {Number}[start_r]:单元格左上角的水平坐标
* {Number}[start_c]:单元格左上角的垂直坐标
* {Number}[end_r]:单元格右下角的水平坐标
* {Number}[end_c]:单元格右下角的垂直坐标
* {Object}[sheet]:当前sheet对象
* {Object}[ctx]:当前画布的context
*/
cellRenderBefore: (cell, position, sheet, ctx) => {
// console.log('单元格渲染前触发:cellRenderBefore');
// console.log('单元格对象:', cell, '单元格所在行号:', position.r, '单元格所在列号:', position.c, '单元格左上角的水平坐标:',
// position.start_r, '单元格左上角的垂直坐标:', position.start_c, '单元格右下角的水平坐标:', position.end_r, '单元格右下角的垂直坐标:', position.end_c,
// '当前sheet对象:', sheet, '当前画布的context:', ctx);
},
/**单元格渲染结束后触发,return false 则不渲染该单元格
* 参数:
* {Object} [cell]:单元格对象
* {Object} [position]:
* {Number} [r]:单元格所在行号
* {Number} [c]:单元格所在列号
* {Number} [start_r]:单元格左上角的水平坐标
* {Number} [start_c]:单元格左上角的垂直坐标
* {Number} [end_r]:单元格右下角的水平坐标
* {Number} [end_c]:单元格右下角的垂直坐标
* {Object} [sheet]:当前sheet对象
* {Object} [ctx]:当前画布的context
*
*/
cellRenderAfter: () => {},
/**所有单元格渲染之前执行的方法
* 参数:
* {Object}[data]:当前工作表二维数组数据
* {Object}[sheet]:当前sheet对象
* {Object}[ctx]:当前画布的context
*/
cellAllRenderBefore: (data, sheet, ctx) => {
// console.log('当前工作表二维数组数据:', data);
},
/**行标题单元格渲染前触发,return false 则不渲染行标题
* 参数:
* {String}[rowNum]:行号
* {Object}[position]:
* {Number}[r]:单元格所在行号
* {Number}[top]:单元格左上角的垂直坐标
* {Number}[width]:单元格宽度
* {Number}[height]:单元格高度
* {Object}[ctx]:当前画布的context
*/
rowTitleCellRenderBefore: () => {},
/** 行标题单元格渲染后触发 return false 则不渲染行标题*/
rowTitleCellRenderAfter: () => {},
/** 列标题单元格渲染前触发 return false 则不渲染列标题 */
columnTitleCellRenderBefore: () => {},
/** 列标题单元格渲染后触发,return false 则不渲染列标题 */
columnTitleCellRenderAfter: () => {},
/** 单元格 end */
/** 鼠标钩子 start */
/** 单元格点击前的事件,return false 则终止之后的点击操作 */
cellMousedownBefore: (cell, position, sheet, ctx) => {},
/** 单元格点击后的事件,return false 则终止之后的点击操作 */
cellMousedownAfter: (cell, position, sheet, ctx) => {},
/** 鼠标移动事件,可通过cell判断鼠标停留在哪个单元格 */
sheetMousemove: () => {},
/** 鼠标按钮释放事件,可通过cell判断鼠标停留在哪个单元格 */
sheetMouseup: () => {},
/** 鼠标滚动事件 */
scroll: (position) => {},
/** 鼠标拖拽文件到Luckysheet内部的结束事件 */
cellDragStop: () => {},
/** 鼠标钩子 end */
/** 选区操作 (包括单元格) start */
/** 框选或者设置选区后触发 参数:{Object}[sheet]:当前选区对象 {Object | Array}[range]:选区范围,可能为多个选区 */
rangeSelect: (sheet, range) => {
console.log("选区范围:", range);
},
/** 移动选区前,包括单个单元格 */
rangeMoveBefore: (range) => {},
/** 移动选区后,包括单个单元格 */
rangeMoveAfter: (range) => {},
/** 选区修改前 */
rangeEditBefore: (range, data) => {},
/** 选区修改后 */
rangeEditAfter: (range, oldData, newData) => {},
/** 选区复制前 */
rangeCopyBefore: (range, data) => {},
/** 选区复制后 */
rangeCopyAfter: (range, data) => {},
/** 选区粘贴前 */
rangePasteBefore: (range, data) => {},
/** 选区粘贴后 */
rangePasteAfter: (range, originData, pasteData) => {},
/** 选区剪切前 */
rangeCutBefore: (range, data) => {},
/** 选区剪切后 */
rangeCutAfter: (range, data) => {},
/** 选区删除前 */
rangeDeleteBefore: (range, data) => {},
/** 选区删除后 */
rangeDeleteAfter: (range, data) => {},
/** 选区清除前 */
rangeClearBefore: (range, data) => {},
/** 选区清除后 */
rangeClearAfter: (range, data) => {},
/** 选区下拉前 */
rangePullBefore: (range) => {},
/** 选区下拉后 */
rangePullAfter: (range) => {},
/** 选区操作 (包括单元格) end */
/** 工作表 start */
/** 创建sheet页前触发,sheet页新建也包含数据透视表新建 */
sheetCreatekBefore: () => {},
/** 创建sheet页后触发,sheet页新建也包含数据透视表新建 */
sheetCreateAfter: (sheet) => {},
/** sheet移动前 */
sheetMoveBefore: (i, order) => {},
/** sheet移动后 */
sheetMoveAfter: (i, oldOrder, newOrder) => {},
/** sheet删除前 */
sheetDeleteBefore: (sheet) => {},
/** sheet删除后 */
sheetDeleteAfter: (sheet) => {},
/** sheet修改名称前 */
sheetEditNameBefore: (i, name) => {},
/** sheet修改名称后 */
sheetEditNameAfter: (i, oldName, newName) => {},
/** sheet修改颜色前 */
sheetEditColorBefore: (i, color) => {},
/** sheet修改颜色后 */
sheetEditColorAfter: (i, oldColor, newColor) => {},
/** sheet缩放前 */
sheetZoomBefore: (i, zoom) => {},
/** sheet缩放后 */
sheetZoomAfter: (i, oldZoom, newZoom) => {},
/** 激活工作表前 */
sheetActivate: (i, isPivotInitial, isNewSheet) => {},
/** 工作表从活动状态转为非活动状态前 */
sheetDeactivateBefore: (i) => {},
/** 工作表从活动状态转为非活动状态后 */
sheetDeactivateAfter: (i) => {},
/** 工作表 end */
/** 工作薄 start */
/** 表格创建之前触发 */
workbookCreateBefore: (book) => {},
/** 表格创建之后触发 */
workbookCreateAfter: (book) => {},
/** 表格销毁之前触发 */
workbookDestroyBefore: (book) => {},
/** 表格销毁之后触发 */
workbookDestroyAfter: (book) => {},
/** 协同编辑中的每次操作后执行的方法,监听表格内容变化,即客户端每执行一次表格操作,Luckysheet将这次操作存到历史记录中后触发,撤销重做时因为也算一次操作,也会触发此钩子函数 参数:{Object}[operate]:本次操作的历史记录信息,根据不同的操作,会有不同的历史记录 */
updated: (operate) => {
console.log("luckysheetUpdatedHook", operate);
// 监听更新,并在3s后自动保存
// if (autoSave) {
// console.log(autoSave, "autoSave");
// clearTimeout(autoSave);
// $(luckysheet_info_detail_save).text("已修改");
// autoSave = setTimeout(() => {
// const excel = luckysheet.getAllSheets();
// // 去除临时数据,减小体积
// for (const i in excel) {
// excel[i].ddata = undefined;
// }
// // $.post('http://127.0.0.0:8081/setWorkBook', {
// // jsonExcel: JSON.stringify(excel)
// // }, () => {
// // $(luckysheet_info_detail_save).text('已保存')
// // })
// }, 1 * 300);
// return true;
// }
},
/** resize 执行之后 */
resized: (size) => {},
/** 工作薄 end */
/** 冻结 start */
/** 设置冻结前 */
frozenCreateBefore: (frozen) => {},
/** 设置冻结后 */
frozenCreateAfter: (frozen) => {},
/** 取消冻结前 */
frozenCancelBefore: (frozen) => {},
/** 取消冻结后 */
frozenCancelAfter: (frozen) => {},
/** 冻结 end */
/** 分页器 start */
/** 点击分页按钮回调函数,返回当前页码 */
onTogglePager: (page) => {},
/** 分页器 end */
},
/** 协同编辑 start */
// loadUrl: gridKey:工作簿的唯一标识, 加载luckysheet数据的地址
// loadUrl: 'http://localhost:8081/getWorkBook?gridKey=1',
// loadUrl: "/baseURL/excel",
// updateUrl: '', //后台websocket地址
allowUpdate: true,
/** 协同编辑 end */
};
/** 整体配置 end */
/** luckysheet 职能预算填报 end */
2.2 填报页面:页面使用
// luckysheet/fill.vue页面
<!-- 填报页面 -->
<template>
<div class="tw-w-full tw-h-full tw-flex tw-flex-col">
<div
class="tw-pr-2 tw-pl-2 tw-pb-2 tw-box-border tw-absolute tw-top-2 tw-left-0 tw-w-full tw-flex tw-justify-between tw-items-center"
@click="exitEditMode">
<div class="tw-flex tw-justify-between tw-items-center">
<div class="tw-mr-3">
<span class="tw-mr-1">cc</span>
<el-select v-model="searchParams.ccNo" placeholder="Select" style="width: 240px" @change="handleChangeCC">
<el-option v-for="item in ccList" :key="item.costCenterNo" :label="item.costCenterDesc"
:value="item.costCenterNo" />
</el-select>
</div>
<div>
<span class="tw-mr-1">年份</span>
<el-date-picker v-model="searchParams.year" type="year" placeholder="选择年份" @change="handleChangeYear" />
</div>
</div>
<div>
<span class="tw-mr-2">{{ selectedFileName }}</span>
<el-button @click="handleReFill">重新填报</el-button>
<el-button @click="handleFillAndJson">{{ mode === 'fill' ? '内容' : '值' }}填充</el-button>
<el-button @click="handleExportExcel" type="primary">导出excel</el-button>
</div>
</div>
<lucky-sheet ref="luckysheetRef" :options="options" :workbook="workbook" :onHooks="handlehooks" />
</div>
</template>
<script setup>
import { onMounted, reactive, onUnmounted, ref, toRaw } from "vue";
import { options } from "@/utils/fillConfig.js";
import { getAuth, updateTemplate, selectCCBudget, updateFunctionalBudget, selectTemplate, deleteCCBudget, getUserInfo, getLeaderCCList, getSelectData } from "@/api/api.js";
import axios from "axios";
import { globalName } from "../../utils/global";
import { exportExcel } from "@/utils/common.js";
import LuckySheet from '@/components/LuckySheet.vue';
import * as XLSX from 'xlsx';
import { ElMessage } from "element-plus";
// 具体数据(假数据)
// const realData = reactive({
// info: { name: "6、职能部门费用预算表-部门名称1.xlsx" },
// sheets: [
// {
// ccNo: '0101', // CC编号 唯一值 (后台预设,后续可通过具体需求拿到该值,返回给前端)
// name: "CC",
// order: "0",
// index: "28",
// allowRangeList: ["$G$2:$H$55", "$G$57:$H$74", "$P$2:$AA$55", "$P$57:$AA$74"], //可编辑区(后台预设,后续根据需求,确定具体模板然后赋值)
// frozen: { // 冻结配置
// range: {
// column_focus: 3,
// row_focus: 0,
// },
// type: 'rangeBoth'
// },
// cells: [ // 可编辑区的单元格数据 无填报过时,cells:[]
// {
// row: 1,
// col: 16,
// val: 23
// }, {
// row: 1,
// col: 17,
// val: 24
// }
// ]
// }
// ]
// })
const realData = reactive({
info: {},
sheets: []
});
const selectedFileName = ref(''); // 选择文件的名称
let workbook = reactive({ //工作簿
info: {},
sheets: []
});
const mode = ref('fill'); // 模式: 'fill' 值填充(模板和数据) / 'default' 内容填充 json
//创建一个ref,来引用子组件
const luckysheetRef = ref(null);
//登录用户信息
const userInfo = ref(null);
const ccList = reactive([]);
// 查询参数
const searchParams = reactive({
ccNo: '',
year: new Date()
});
const isInitLoad = ref(false);
const count = ref(0);
const realSheetCellLength = ref(0);
onMounted(() => {
console.log('onMounted')
getUser();
});
onUnmounted(() => {
exitEditMode();
});
// hooks
const handlehooks = () => {
return {
updated: handleUpdate,
// cellUpdated: handleCellUpdated,
workbookCreateAfter: handleWorkbookCreateAfter, // 表格创建之后触发
}
}
const handleWorkbookCreateAfter = () => {
console.log('mode:', mode.value);
if (mode.value === 'fill') {
isInitLoad.value = true;
// 方案 模板 + 数据整合
handleIntegration();
}
}
// 内容填充和值填充切换
const handleFillAndJson = () => {
mode.value = mode.value === 'fill' ? 'default' : 'fill';
getUser();
}
// 获取登录人信息
const getUser = () => {
getUserInfo().then(res => {
if (res.success) {
userInfo.value = res.entity;
getCCList();
}
})
};
// 获取cc列表
const getCCList = () => {
const params = {
empNo: userInfo.value.code
}
getLeaderCCList(params).then(res => {
if (res.success) {
Object.assign(ccList, res.entity);
searchParams.ccNo = res.entity[0].costCenterNo;
init();
}
})
}
const init = async () => {
if (mode.value === 'default') {
getFillJson();
} else if (mode.value === 'fill') {
await getFillData();
handleTemplateJson();
}
}
//选择cc
const handleChangeCC = (val) => {
searchParams.ccNo = val;
init();
}
// 选择年份
const handleChangeYear = (val) => {
searchParams.year = val;
init();
}
// 获取实际填充数据
const getFillData = async () => {
const params = {
ccNo: searchParams.ccNo,
year: searchParams.year.getFullYear(),
}
await getSelectData(params).then(res => {
if (res.success) {
realData.sheets = res.entity;
}
})
}
// 重新填报 (有模板,无数据)
const handleReFill = () => {
ElMessageBox.confirm(
'重新填报,将废弃所有填报历史数据,是否继续?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
const params = {
ccNo: searchParams.ccNo,
year: searchParams.year.getFullYear()
}
deleteCCBudget(params).then(res => {
if (res.success) {
ElMessage({
type: 'info',
message: '历史数据已删除,请重新填报'
})
mode.value = 'default';
handleTemplateJson();
}
})
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
// 获取模板json
const handleTemplateJson = () => {
// 本地文件模拟数据
// axios.get(`/${globalName}/json/template.json`).then((res) => {
// if (res.status === 200) {
// const result = res.data;
// //假设这是后台数据库中的数据,要修改该数据库的数据,使用深拷贝,两者才能没有关系,用来验证修改单个单元格时,是否会有数据问题。
// Object.assign(workbook, JSON.parse(JSON.stringify(result)));
// console.log('result', result);
// selectedFileName.value = result.info.name;
// callChildInit();
// }
// });
const params = {
ccNo: searchParams.ccNo,
year: searchParams.year.getFullYear()
}
selectTemplate(params).then(res => {
if (res.success) {
Object.assign(workbook, res.entity);
selectedFileName.value = res.entity.info.name;
callChildInit();
}
})
}
// 整合数据 (模板与具体数据的整合)
const handleIntegration = () => {
realData.sheets.forEach((realSheet, idx) => {
workbook.sheets.forEach((sheet, sheetIdx) => {
console.log('realSheetIndex:', realSheet.sheetIndex, 'sheetIndex:', sheet.index);
if (realSheet.sheetIndex.toString() === sheet.index.toString()) {
// 设置数据验证
// handleDataVerification(sheet);
// 设置可编辑区
handleAllowRangeList(idx, realSheet);
// 设置冻结
handleFrozen(idx, realSheet);
// 单元格数据填充
handleCellFill(realSheet, sheet);
}
})
})
}
//单元格数据填充到模板
const handleCellFill = (realSheet, sheet) => {
// eg: luckysheet.setCellValue(1, 16, 89, { order: 0 });
realSheet.cells.forEach(cell => {
// 这个会触发updated方法,导致调用接口。 知道数据结构的暂不用这种方式 start
// console.log(luckysheet, 23232323)
luckysheet.setCellValue(cell.row, cell.col, cell.cellVal, { order: realSheet.order });
// 这个会触发updated方法,导致调用接口。 知道数据结构的暂不用这种方式 end
// sheet.data[cell.row][cell.col].v = cell.cellVal;
})
realSheetCellLength.value = realSheet.cells.length;
}
// 设置允许编辑区域
const handleAllowRangeList = (idx, realSheet) => {
// 可编辑区集合 start
let rangeList = [];
realSheet.allowRangeList.forEach(range => {
const curRange = {
"name": "Default0", //名称
"password": "", //密码
"hintText": "", //提示文字
"algorithmName": "None", //加密方案:MD2,MD4,MD5,RIPEMD-128,RIPEMD-160,SHA-1,SHA-256,SHA-384,SHA-512,WHIRLPOOL
"saltValue": null, //密码解密的盐参数,为一个自己定的随机数值
"checkRangePasswordUrl": null,
sqref: range //区域范围
};
rangeList.push(curRange);
})
// 可编辑区集合 end
workbook.sheets[idx].config = {
...workbook.sheets[idx].config,
authority: {
"password": "",
"algorithmName": "None",
"saltValue": null,
"hintText": "",
"sheet": 1,
"selectLockedCells": 1,
"selectunLockedCells": 1,
"formatCells": 0,
"formatColumns": 0,
"formatRows": 0,
"insertColumns": 0,
"insertRows": 0,
"insertHyperlinks": 0,
"deleteColumns": 0,
"deleteRows": 0,
"sort": 0,
"filter": 0,
"usePivotTablereports": 0,
"editObjects": 0,
"editScenarios": 0,
"allowRangeList": rangeList
},
}
}
// 设置冻结配置
const handleFrozen = (idx, realSheet) => {
workbook.sheets[idx].frozen = realSheet.frozen;
}
//设置数据验证 (暂时废弃。数据验证交予后台接口处理了)
const handleDataVerification = (sheet) => {
luckysheet.setDataVerification({
type: "number",
type2: "bw",
value1: "0",
value2: "10000",
checked: false, //是否勾选中复选框;type为checkbox时需配置;
remote: false, //自动远程获取选项
prohibitInput: true, //输入数据无效时禁止输入;默认为false;
hintShow: true, //选中单元格时显示提示语;默认为false;
hintText: "请输入数字:0-10000", //提示语文本,hintShow为true时需配置;
}, {
range: "G2:H55",
order: sheet.order,
})
luckysheet.setDataVerification({
type: "number",
type2: "bw",
value1: "0",
value2: "10000",
checked: false, //是否勾选中复选框;type为checkbox时需配置;
remote: false, //自动远程获取选项
prohibitInput: true, //输入数据无效时禁止输入;默认为false;
hintShow: true, //选中单元格时显示提示语;默认为false;
hintText: "请输入数字:0-10000", //提示语文本,hintShow为true时需配置;
}, {
range: "G57:H74",
order: sheet.order,
})
luckysheet.setDataVerification({
type: "number",
type2: "bw",
value1: "0",
value2: "10000",
checked: false, //是否勾选中复选框;type为checkbox时需配置;
remote: false, //自动远程获取选项
prohibitInput: true, //输入数据无效时禁止输入;默认为false;
hintShow: true, //选中单元格时显示提示语;默认为false;
hintText: "请输入数字:0-10000", //提示语文本,hintShow为true时需配置;
}, {
range: "P2:AA55",
order: sheet.order,
})
luckysheet.setDataVerification({
type: "number",
type2: "bw",
value1: "0",
value2: "10000",
checked: false, //是否勾选中复选框;type为checkbox时需配置;
remote: false, //自动远程获取选项
prohibitInput: true, //输入数据无效时禁止输入;默认为false;
hintShow: true, //选中单元格时显示提示语;默认为false;
hintText: "请输入数字:0-10000", //提示语文本,hintShow为true时需配置;
}, {
range: "P57:AA74",
order: sheet.order,
})
};
// 获取填报excel 获取public文件夹下指定json文件内容
const getFillJson = () => {
// 模拟数据
// axios.get(`/${globalName}/json/luckysheet.json`).then((res) => {
// if (res.status === 200) {
// const result = res.data;
// //假设这是后台数据库中的数据,要修改该数据库的数据,使用深拷贝,两者才能没有关系,用来验证修改单个单元格时,是否会有数据问题。
// Object.assign(workbook, JSON.parse(JSON.stringify(result)));
// console.log('result', result);
// selectedFileName.value = result.info.name;
// callChildInit();
// }
// });
const params = {
ccNo: searchParams.ccNo,
year: searchParams.year.getFullYear()
}
selectCCBudget(params).then(res => {
if (res.success) {
Object.assign(workbook, res.entity);
selectedFileName.value = res.entity.info.name;
callChildInit();
}
})
};
// 调用子组件的初始化
const callChildInit = async (option) => {
if (luckysheetRef.value) {
await luckysheetRef.value.handleInit(option);
}
}
// 整个工作薄数据保存
const handleUpdateByWorkBook = async () => {
let allSheets = handleFrozenMis();
// 去除临时数据,减小体积
for (const i in allSheets) {
allSheets[i].row = allSheets[i].data.length; //行数
allSheets[i].column = allSheets[i].data[0].length; //列数
// allSheets[i].data = undefined;
}
workbook.sheets = allSheets;
await autoSave(workbook);
}
// 以单个表的方式进行数据保存
const hanldeUpdateBySheet = async (operate) => {
console.log("filling页面:绑定更新处理函数", operate);
const curIndex = workbook.sheets.findIndex(sheet => sheet.index.toString() === operate.sheetIndex.toString());
workbook.sheets[curIndex].data = operate.curdata;
await autoSave(workbook);
}
/** 单元格数据保存 考虑到该页面,模板已固定,只填数据。只更新修改的数据。 */
const handleUpdate = async (operate) => {
console.log("filling页面:绑定更新处理函数", operate);
// 做个监测,防止初始化时调用updated的hook引起的接口调用 start
count.value++;
console.log('count:', count.value, 'realSheetCellLength.length:', realSheetCellLength.value)
if (count.value <= realSheetCellLength.value) {
isInitLoad.value = true;
} else {
isInitLoad.value = false;
}
// 做个监测,防止初始化时调用updated的hook引起的接口调用 start
if (!isInitLoad.value) {
const curData = workbook.sheets.find(sheet => sheet.index.toString() === operate.sheetIndex.toString());
const curIndex = workbook.sheets.findIndex(sheet => sheet.index.toString() === operate.sheetIndex.toString());
const { range, curdata, curConfig, type } = operate;
if (type === "datachange") {
const { column, row } = range[0];
/** row:[6,9] column:[8,11]
* [6,8],[6,9],[6,10],[6,11]
* [7,8],[7,9],[7,10],[7,11]
* [8,8],[8,9],[8,10],[8,11]
*/
// 年份、CCNO、预算小类编号、月份、金额
const cells = [];
for (let i = row[0]; i <= row[1]; i++) {
for (let j = column[0]; j <= column[1]; j++) {
const val = luckysheet.getCellValue(i, j, { type: "v", order: curData.order }); //修改的当前单元格具体值
const detailCode = luckysheet.getCellValue(i, 4, { type: 'v', order: curData.order }); //预算小类编号
cells.push({
row: i, //行号
col: j, //列号
cellJsonData: operate.curdata[i][j], // 工作表数据变更后的单元格内容
cellVal: val,
detailCode: detailCode, //预算小类编号
})
}
}
//组合数据
const changes = {
sheetIndex: curData.index, // sheet页唯一id
sheetOrder: curData.order, // 排序情况,即下标
ccNo: searchParams.ccNo, // CC 编号
year: searchParams.year.getFullYear(), //年份
cells: cells,//变更单元格集合
};
console.log('changes变更数据:', changes);
// 模拟后端接口修改数据 临时处理
// changes.cells.forEach(cell => {
// workbook.sheets[changes.sheetOrder].data[cell.row][cell.col] = cell.cellJsonData
// })
updateFunctionalBudget(changes).then(res => {
if (res.success) {
} else {
// 模拟错误单元格数据,清空单元格
const tempData = [
{
row: 1,
col: 16,
msg: '不是数字'
}, {
row: 1,
col: 17,
msg: '不是数字'
}, {
row: 1,
col: 18,
msg: '不是数字'
}
]
tempData.forEach(cell => {
luckysheet.clearCell(cell.row, cell.col);
})
}
})
}
// await autoSave(workbook);
}
};
// 自动保存
const autoSave = async (workbook) => {
const newWorkbook = toRaw(workbook); // toRaw获取响应式对象的原始版本
console.log('newWorkbook', newWorkbook);
let params = {
jsonData: JSON.stringify(newWorkbook),
targetFilePath:
process.env.NODE_ENV === "development"
? "D://hhkj_project//mis//function-budget//public//json//luckysheet.json"
: "D://apache-tomcat-7.0.64-8080-mis//webapps//functionalBudget//json//luckysheet.json",
};
await updateTemplate(params).then((res) => {
if (res.success) {
// $(luckysheet_info_detail_save).text("已保存");
ElMessage({
message: "填报数据保存成功",
type: "success",
});
}
});
};
//导出
const handleExportExcel = () => {
exportExcel();
};
const handleCellUpdated = (r, c, oldValue, newValue, isRefresh) => {
console.log("cellUpdated:", r, c, oldValue, newValue);
// 获取单元格的值
const value = luckysheet.getCellValue(r, c, { type: "v" });
console.log("更新后的值:", value, "对值进行数据验证");
};
/**
* 协同
* loadUrl:
* $.post(loadurl,{"gridKey":server.gridKey},function(d){})
*/
</script>
<!-- scss配置已生效 -->
<style lang="scss" scoped></style>
2.3 模板页面:配置文件
// src/utils/templateConfig.js
import { saveAs } from "file-saver";
/** template 模板 start */
/** 工作表数据及配置 start */
export const sheetData = [
{
name: "CC", //工作表名称
color: "", //工作表颜色,工作表名称下方会有一条底部边框
index: 0, //工作表索引,作为唯一key值使用,不是工作表顺序,和order区分开。
status: 1, //激活状态,仅有一个激活状态的工作表,其它工作表为0
order: 0, //工作表的下标
hide: 0, //是否隐藏 0不隐藏 1隐藏
row: 36, //行数
column: 18, //列数
celldata: [],
config: {},
},
];
/** 工作表数据及配置 end */
/** 整体配置 start */
//配置项 作用于整个表格。特别的,单个sheet的配置项需要在options.data数组中
export const options = {
container: "luckysheet", // 设定DOM容器的id:luckysheet为容器id
title: "Luckysheet Demo", // 设置表格名称
lang: "zh", // 设定表格语言
// 更多其它设置...
data: sheetData,
column: 18, // 空表格默认的列数量
row: 36, //空表格默认的行数据量
showtoolbar: true, //是否显示工具栏
showtoolbarConfig: {
//自定义配置工具栏
image: false, //插入图片
link: false, //插入链接
chart: false, //图表(图标隐藏,但是如果配置了chart插件,右击仍然可以新建图表)
postil: false, //批注
pivotTable: false, //数据透视表
sortAndFilter: false, //排序和筛选
conditionalFormat: false, //条件格式
dataVerification: false, //数据验证
splitColumn: false, //分列
screenshot: false, //截图
findAndReplace: false, //查找替换
protection: false, // 工作表保护
print: false, //打印
},
showinfobar: false, //是否显示顶部信息栏
showsheetbar: true, //是否显示底部sheet页按钮
showsheetbarConfig: {
//自定义配置底部sheet页按钮
add: false, //新增sheet
menu: false, //sheet管理菜单
sheet: true, //sheet页显示
},
showstatisticBar: false, //是否显示底部计数栏
showstatisticBarConfig: {
//自定义配置底部计数栏
count: true, //计数栏
view: false, //打印视图
zoom: true, //缩放
},
enableAddRow: false, //允许添加行
enableAddBackTop: false, //允许回到顶部
userInfo: false, //右上角的用户信息展示
userMenuItem: [], //点击右上角的用户信息弹出的菜单
myFolderUrl: "", //左上角 < 返回按钮的链接
functionButton: "", //右上角功能按钮
cellRightClickConfig: {
//自定义配置单元格右击菜单
copy: true, //复制
copyAs: false, //复制为
paste: true, //粘贴
insertRow: false, //插入行
insertColumn: false, //插入列
deleteRow: false, //删除选中行
deleteColumn: false, //删除选中列
deleteCell: false, //删除单元格
hideRow: false, //隐藏选中行和显示选中行
hideColumn: false, //隐藏选中列和显示选中列
rowHeight: true, //行高
columnWidth: true, //列宽
clear: false, //清除内容
matrix: false, //矩阵操作选区
sort: false, //排序选区
filter: false, //筛选选区
chart: false, //图表生成
image: false, //插入图片
link: false, //插入链接
data: true, //数据验证
cellFormat: true, //设置单元格格式
},
sheetRightClickConfig: {
//自定义配置sheet页右击菜单
delete: false, //删除
copy: false, //复制
rename: false, //重命名
color: false, //更改颜色
hide: false, //隐藏,取消隐藏
move: false, //向左移,向右移
},
rowHeaderWidth: 46, //行标题区域的宽度,如果设置为0,则表示隐藏行标题
columnHeaderHeight: 20, //列标题区域的高度,如果设置为0,则表示隐藏列标题
sheetFormulaBar: true, //是否显示公示栏
defaultFontSize: 11, //初始化默认字体大小
limitSheetNameLength: true, //工作表重命名等场景下是否限制工作表名称的长度
defaultSheetNameMaxLength: 31, //默认允许的工作表名最大长度
// pager: { //分页器按钮设置
// pageIndex: 1, //当前的页码
// pageSize: 10, //每页显示多少行数据
// total: 50, //数据总行数
// selectOption: [10, 20], //允许设置每页行数的选项
// },
hook: {
//钩子函数
/** 单元格 start */
/**进入单元格编辑模式之前触发。在选中了某个单元格且在非编辑状态下,通常有以下三种常规方法触发进入编辑模式 1.双击单元格 2.敲Enter键 3.使用API:enterEditMode
* 参数:{ Array }[range]: 当前选区范围
*/
cellEditBefore: (range) => {
console.log("进入单元格编辑模式之前触发:cellEditBefore");
console.log(`当前选区范围:`, range);
},
/**更新这个单元格值之前触发,return false 则不执行后续的更新。在编辑状态下修改了单元格之后,退出编辑模式并进行数据更新之前触发这个钩子
* 参数:
* {Number}[r]:单元格所在行数
* {Number}[c]:单元格所在列数
* {Object | String | Number}[value]:要修改的单元格内容
* {Boolean}[isRefresh]:是否刷新整个表格
*/
cellUpdateBefore: (r, c, value, isRefresh) => {
console.log("更新这个单元格值之前触发:cellUpdateBefore");
console.log(
`单元格所在行:${r};单元格所在列:${c};是否刷新整个表格:${isRefresh};要修改的单元格内容: `,
value
);
},
/** 更新这个单元格后触发 */
cellUpdated: (r, c, oldValue, newValue, isRefresh) => {
console.log("cellUpdated:", r, c, oldValue, newValue);
// 获取单元格的值
const value = luckysheet.getCellValue(r, c, { type: "v" });
console.log("更新后的值:", value, "对值进行数据验证");
},
/**单元格渲染前触发 return false 则不渲染该单元格
* 参数:
* {Object}[cell]:单元格对象
* {Object}[position]:
* {Number}[r]:单元格所在行号
* {Number}[c]:单元格所在列号
* {Number}[start_r]:单元格左上角的水平坐标
* {Number}[start_c]:单元格左上角的垂直坐标
* {Number}[end_r]:单元格右下角的水平坐标
* {Number}[end_c]:单元格右下角的垂直坐标
* {Object}[sheet]:当前sheet对象
* {Object}[ctx]:当前画布的context
*/
cellRenderBefore: (cell, position, sheet, ctx) => {
// console.log('单元格渲染前触发:cellRenderBefore');
// console.log('单元格对象:', cell, '单元格所在行号:', position.r, '单元格所在列号:', position.c, '单元格左上角的水平坐标:',
// position.start_r, '单元格左上角的垂直坐标:', position.start_c, '单元格右下角的水平坐标:', position.end_r, '单元格右下角的垂直坐标:', position.end_c,
// '当前sheet对象:', sheet, '当前画布的context:', ctx);
},
/**单元格渲染结束后触发,return false 则不渲染该单元格
* 参数:
* {Object} [cell]:单元格对象
* {Object} [position]:
* {Number} [r]:单元格所在行号
* {Number} [c]:单元格所在列号
* {Number} [start_r]:单元格左上角的水平坐标
* {Number} [start_c]:单元格左上角的垂直坐标
* {Number} [end_r]:单元格右下角的水平坐标
* {Number} [end_c]:单元格右下角的垂直坐标
* {Object} [sheet]:当前sheet对象
* {Object} [ctx]:当前画布的context
*
*/
cellRenderAfter: () => {},
/**所有单元格渲染之前执行的方法
* 参数:
* {Object}[data]:当前工作表二维数组数据
* {Object}[sheet]:当前sheet对象
* {Object}[ctx]:当前画布的context
*/
cellAllRenderBefore: (data, sheet, ctx) => {
// console.log('当前工作表二维数组数据:', data);
},
/**行标题单元格渲染前触发,return false 则不渲染行标题
* 参数:
* {String}[rowNum]:行号
* {Object}[position]:
* {Number}[r]:单元格所在行号
* {Number}[top]:单元格左上角的垂直坐标
* {Number}[width]:单元格宽度
* {Number}[height]:单元格高度
* {Object}[ctx]:当前画布的context
*/
rowTitleCellRenderBefore: () => {},
/** 行标题单元格渲染后触发 return false 则不渲染行标题*/
rowTitleCellRenderAfter: () => {},
/** 列标题单元格渲染前触发 return false 则不渲染列标题 */
columnTitleCellRenderBefore: () => {},
/** 列标题单元格渲染后触发,return false 则不渲染列标题 */
columnTitleCellRenderAfter: () => {},
/** 单元格 end */
/** 鼠标钩子 start */
/** 单元格点击前的事件,return false 则终止之后的点击操作 */
cellMousedownBefore: (cell, position, sheet, ctx) => {},
/** 单元格点击后的事件,return false 则终止之后的点击操作 */
cellMousedownAfter: (cell, position, sheet, ctx) => {},
/** 鼠标移动事件,可通过cell判断鼠标停留在哪个单元格 */
sheetMousemove: () => {},
/** 鼠标按钮释放事件,可通过cell判断鼠标停留在哪个单元格 */
sheetMouseup: () => {},
/** 鼠标滚动事件 */
scroll: (position) => {},
/** 鼠标拖拽文件到Luckysheet内部的结束事件 */
cellDragStop: () => {},
/** 鼠标钩子 end */
/** 选区操作 (包括单元格) start */
/** 框选或者设置选区后触发 参数:{Object}[sheet]:当前选区对象 {Object | Array}[range]:选区范围,可能为多个选区 */
rangeSelect: (sheet, range) => {
console.log("选区范围:", range);
},
/** 移动选区前,包括单个单元格 */
rangeMoveBefore: (range) => {},
/** 移动选区后,包括单个单元格 */
rangeMoveAfter: (range) => {},
/** 选区修改前 */
rangeEditBefore: (range, data) => {},
/** 选区修改后 */
rangeEditAfter: (range, oldData, newData) => {},
/** 选区复制前 */
rangeCopyBefore: (range, data) => {},
/** 选区复制后 */
rangeCopyAfter: (range, data) => {},
/** 选区粘贴前 */
rangePasteBefore: (range, data) => {},
/** 选区粘贴后 */
rangePasteAfter: (range, originData, pasteData) => {},
/** 选区剪切前 */
rangeCutBefore: (range, data) => {},
/** 选区剪切后 */
rangeCutAfter: (range, data) => {},
/** 选区删除前 */
rangeDeleteBefore: (range, data) => {},
/** 选区删除后 */
rangeDeleteAfter: (range, data) => {},
/** 选区清除前 */
rangeClearBefore: (range, data) => {},
/** 选区清除后 */
rangeClearAfter: (range, data) => {},
/** 选区下拉前 */
rangePullBefore: (range) => {},
/** 选区下拉后 */
rangePullAfter: (range) => {},
/** 选区操作 (包括单元格) end */
/** 工作表 start */
/** 创建sheet页前触发,sheet页新建也包含数据透视表新建 */
sheetCreatekBefore: () => {},
/** 创建sheet页后触发,sheet页新建也包含数据透视表新建 */
sheetCreateAfter: (sheet) => {},
/** sheet移动前 */
sheetMoveBefore: (i, order) => {},
/** sheet移动后 */
sheetMoveAfter: (i, oldOrder, newOrder) => {},
/** sheet删除前 */
sheetDeleteBefore: (sheet) => {},
/** sheet删除后 */
sheetDeleteAfter: (sheet) => {},
/** sheet修改名称前 */
sheetEditNameBefore: (i, name) => {},
/** sheet修改名称后 */
sheetEditNameAfter: (i, oldName, newName) => {},
/** sheet修改颜色前 */
sheetEditColorBefore: (i, color) => {},
/** sheet修改颜色后 */
sheetEditColorAfter: (i, oldColor, newColor) => {},
/** sheet缩放前 */
sheetZoomBefore: (i, zoom) => {},
/** sheet缩放后 */
sheetZoomAfter: (i, oldZoom, newZoom) => {},
/** 激活工作表前 */
sheetActivate: (i, isPivotInitial, isNewSheet) => {},
/** 工作表从活动状态转为非活动状态前 */
sheetDeactivateBefore: (i) => {},
/** 工作表从活动状态转为非活动状态后 */
sheetDeactivateAfter: (i) => {},
/** 工作表 end */
/** 工作薄 start */
/** 表格创建之前触发 */
workbookCreateBefore: (book) => {},
/** 表格创建之后触发 */
workbookCreateAfter: (book) => {},
/** 表格销毁之前触发 */
workbookDestroyBefore: (book) => {},
/** 表格销毁之后触发 */
workbookDestroyAfter: (book) => {},
/** 协同编辑中的每次操作后执行的方法,监听表格内容变化,即客户端每执行一次表格操作,Luckysheet将这次操作存到历史记录中后触发,撤销重做时因为也算一次操作,也会触发此钩子函数 参数:{Object}[operate]:本次操作的历史记录信息,根据不同的操作,会有不同的历史记录 */
updated: (operate) => {},
/** resize 执行之后 */
resized: (size) => {},
/** 工作薄 end */
/** 冻结 start */
/** 设置冻结前 */
frozenCreateBefore: (frozen) => {
console.log("frozenCreateBefore", frozen);
},
/** 设置冻结后 */
frozenCreateAfter: (frozen) => {
console.log("frozenCreateAfter", frozen);
},
/** 取消冻结前 */
frozenCancelBefore: (frozen) => {
console.log("frozenCancelBefore", frozen);
},
/** 取消冻结后 */
frozenCancelAfter: (frozen) => {
console.log("frozenCancelAfter", frozen);
},
/** 冻结 end */
/** 分页器 start */
/** 点击分页按钮回调函数,返回当前页码 */
onTogglePager: (page) => {},
/** 分页器 end */
},
/** 协同编辑 start */
// loadUrl: gridKey:工作簿的唯一标识, 加载luckysheet数据的地址
// loadUrl: 'http://localhost:8081/getWorkBook?gridKey=1',
// loadUrl: "/baseURL/excel",
// updateUrl: '', //后台websocket地址
allowUpdate: false,
/** 协同编辑 end */
};
/** 整体配置 end */
/** template 模板 end */
2.4. 模板页面:页面使用
//luckysheet/template.vue
<!-- 模板填写 -->
<template>
<div class="tw-w-full tw-h-full tw-flex tw-flex-col">
<div class="tw-pr-2 tw-pl-2 tw-pb-2 tw-box-border tw-absolute tw-top-2 tw-left-0 tw-w-full tw-flex tw-justify-between tw-items-center"
@click="exitEditMode">
<div class="tw-flex tw-justify-between tw-items-center">
<div>
<span class="tw-mr-1">年份</span>
<el-date-picker v-model="searchParams.year" type="year" placeholder="选择年份"
@change="handleChangeYear" />
</div>
</div>
<div>
<input type="file" class="tw-hidden tw-text-xs" ref="fileInput" @change="handleFileChange"
accept=".xlsx,.xls" />
<span v-if="selectedFileName" class="tw-mr-2">{{ selectedFileName }}</span>
<el-button @click="chooseFile">选择文件</el-button>
<el-button @click="handleAbandon">模拟无模板</el-button>
<el-button @click="handleSubmitTemplate" type="primary">提交模板</el-button>
</div>
</div>
<lucky-sheet ref="luckysheetRef" :options="options" :workbook="workbook" :onSave="autoSave" />
</div>
</template>
<script setup>
import LuckyExcel from "luckyexcel";
import { onMounted, reactive, onUnmounted, ref, toRaw } from "vue";
import { saveAs } from "file-saver";
import { options, sheetData } from "@/utils/templateConfig.js";
import axios from "axios";
import { globalName } from "@/utils/global";
import { getAuth, updateTemplate, saveTemplate, selectTemplate } from "@/api/api.js";
import { ElMessage } from "element-plus";
import LuckySheet from '@/components/LuckySheet.vue';
const fileInput = ref(null);
const selectedFileName = ref('');
let workbook = reactive({
info: {},
sheets: []
});
// 查询参数
const searchParams = reactive({
year: new Date()
})
//创建一个ref,来引用子组件
const luckysheetRef = ref(null);
const isHasTemplate = ref(true);
onMounted(() => {
// getTemplateJson();
getTemplate();
});
onUnmounted(() => {
exitEditMode();
});
// 获取模板文件 获取public文件夹下指定json文件内容 (本地文件数据模拟)
const getTemplateJson = () => {
axios.get(`/${globalName}/json/template.json`).then((res) => {
if (res.status === 200) {
if (res.data?.info) {
Object.assign(workbook, res.data);
selectedFileName.value = res.data.info.name;
callChildInit();
} else {
selectedFileName.value = '职能预算模板'
Object.assign(workbook, {
info: {
name: '职能预算模板',
},
sheets: []
});
callChildInit({
...options,
data: [{
name: "CC", //工作表名称
color: "", //工作表颜色,工作表名称下方会有一条底部边框
index: 0, //工作表索引,作为唯一key值使用,不是工作表顺序,和order区分开。
status: 1, //激活状态,仅有一个激活状态的工作表,其它工作表为0
order: 0, //工作表的下标
hide: 0, //是否隐藏 0不隐藏 1隐藏
row: 36, //行数
column: 18, //列数
defaultRowHeight: 19, //自定义行高
defaultColWidth: 73, //自定义列宽
celldata: [],
}],
title: '职能预算模板',
});
}
}
});
};
// 修改年份
const handleChangeYear = (val) => {
searchParams.year = val;
getTemplate();
}
// 废弃模板(模拟无模板操作)
const handleAbandon = () => {
isHasTemplate.value = false;
getTemplate();
}
// 查询模板 (接口)
const getTemplate = () => {
const params = {
//当year年份为null或空时,视为未上传模版,返回数据为空
year: isHasTemplate.value ? searchParams.year.getFullYear() : null
}
selectTemplate(params).then(res => {
if (res.success) {
if (res.entity.info) { //有模板
Object.assign(workbook, res.entity);
selectedFileName.value = res.entity.info.name;
callChildInit();
} else {
ElMessage.info({
type: 'info',
message: '未上传模版,请先选择并上传模版'
})
selectedFileName.value = 'CC模板'
Object.assign(workbook, {
info: {
name: 'CC模板',
},
sheets: sheetData || [],
});
callChildInit({
...options,
});
}
}
})
}
// 调用子组件的初始化
const callChildInit = (option) => {
if (luckysheetRef.value) {
luckysheetRef.value.handleInit(option);
}
}
// 选择文件 将excel文件转为luckysheet的数据
const handleFileChange = (event) => {
const file = event.target.files[0];
LuckyExcel.transformExcelToLucky(
file,
(exportJson, luckysheetfile) => {
// 转换后获取工作表数据
console.log("excel转换为luckysheet的数据:", exportJson);
// workbook = exportJson;
Object.assign(workbook, exportJson);
// 销毁原来的表格
luckysheet.destroy();
callChildInit({
...options,
data: exportJson.sheets,
title: exportJson.info.name,
});
selectedFileName.value = file.name;
resetFileInput();
},
(error) => {
// 如果抛出任何错误,则处理错误
console.log(error);
}
);
};
/** 解决 input type='file' 的change事件,文件名称相同时,不能重新上传问题(浏览器安全机制决定) start */
const chooseFile = () => {
//触发隐藏的文件输入框
fileInput.value.click();
}
const resetFileInput = () => {
//模拟点击事件来重置文件输入的状态
fileInput.value.value = '';
}
/** 解决 input type='file' 的change事件,文件名称相同时,不能重新上传问题(浏览器安全机制决定) end */
// 自动保存 (数据模拟)
const autoSave = (workbook) => {
const newWorkbook = toRaw(workbook); // toRaw获取响应式对象的原始版本
console.log('newWorkbook', newWorkbook);
let params = {
jsonData: JSON.stringify(newWorkbook),
targetFilePath:
process.env.NODE_ENV === "development"
? "D://hhkj_project//mis//function-budget//public//json//template.json"
: "D://apache-tomcat-7.0.64-8080-mis//webapps//functionalBudget//json//template.json",
};
updateTemplate(params).then((res) => {
if (res.success) {
// $(luckysheet_info_detail_save).text("已保存");
ElMessage({
message: "模板数据保存成功",
type: "success",
});
}
});
};
// 提交模板
const handleSubmitTemplate = () => {
// 调用子组件整理数据方法
// luckysheetRef.value.handleData();
const newWorkbook = toRaw(workbook); // toRaw获取响应式对象的原始版本
const params = {
jsonData: JSON.stringify(newWorkbook),
targetFilePath: '',
year: searchParams.year.getFullYear()
}
saveTemplate(params).then(res => {
if (res.success) {
ElMessage({
message: "提交模板成功",
type: "success",
});
}
})
};
/**
* 如何解决冻结行列区不准的问题
* 1. 改源码 - 耗时且不准
* 2. 假设在填写模板时,去掉设置冻结的操作
* 在填写数据页面,把设置冻结的操作放开。然后供填写时设置方便。
* 提交时,去除冻结的配置。以防止回显时,冻结列现实错乱的问题。
*/
/**
* 综合考虑:
* 模板页面不需要自动保存功能,因为会涉及到工具栏的操作。
* 目前没有采用实时官方提供的loadurl,updateurl,allowupdate的这种方式去更新,需要用到websocket,后台数据难存储,所以,采用普通的方式保存
*/
</script>
<!-- scss配置已生效 -->
<style lang="scss" scoped></style>
2.5. 公共组件 luckysheet
// src/components/luckysheet.vue
<!-- 模板填写 -->
<template>
<div id="luckysheet" class="tw-m-0 tw-p-0 tw-absolute tw-w-full tw-left-0 tw-top-12"
v-click-outside="handleClickOutSide"></div>
</template>
<script setup>
import LuckyExcel from "luckyexcel";
import { onMounted, reactive, onUnmounted, ref, toRaw, watch } from "vue";
import { saveAs } from "file-saver";
const props = defineProps({
options: {
type: Object,
default: () => {
return {}
}
},
workbook: {
type: Object,
default: () => {
return {
info: {},
sheets: []
}
}
},
onSave: {
type: Function,
default: () => () => { } //默认值定义为一个空函数
},
onUpdate: {
type: Function,
default: () => () => { }//默认值定义为一个空函数
},
onCellUpdated: {
type: Function,
default: () => () => { }
},
onHooks: {
type: Function,
default: () => () => { }
}
})
let { options, workbook, onSave, onUpdate, onCellUpdated, onHooks } = props;
onMounted(() => { })
onUnmounted(() => {
exitEditMode();
});
const handleInit = async (option) => {
await createLuckysheet(option);
init();
}
// 创建luckysheet
const createLuckysheet = async (option) => {
let temp = {};
if (option) {
temp = { ...option };
} else {
temp = {
...options,
data: workbook.sheets,
title: workbook.info.name,
}
}
console.log('workbook.sheets', workbook.sheets)
await luckysheet.create({
...temp,
hook: {
/** 冻结相关的hook 触发不生效 。。。。 start */
// frozenCreateAfter: (frozen) => { },
// frozenCancelBefore: (frozen) => { },
// /** 冻结相关的hook 触发不生效 。。。。 end */
// //激活工作表前
// sheetActivate: (i, isPivotInitial, isNewSheet) => {
// console.log('sheetActivate:', i, isPivotInitial, isNewSheet);
// },
// //工作表从活动状态转为非活动状态前
// sheetDeactivateBefore: (i) => {
// console.log('sheetDeactivateBefore:', i);
// },
...onHooks(),
},
});
};
// 初始化
const init = () => {
getAllSheets();
};
// 所有工作表的配置信息
const getAllSheets = () => {
// 获取所有工作表的配置信息
const allSheets = luckysheet.getAllSheets();
console.log("所有工作表的配置信息:", allSheets);
return allSheets;
};
// 工作表数据
const getSheetData = () => {
const luckysheetdata = luckysheet.getSheetData({ order: 0 });
console.log("工作表数据", luckysheetdata);
return luckysheetdata;
};
// data => celldata,data二维数组数据转化成 {r,c,v}格式一维数组
const transToCellData = () => {
const cellData = luckysheet.transToCellData(getSheetData());
console.log("data=>celldata", cellData);
return cellData;
};
// celldata => data, celldata一维数组数据转化成表格所需二维数组
const transToData = () => {
const data = luckysheet.transToData(transToCellData());
console.log("celldata=>data", data);
};
// 工作表配置
const getConfig = () => {
const config = luckysheet.getConfig({ order: 0 });
console.log("工作表配置", config);
return config;
};
// 单元格的值
const getCellValue = (row = 0, col = 0) => {
const cellValue = luckysheet.getCellValue(row, col, { type: "v" });
console.log("单元格的值", cellValue);
return getCellValue;
};
// 获取工作薄下某个单表中每个单元格的值
const getSheetCellValue = () => { //sheet
let sheet = workbook.sheets.find(item => item.index === '1');
// const rows = sheet.data.length; //行
// const cols = sheet.data[0].length; //列
const rows = sheet.row;
const cols = sheet.column;
// 获取每个单元格的值
const arr = new Array(rows);
for (let i = 0; i < rows; i++) {
arr[i] = new Array(cols);
}
// 逐个追加到二维数组中
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
// arr[i][j] = luckysheet.getCellValue(i, j, { type: "v" });
// 获取到的由公式的单元格的值不是最新的值(公式计算后的)。。。也不好使呀
arr[i][j] = luckysheet.getCellValue(i, j, { order: '0', type: 'v', f: true });
}
}
console.log("每个单元格的值", arr);
return arr;
};
// 指定工作表范围设置数据验证功能,并设置参数
const setDataVerification = () => {
const optionItem = {
type: "number", //数字 number_decimal:小数
type2: "bw",
value1: "0", //最小值为0
value2: "1000000", //最大值为1000000
checked: false, //是否勾选中复选框;type为checkbox时需配置;
remote: false, //自动远程获取选项
prohibitInput: false, //输入数据无效时禁止输入;默认为false;
hintShow: false, //选中单元格时显示提示语;默认为false;
hintText: "请输入0到1000000之间的整数", //提示语文本,hintShow为true时需配置;
};
const setting = {
range: { row: [1, 74], column: [14, 26] }, //区间
};
luckysheet.setDataVerification(optionItem, setting, (res) => {
console.log(res, 1);
});
};
// toJson
const toJson = () => {
const json = luckysheet.toJson();
};
const closeWebsocket = () => {
luckysheet.closeWebsocket();
};
// 返回所有表格数据结构的一维数组
const getLuckysheetfile = () => {
// 调试使用,不适用初始化表格
const luckysheetfile = luckysheet.getLuckysheetfile();
};
// 处理冻结错位
const handleFrozenMis = () => {
//调用luckysheet提供的API获取当前编辑的表格数据
let allSheets = getAllSheets();
/** 解决这三种情况下,保存数据,重新渲染时,冻结行列错位问题 start */
// 冻结行到选区
allSheets.forEach(sheet => {
if (sheet.frozen) {
if (sheet.frozen.type === "rangeRow") {
sheet.frozen.range.row_focus =
sheet.frozen.range.row_focus - 1;
}
// 冻结列到选区
if (sheet.frozen.type === "rangeColumn") {
sheet.frozen.range.column_focus =
sheet.frozen.range.column_focus - 1;
}
// 冻结行列到选区
if (sheet.frozen.type === "rangeBoth") {
sheet.frozen.range.column_focus =
sheet.frozen.range.column_focus - 1;
sheet.frozen.range.row_focus =
sheet.frozen.range.row_focus - 1;
}
}
})
return allSheets;
/** 解决这三种情况下,保存数据,重新渲染时,冻结行列错位问题 end */
};
//保存数据到本地文件 (供模拟)
const saveToLocalFile = (data) => {
const content = JSON.stringify(allSheets);
const blob = new Blob([content], {
type: "text/plain;charset=utf-8",
});
saveAs(blob, "template.json");
};
// 退出编辑模式
const exitEditMode = () => {
// 自动退出编辑模式的操作,主要是为了触发自动保存单元格
luckysheet.exitEditMode();
};
const handleUpdate = (operate) => {
console.log("绑定更新处理函数", operate);
const allSheets = getAllSheets();
// 只修改当前工作表的的数据,加快保存速度。
const curSheetIndex = allSheets.findIndex(sheet => sheet.index.toString() === operate.sheetIndex.toString());
// 工作表数据变更后的数据
const curdata = operate.curdata;
handleData(curSheetIndex, curdata);
};
//摘出变更数据 / 要保存的数据
const handleData = (curSheetIndex, curdata) => {
if (curSheetIndex) { //单个表变更 只需修改数据库中对应的sheetIndex的表的data数据。 ???需要接口配合
// 类似这样
workbook.sheets[curSheetIndex].data = curdata;
} else { //整个工作簿数据保存
let allSheets = handleFrozenMis();
// 去除临时数据,减小体积
for (const i in allSheets) {
allSheets[i].row = allSheets[i].data.length; //行数
allSheets[i].column = allSheets[i].data[0].length; //列数
// allSheets[i].data = undefined;
}
workbook.sheets = allSheets;
}
// 调用父组件的保存方法
onSave(workbook);
}
// 点击luckysheet之外的元素
const handleClickOutSide = () => {
exitEditMode();
}
/**defineExpose用于子组件向父组件暴露属性和方法;
* defineProps用于声明和定义props,接收父组件传值;
* defineEmits则用于在子组件中注册和触发自定义事件,传递信息给父组件
*/
defineExpose({
handleInit,
handleData
})
/**
* 如何解决冻结行列区不准的问题
* 1. 改源码 - 耗时且不准
* 2. 假设在填写模板时,去掉设置冻结的操作
* 在填写数据页面,把设置冻结的操作放开。然后供填写时设置方便。
* 提交时,去除冻结的配置。以防止回显时,冻结列现实错乱的问题。
*/
/**
* 综合考虑:
* 模板页面不需要自动保存功能,因为会涉及到工具栏的操作。
* 目前没有采用实时官方提供的loadurl,updateurl,allowupdate的这种方式去更新,需要用到websocket,后台数据难存储,所以,采用普通的方式保存
*/
</script>
<!-- scss配置已生效 -->
<style lang="scss" scoped>
#luckysheet {
height: calc(100% - 50px);
}
</style>
三、界面效果
-
模板页面:供制作模板,有头部工具栏(可简单修改)
-
填报页面:供填报 除部分数据可填写外,其它只读,且不可更改模板结构样式等。