vue3 + Luckysheet的使用

vue3 + Luckysheet的使用

  1. 模板页面功能:
  • 导入excel
  • 提交模板(一次性提交完整json)
  1. 填报页面功能:
  • 导出excel
  • 自动保存(修改单元格触发)
  • 模板json与已填单元格数据整合作为填报页面数据。
  • 设置冻结
  • 设置可编辑区
  • 设置数据验证
  • 单元格内容填充(替换填报json)与单元格值填充(只保存值 + 模板json组合)
  1. 问题修复
  • 修改 luckysheet 源代码。问题修复:设置单元格格式为数字的千分位格式时,数据验证,type设置为数字时,验证不通过
    在这里插入图片描述在这里插入图片描述
  • 带千分位符格式的数字,使用序列填充后,后面填充的值会是字符串,且数据不对,及不会自动触发公式的计算。如出现898,9.010这样的数据,所以,直接将源码中的填充方式默认修改为复制,其它提供的一律砍掉。(目前也用不上,临时修改)
    官方提供dropType的类型
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

注:修改完luckysheet源码后,需要重新打包再引用到项目中。

一、 Luckysheet的介绍

Luckysheet,一款纯前端类似excel的在线表格,功能强大、配置简单、完全开源。

二、Luckysheet简单使用

luckysheet源码地址:https://gitee.com/mengshukeji/Luckysheet/

  1. 下载源码,查看package.json文件,打包生成dist文件夹 npm run build
  2. 将打包生成的dist文件,重命名为luckysheet_dist,放在 根目录/public文件夹下
  3. 在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>

三、界面效果

  1. 模板页面:供制作模板,有头部工具栏(可简单修改)
    在这里插入图片描述

  2. 填报页面:供填报 除部分数据可填写外,其它只读,且不可更改模板结构样式等。
    在这里插入图片描述

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值