Explorar el Código

华创启德质量指标:新增excel组件,新增日期周期组件

ZhuJiaHao hace 1 semana
padre
commit
5d8d7defce

+ 90 - 0
package-lock.json

@@ -19,6 +19,7 @@
         "@fullcalendar/vue": "^5.9.0",
         "@handsontable/vue": "^4.1.1",
         "@jiaminghi/data-view": "^2.10.0",
+        "@revolist/vue-datagrid": "^4.22.1",
         "animate.css": "^4.1.0",
         "area-data": "^5.0.6",
         "axios": "^0.21.1",
@@ -62,6 +63,7 @@
         "long": "^4.0.0",
         "lowdb": "^1.0.0",
         "luckyexcel": "^1.0.1",
+        "luckysheet": "^2.1.13",
         "mockjs": "^1.1.0",
         "normalize.css": "^8.0.1",
         "nprogress": "^0.2.0",
@@ -2833,6 +2835,19 @@
         "node": "*"
       }
     },
+    "node_modules/@revolist/revogrid": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@revolist/revogrid/-/revogrid-4.22.1.tgz",
+      "integrity": "sha512-hCkMHgl9xZxvlsY3JiwD8h39Ary460HKRprqKNIw8SyV0Vf9A+c9N8Nbm/jk9YGcQTvrDU3jgFRdN/l+s255gw=="
+    },
+    "node_modules/@revolist/vue-datagrid": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@revolist/vue-datagrid/-/vue-datagrid-4.22.1.tgz",
+      "integrity": "sha512-EZFjYsvNHmjrvCdpDQgTJf+ozgk/KkQ8YfUhCGyDwBk/4z8/dfafu7wuKiYI/EgnAEuotGkmuxr95Ub1mriZjw==",
+      "dependencies": {
+        "@revolist/revogrid": "4.22.1"
+      }
+    },
     "node_modules/@soda/friendly-errors-webpack-plugin": {
       "version": "1.8.1",
       "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
@@ -10645,6 +10660,11 @@
         "rimraf": "bin.js"
       }
     },
+    "node_modules/flatpickr": {
+      "version": "4.6.13",
+      "resolved": "https://registry.npmmirror.com/flatpickr/-/flatpickr-4.6.13.tgz",
+      "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw=="
+    },
     "node_modules/flatted": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
@@ -15207,6 +15227,25 @@
         "jszip": "^3.5.0"
       }
     },
+    "node_modules/luckysheet": {
+      "version": "2.1.13",
+      "resolved": "https://registry.npmmirror.com/luckysheet/-/luckysheet-2.1.13.tgz",
+      "integrity": "sha512-ZotItRKh3fxEtYz0GrZxkf97jeQSGsJpFNAu1I0NMDQ6rVrHAWKeggFak5pClGQ3DP62Gi8kd+8rzOpyY/UNZw==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.1",
+        "dayjs": "^1.9.6",
+        "flatpickr": "^4.6.6",
+        "jquery": "^2.2.4",
+        "numeral": "^2.0.6",
+        "pako": "^1.0.11"
+      }
+    },
+    "node_modules/luckysheet/node_modules/jquery": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmmirror.com/jquery/-/jquery-2.2.4.tgz",
+      "integrity": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==",
+      "deprecated": "This version is deprecated. Please upgrade to the latest version or find support at https://www.herodevs.com/support/jquery-nes."
+    },
     "node_modules/m3u8-parser": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz",
@@ -16241,6 +16280,14 @@
         "node": "*"
       }
     },
+    "node_modules/numeral": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/numeral/-/numeral-2.0.6.tgz",
+      "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/nwsapi": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
@@ -26519,6 +26566,19 @@
         }
       }
     },
+    "@revolist/revogrid": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@revolist/revogrid/-/revogrid-4.22.1.tgz",
+      "integrity": "sha512-hCkMHgl9xZxvlsY3JiwD8h39Ary460HKRprqKNIw8SyV0Vf9A+c9N8Nbm/jk9YGcQTvrDU3jgFRdN/l+s255gw=="
+    },
+    "@revolist/vue-datagrid": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmmirror.com/@revolist/vue-datagrid/-/vue-datagrid-4.22.1.tgz",
+      "integrity": "sha512-EZFjYsvNHmjrvCdpDQgTJf+ozgk/KkQ8YfUhCGyDwBk/4z8/dfafu7wuKiYI/EgnAEuotGkmuxr95Ub1mriZjw==",
+      "requires": {
+        "@revolist/revogrid": "4.22.1"
+      }
+    },
     "@soda/friendly-errors-webpack-plugin": {
       "version": "1.8.1",
       "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
@@ -32824,6 +32884,11 @@
         }
       }
     },
+    "flatpickr": {
+      "version": "4.6.13",
+      "resolved": "https://registry.npmmirror.com/flatpickr/-/flatpickr-4.6.13.tgz",
+      "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw=="
+    },
     "flatted": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
@@ -36344,6 +36409,26 @@
         "jszip": "^3.5.0"
       }
     },
+    "luckysheet": {
+      "version": "2.1.13",
+      "resolved": "https://registry.npmmirror.com/luckysheet/-/luckysheet-2.1.13.tgz",
+      "integrity": "sha512-ZotItRKh3fxEtYz0GrZxkf97jeQSGsJpFNAu1I0NMDQ6rVrHAWKeggFak5pClGQ3DP62Gi8kd+8rzOpyY/UNZw==",
+      "requires": {
+        "@babel/runtime": "^7.12.1",
+        "dayjs": "^1.9.6",
+        "flatpickr": "^4.6.6",
+        "jquery": "^2.2.4",
+        "numeral": "^2.0.6",
+        "pako": "^1.0.11"
+      },
+      "dependencies": {
+        "jquery": {
+          "version": "2.2.4",
+          "resolved": "https://registry.npmmirror.com/jquery/-/jquery-2.2.4.tgz",
+          "integrity": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q=="
+        }
+      }
+    },
     "m3u8-parser": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz",
@@ -37206,6 +37291,11 @@
         "bignumber.js": "^8.0.1"
       }
     },
+    "numeral": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/numeral/-/numeral-2.0.6.tgz",
+      "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="
+    },
     "nwsapi": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",

+ 2 - 0
package.json

@@ -32,6 +32,7 @@
     "@fullcalendar/vue": "^5.9.0",
     "@handsontable/vue": "^4.1.1",
     "@jiaminghi/data-view": "^2.10.0",
+    "@revolist/vue-datagrid": "^4.22.1",
     "animate.css": "^4.1.0",
     "area-data": "^5.0.6",
     "axios": "^0.21.1",
@@ -75,6 +76,7 @@
     "long": "^4.0.0",
     "lowdb": "^1.0.0",
     "luckyexcel": "^1.0.1",
+    "luckysheet": "^2.1.13",
     "mockjs": "^1.1.0",
     "normalize.css": "^8.0.1",
     "nprogress": "^0.2.0",

+ 3 - 0
public/index.html

@@ -28,12 +28,15 @@
     <script src="https://cdn.jsdelivr.net/npm/luckysheet/dist/luckysheet.umd.js"></script> -->
 
     <!-- 使用luckysheet文件 本地引入 -->
+    <!-- 已移至 /luckysheet.html 独立页面,避免全局污染 -->
+    <!-- 
     <link rel='stylesheet' href='lib/luckysheet/plugins/css/pluginsCss.css' />
     <link rel='stylesheet' href='lib/luckysheet/plugins/plugins.css' />
     <link rel='stylesheet' href='lib/luckysheet/css/luckysheet.css' />
     <link rel='stylesheet' href='lib/luckysheet/assets/iconfont/iconfont.css' />
     <script src="lib/luckysheet/plugins/js/plugin.js"></script>
     <script src="lib/luckysheet/luckysheet.umd.js"></script>
+    -->
 
     <title><%= VUE_APP_TITLE %></title>
 	<script type="text/javascript" src="/word/web-apps/apps/api/documents/api.js"></script>

+ 143 - 0
public/luckysheet.html

@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8">
+  <link rel='stylesheet' href='lib/luckysheet/plugins/css/pluginsCss.css' />
+  <link rel='stylesheet' href='lib/luckysheet/plugins/plugins.css' />
+  <link rel='stylesheet' href='lib/luckysheet/css/luckysheet.css' />
+  <link rel='stylesheet' href='lib/luckysheet/assets/iconfont/iconfont.css' />
+  <script src="lib/luckysheet/plugins/js/plugin.js"></script>
+  <script src="lib/luckysheet/luckysheet.umd.js"></script>
+  <style>
+    body, html {
+      margin: 0;
+      padding: 0;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+    }
+    #luckysheet {
+      margin: 0;
+      padding: 0;
+      width: 100%;
+      height: 100%;
+    }
+  </style>
+</head>
+<body>
+  <div id="luckysheet"></div>
+  <script>
+    let isInitialized = false
+    
+    window.addEventListener('message', function(event) {
+      const { type, data, readonly } = event.data
+      
+      if (type === 'init' && !isInitialized) {
+        luckysheet.create({
+          container: 'luckysheet',
+          lang: 'zh', // 设置中文语言,工具栏提示为中文
+          // 控制顶部工具栏显示:
+          // - readonly=true (查阅模式): showtoolbar=false 隐藏工具栏
+          // - readonly=false (添加/编辑模式): showtoolbar=true 显示工具栏
+          showtoolbar: !readonly,
+          showinfobar: false,
+          showsheetbar: true,
+          // 控制是否允许增加行/列
+          enableAddRow: !readonly,
+          enableAddCol: !readonly,
+          userInfo: false,
+          // 状态栏设置(包含缩放控制):
+          // - showstatisticBar: true 显示状态栏(包含缩放控制)
+          showstatisticBar: true,
+          // 控制编辑栏显示:
+          // - readonly=true: sheetFormulaBar=false 隐藏编辑栏
+          // - readonly=false: sheetFormulaBar=true 显示编辑栏
+          sheetFormulaBar: !readonly,
+          allowEdit: !readonly,
+          // 启用缩放控制 - 优化配置
+          showtoolbarConfig: {
+            zoom: true,  // 启用缩放控制,包括右下角的缩放组件
+          },
+          // 缩放比例配置
+          zoomRatio: {
+            default: 100,  // 默认缩放比例
+            max: 400,      // 最大缩放比例
+            min: 10        // 最小缩放比例
+          },
+          // 显示行号列标
+          showRowBar: true,
+          showColumnBar: true,
+          // 单元格右键菜单
+          cellRightClickConfig: {
+            copy: true,
+            paste: true,
+            insertRow: !readonly,
+            insertColumn: !readonly,
+            deleteRow: !readonly,
+            deleteColumn: !readonly,
+            hideRow: !readonly,
+            hideColumn: !readonly,
+            rowHeight: !readonly,
+            columnWidth: !readonly
+          },
+          data: data || [{ name: 'Sheet1', color: '', status: 1, order: 0, data: [[{ v: '' }]] }],
+          hook: {}
+        })
+        isInitialized = true
+      } else if (type === 'getData') {
+        const data = luckysheet.getAllSheets()
+        event.source.postMessage({ type: 'dataResult', data: data }, event.origin)
+      } else if (type === 'setData') {
+        luckysheet.destroy()
+        isInitialized = false
+        luckysheet.create({
+          container: 'luckysheet',
+          lang: 'zh',
+          showtoolbar: !readonly,
+          showinfobar: false,
+          showsheetbar: true,
+          enableAddRow: !readonly,
+          enableAddCol: !readonly,
+          userInfo: false,
+          showstatisticBar: true,
+          sheetFormulaBar: !readonly,
+          allowEdit: !readonly,
+          // 启用缩放控制 - 优化配置
+          showtoolbarConfig: {
+            zoom: true,  // 启用缩放控制,包括右下角的缩放组件
+          },
+          // 缩放比例配置
+          zoomRatio: {
+            default: 100,  // 默认缩放比例
+            max: 400,      // 最大缩放比例
+            min: 10        // 最小缩放比例
+          },
+          // 显示行号列标
+          showRowBar: true,
+          showColumnBar: true,
+          // 单元格右键菜单
+          cellRightClickConfig: {
+            copy: true,
+            paste: true,
+            insertRow: !readonly,
+            insertColumn: !readonly,
+            deleteRow: !readonly,
+            deleteColumn: !readonly,
+            hideRow: !readonly,
+            hideColumn: !readonly,
+            rowHeight: !readonly,
+            columnWidth: !readonly
+          },
+          data: data,
+          hook: {}
+        })
+        isInitialized = true
+
+      }
+    })
+    
+    // 通知父页面已准备好
+    window.parent.postMessage({ type: 'ready' }, '*')
+  </script>
+</body>
+</html>

+ 58 - 0
src/api/platform/onlineSheet/onlineSheetApi.js

@@ -0,0 +1,58 @@
+import request from '@/utils/request'
+import { SYSTEM_URL } from '@/api/baseUrl'
+
+/**
+ * 保存在线表格数据
+ * @param {*} params { id, content }
+ * id: 表格ID(新建时不传,更新时传)
+ * content: 表格JSON数据(字符串格式)
+ * @returns {Promise} 返回 { state: 200, data: { id: '...' } }
+ */
+export function saveSheet(params) {
+  return request({
+    url: SYSTEM_URL() + '/sheet/save',
+    method: 'post',
+    isLoading: true,
+    data: params
+  })
+}
+
+/**
+ * 获取在线表格数据
+ * @param {*} params { id }
+ * id: 表格ID
+ * @returns {Promise} 返回 { state: 200, data: { id: '...', content: '...' } }
+ */
+export function getSheet(params) {
+  return request({
+    url: SYSTEM_URL() + '/sheet/get',
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 删除在线表格数据
+ * @param {*} params { id }
+ * id: 表格ID
+ */
+export function removeSheet(params) {
+  return request({
+    url: SYSTEM_URL() + '/sheet/remove',
+    method: 'post',
+    isLoading: true,
+    params: params
+  })
+}
+
+/**
+ * 查询在线表格列表
+ * @param {*} params
+ */
+export function querySheetList(params) {
+  return request({
+    url: SYSTEM_URL() + '/sheet/query',
+    method: 'post',
+    data: params
+  })
+}

+ 384 - 0
src/views/component/onlineSheet/onlineSheetData.vue

@@ -0,0 +1,384 @@
+<!--
+    在线表格组件 - 基于 luckysheet(iframe 隔离)
+-->
+<template>
+  <div class="online-sheet-data">
+    <!-- 顶部按钮栏:只在非只读模式下显示 -->
+    <el-row type="flex" style="margin-bottom: 10px">
+      <el-col>
+        <div class="button">
+          <!-- 导出 Excel 按钮:导出当前表格内容为 .xlsx 文件 -->
+          <!-- 注意:不管 readonly 是什么情况,都要可见且可操作 -->
+          <!--
+          <el-button
+            type="warning"
+            size="mini"
+            icon="ibps-icon-export"
+            @click="handleExport"
+          >导出Excel</el-button>
+           -->
+          <!-- 导入 Excel 按钮:支持 .xlsx 和 .xls 格式 -->
+          <el-button
+            v-if="!readonly"
+            type="primary"
+            size="mini"
+            icon="ibps-icon-upload"
+            @click="handleImport"
+          >导入Excel</el-button>
+          <!-- 保存按钮:将表格数据保存到后端 -->
+          <el-button
+            v-if="!readonly"
+            type="success"
+            size="mini"
+            icon="ibps-icon-save"
+            @click="handleSave"
+          >保存</el-button>
+        </div>
+      </el-col>
+    </el-row>
+    <!-- luckysheet 表格区域:通过 iframe 隔离,避免全局事件冲突 -->
+    <el-row>
+      <el-col>
+        <iframe
+          ref="luckysheetFrame"
+          src="/luckysheet.html"
+          style="width: 100%; height: 600px; border: 1px solid #ddd;"
+          @load="onIframeLoad"
+        />
+      </el-col>
+    </el-row>
+    <!-- 隐藏的文件选择器:用于导入 Excel 文件 -->
+    <input
+      ref="fileInput"
+      type="file"
+      accept=".xlsx,.xls"
+      style="display: none"
+      @change="handleFileChange"
+    >
+  </div>
+</template>
+
+<script>
+import LuckyExcel from 'luckyexcel'
+import * as XLSX from 'xlsx'
+import { saveSheet, getSheet } from '@/api/platform/onlineSheet/onlineSheetApi'
+import { exportToExcel } from './onlineSheetExcelExport'  // Excel导出模块
+
+export default {
+  name: 'OnlineSheetData',
+  props: {
+    formData: {
+      type: Object,
+      default: () => ({})
+    },
+    readonly: {
+      type: Boolean,
+      default: false
+    },
+    // 表格 ID:用于保存和查询(暂时用不到,备用)
+    sheetId: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      sheetData: null, // 表格数据
+      iframeReady: false, // iframe 加载完成标记
+      isUpdatingFromParent: false, // 标记是否来自父组件的更新,避免循环
+      localSheetId: '' // 本地表格ID,从父组件数据或后端获取
+    }
+  },
+  watch: {
+    // 监听父组件传递的表格数据
+    'formData.biaoGeNeiRong': {
+      handler(val) {
+        if (val && !this.isUpdatingFromParent) {
+          // 标记开始内部更新,避免无限循环
+          this.isUpdatingFromParent = true
+          this.initData(val)
+        }
+      },
+      immediate: true
+    }
+  },
+  mounted() {
+    // 监听 iframe 的消息
+    window.addEventListener('message', this.handleMessage)
+  },
+  beforeDestroy() {
+    window.removeEventListener('message', this.handleMessage)
+  },
+  methods: {
+    // 处理 iframe 发来的消息
+    handleMessage(event) {
+      if (event.data.type === 'ready') {
+        // iframe 准备好,发送初始化数据
+        this.iframeReady = true
+        this.sendToIframe('init', this.sheetData)
+      } else if (event.data.type === 'dataResult') {
+        // 接收 iframe 返回的表格数据(在保存时使用)
+        this.sheetData = event.data.data
+      }
+    },
+    // iframe 加载完成事件(暂时用不到,备用)
+    onIframeLoad() {
+      // iframe 加载完成
+    },
+    // 发送消息到 iframe
+    sendToIframe(type, data = null) {
+      if (this.$refs.luckysheetFrame && this.$refs.luckysheetFrame.contentWindow) {
+        this.$refs.luckysheetFrame.contentWindow.postMessage({
+          type,
+          data,
+          readonly: this.readonly // 传递只读模式标记
+        }, '*')
+      }
+    },
+    // 初始化表格数据
+    initData(dataStr) {
+      try {
+        // 解析 JSON 字符串
+        const parsedData = JSON.parse(dataStr)
+        
+        // 判断是否包含 sheetId 和 sheets
+        if (parsedData && parsedData.sheetId) {
+          // 如果有 sheetId,记录到本地
+          this.localSheetId = parsedData.sheetId
+        }
+        
+        if (parsedData && parsedData.sheets) {
+          // 直接使用传入的表格数据
+          this.sheetData = parsedData.sheets
+          if (this.iframeReady) {
+            this.sendToIframe('init', this.sheetData)
+          }
+        } else {
+          // 空数据,初始化空表格
+          this.sheetData = null
+          if (this.iframeReady) {
+            this.sendToIframe('init', null)
+          }
+        }
+        
+        // 注意:不再从后端加载数据,因为我们已将后端保存接口注释
+        // 所有数据都直接来自父组件传递的 biaoGeNeiRong 字段
+      } catch (e) {
+        console.error('解析表格数据失败:', e)
+        this.sheetData = null
+      }
+    },
+    // 从后端加载表格数据(暂时用不到,备用)
+    async loadSheetData(id) {
+      try {
+        const response = await getSheet({ id })
+        if (response.state === 200 && response.data) {
+          this.sheetData = JSON.parse(response.data.content)
+          if (this.iframeReady) {
+            this.sendToIframe('init', this.sheetData)
+          }
+        }
+      } catch (error) {
+        console.error('加载表格数据失败:', error)
+        this.$message.error('加载表格数据失败!')
+      }
+    },
+    // 点击导入按钮
+    handleImport() {
+      this.$refs.fileInput.click()
+    },
+    // 处理文件选择
+    handleFileChange(e) {
+      const file = e.target.files[0]
+      if (!file) return
+
+      // 验证文件类型
+      const fileName = file.name
+      const fileExt = fileName.substring(fileName.lastIndexOf('.')).toLowerCase()
+      
+      if (fileExt !== '.xlsx' && fileExt !== '.xls') {
+        this.$message.error('仅支持导入 .xlsx 和 .xls 格式的 Excel 文件!')
+        e.target.value = ''
+        return
+      }
+
+      // 如果是 .xls 文件,提示用户样式可能丢失
+      if (fileExt === '.xls') {
+        this.$confirm(
+          '.xls 格式导入时可能无法保留单元格样式(如颜色、边框等)。建议将文件另存为 .xlsx 格式后再导入,以完整保留样式。是否继续导入?',
+          '格式提示',
+          {
+            confirmButtonText: '继续导入',
+            cancelButtonText: '取消',
+            type: 'warning'
+          }
+        ).then(() => {
+          this.processFile(file, fileExt)
+        }).catch(() => {
+          e.target.value = ''
+        })
+      } else {
+        this.processFile(file, fileExt)
+      }
+    },
+    // 处理文件读取
+    processFile(file, fileExt) {
+      const reader = new FileReader()
+      reader.onload = (evt) => {
+        try {
+          // 如果是 .xls 文件,先用 xlsx 转换为 .xlsx
+          if (fileExt === '.xls') {
+            this.handleXlsFile(evt.target.result)
+          } else {
+            // .xlsx 文件直接用 LuckyExcel 解析
+            this.handleXlsxFile(evt.target.result)
+          }
+        } catch (error) {
+          console.error('导入错误:', error)
+          this.$message.error(`导入失败:${error.message || '文件解析错误,请检查文件格式是否正确'}`)
+        }
+      }
+      reader.onerror = () => {
+        this.$message.error('导入失败:文件读取错误!')
+      }
+      reader.readAsArrayBuffer(file)
+    },
+    // 处理 .xls 文件
+    handleXlsFile(arrayBuffer) {
+      try {
+        // 使用 xlsx 读取 .xls,保留单元格样式
+        const workbook = XLSX.read(arrayBuffer, { 
+          type: 'array',
+          cellStyles: true  // 保留样式信息
+        })
+        
+        // 转换为 .xlsx 格式的 ArrayBuffer,保留样式
+        const xlsxArrayBuffer = XLSX.write(workbook, { 
+          bookType: 'xlsx', 
+          type: 'array',
+          cellStyles: true,  // 保留样式
+          bookSST: false
+        })
+        
+        // 用 LuckyExcel 解析转换后的数据
+        this.handleXlsxFile(xlsxArrayBuffer)
+      } catch (error) {
+        console.error('XLS 转换错误:', error)
+        this.$message.error('导入失败:.xls 文件格式不支持或已损坏!')
+      }
+    },
+    // 处理 .xlsx 文件
+    handleXlsxFile(arrayBuffer) {
+      LuckyExcel.transformExcelToLucky(arrayBuffer, (exportJson, luckysheetfile) => {
+        if (exportJson.sheets && exportJson.sheets.length > 0) {
+          this.sheetData = exportJson.sheets
+          this.sendToIframe('setData', this.sheetData)
+          this.$message.success(`导入成功!共 ${exportJson.sheets.length} 个 Sheet`)
+          // 导入后不自动保存,等待用户点击保存按钮
+        } else {
+          this.$message.error('导入失败:Excel 文件为空或格式错误!')
+        }
+      })
+    },
+    // 保存表格数据
+    async handleSave() {
+      try {
+        // 从 iframe 获取最新数据
+        this.sendToIframe('getData')
+        
+        // 等待数据返回
+        await new Promise(resolve => setTimeout(resolve, 200))
+        
+        if (this.sheetData) {
+          this.$message.success('表格数据已保存到表单!')
+          
+          // 标记为内部更新,避免循环
+          this.isUpdatingFromParent = true
+          
+          // 将表格数据以 JSON 字符串格式传递给父组件,key 使用 'biaoGeNeiRong'
+          this.$emit('change-data', 'biaoGeNeiRong', JSON.stringify({ 
+            sheetId: this.localSheetId, 
+            sheets: this.sheetData 
+          }))
+          
+          // 重置标记
+          this.$nextTick(() => {
+            this.isUpdatingFromParent = false
+          })
+        } else {
+          this.$message.warning('没有表格数据可保存!')
+        }
+      } catch (error) {
+        console.error('保存错误:', error)
+        this.$message.error(`保存失败:${error.message || '数据处理错误'}`)
+      }
+    },
+    // 导出 Excel 文件
+    async handleExport() {
+      try {
+        // 显示加载状态
+        const loading = this.$loading({
+          lock: true,
+          text: '正在准备导出数据...',
+          spinner: 'el-icon-loading',
+          background: 'rgba(0, 0, 0, 0.7)'
+        })
+        
+        try {
+          // 使用外部导出模块
+          await this.useExternalExport(loading)
+        } catch (error) {
+          console.error('导出错误:', error)
+          this.$message.error(`导出失败: ${error.message || '数据处理错误'}`)
+        } finally {
+          loading.close()
+        }
+        
+      } catch (outerError) {
+        console.error('导出外层错误:', outerError)
+        this.$message.error('导出过程发生异常')
+      }
+    },
+    
+    // 使用外部导出模块导出Excel
+    async useExternalExport(loading) {
+      if (!this.sheetData || this.sheetData.length === 0) {
+        this.$message.warning('没有表格数据可导出!')
+        loading.close()
+        return
+      }
+      
+      // 从 iframe 获取最新数据
+      this.sendToIframe('getData')
+      
+      // 等待数据更新
+      await new Promise(resolve => setTimeout(resolve, 500))
+      
+      try {
+        // 使用独立的导出模块
+        const result = await exportToExcel(this.sheetData)
+        
+        if (result.success) {
+          this.$message.success(`导出成功!文件: ${result.fileName}`)
+        } else {
+          this.$message.error(`导出失败: ${result.error || '未知错误'}`)
+        }
+      } catch (error) {
+        this.$message.error(`导出失败: ${error.message || '未知错误'}`)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.online-sheet-data {
+  .button {
+    display: flex;
+    flex-direction: row-reverse;
+    .el-button {
+      margin-left: 5px;
+    }
+  }
+}
+</style>

+ 403 - 0
src/views/component/onlineSheet/onlineSheetExcelExport.js

@@ -0,0 +1,403 @@
+import ExcelJS from 'exceljs'
+import FileSaver from 'file-saver'
+
+// 颜色转换辅助方法
+function convertColorToHex(color) {
+  if (!color) return '000000'
+  
+  // 如果已经是6位HEX(不含#)
+  if (/^[0-9A-Fa-f]{6}$/.test(color)) return color.toUpperCase()
+  
+  // 如果带#,去掉#
+  if (color.startsWith('#')) {
+    const hex = color.substring(1)
+    // 如果是3位简写,转换为6位
+    if (hex.length === 3) {
+      return hex.split('').map(c => c + c).join('').toUpperCase()
+    }
+    return hex.toUpperCase()
+  }
+  
+  // 处理rgb/rgba格式
+  if (color.startsWith('rgb')) {
+    const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/)
+    if (match) {
+      const r = parseInt(match[1]).toString(16).padStart(2, '0')
+      const g = parseInt(match[2]).toString(16).padStart(2, '0')
+      const b = parseInt(match[3]).toString(16).padStart(2, '0')
+      return (r + g + b).toUpperCase()
+    }
+  }
+  
+  // 默认返回黑色
+  return '000000'
+}
+
+// 格式转换辅助方法
+function convertDateFormat(format) {
+  if (!format) return format
+  
+  // 常见的中文格式转换
+  const formatMap = {
+    'yyyy-mm-dd': 'yyyy/mm/dd',
+    'yyyy年mm月dd日': 'yyyy"年"mm"月"dd"日"',
+    'm/d/yy': 'yyyy/m/d',  // 将两位年份格式改为四位年份
+    'yyyy/m/d': 'yyyy/m/d',
+    'h:mm': 'h:mm',
+    'h:mm:ss': 'h:mm:ss',
+    '@': '@',
+    'General': 'General',
+    '0_ ': '0',  // 清理带空格的格式
+    '0%': '0%',
+    '0.00%': '0.00%',
+    '0.0%': '0.0%',
+    '0.0': '0.0',
+    '0.00': '0.00'
+  }
+  
+  return formatMap[format] || format
+}
+
+// 应用单元格样式
+function applyCellStyle(excelCell, cell) {
+  try {
+    if (!cell) return
+    
+    // 根据数据结构提取样式对象
+    let styleObj = null
+    
+    if (cell.v && typeof cell.v === 'object') {
+      // celldata 格式: {r: 4, c: 0, v: {ct: {...}, bg: '#00B0F0', ...}}
+      styleObj = cell.v
+    } else if (cell.ct || cell.bg || cell.fs || cell.fc) {
+      // data 数组格式: {ct: {...}, bg: '#00B0F0', fs: 11, fc: '#000000', ...}
+      styleObj = cell
+    } else {
+      return
+    }
+    
+    if (!styleObj) return
+    
+    // 字体样式
+    if (styleObj.fs || styleObj.fc || styleObj.ff || styleObj.bl) {
+      const font = {}
+      
+      if (styleObj.fs) font.size = styleObj.fs
+      if (styleObj.fc) {
+        const colorHex = convertColorToHex(styleObj.fc)
+        font.color = { argb: 'FF' + colorHex }
+      }
+      if (styleObj.ff) font.name = styleObj.ff
+      if (styleObj.bl === 1) font.bold = true
+      
+      if (Object.keys(font).length > 0) {
+        excelCell.font = font
+      }
+    }
+    
+    // 填充(背景色)
+    if (styleObj.bg) {
+      const bgColorHex = convertColorToHex(styleObj.bg)
+      try {
+        excelCell.fill = {
+          type: 'pattern',
+          pattern: 'solid',
+          fgColor: { argb: 'FF' + bgColorHex }
+        }
+      } catch (fillError) {
+        // 静默处理填充错误
+      }
+    }
+    
+    // 对齐
+    if (styleObj.vt !== undefined || styleObj.ht !== undefined) {
+      const alignment = {}
+      if (styleObj.vt !== undefined) {
+        alignment.vertical = styleObj.vt === 0 ? 'top' : styleObj.vt === 2 ? 'bottom' : 'middle'
+      }
+      if (styleObj.ht !== undefined) {
+        alignment.horizontal = styleObj.ht === 0 ? 'left' : styleObj.ht === 2 ? 'right' : 'center'
+      }
+      excelCell.alignment = alignment
+    }
+  } catch (error) {
+    // 静默处理样式应用错误
+  }
+}
+
+// 设置单元格值(处理格式)
+function setCellValue(excelCell, cell) {
+  if (!cell) return
+  
+  let value = ''
+  let ct = null
+  let displayText = null
+  
+  // 处理不同的数据结构
+  if (cell.v && typeof cell.v === 'object' && cell.v !== null) {
+    // cell.v 是对象格式(来自 celldata)
+    if (cell.v.ct || cell.v.v !== undefined || cell.v.m !== undefined) {
+      // 包含 ct 属性或者 v/m 属性
+      value = cell.v.v !== undefined ? cell.v.v : cell.v
+      ct = cell.v.ct
+      displayText = cell.v.m // 显示文本
+      
+      // 如果是合并单元格,不设置值
+      if (cell.v.mc) {
+        return
+      }
+    } else {
+      // cell.v是简单对象但不是ct格式
+      value = cell.v
+    }
+  } else if (cell.ct) {
+    // 直接包含 ct 格式(来自 data 数组)
+    // 注意:data数组中的cell.v是原始值,不是对象
+    value = cell.v !== undefined ? cell.v : ''
+    ct = cell.ct
+    displayText = cell.m // 显示文本
+  } else if (cell.v !== undefined) {
+    // 简单的 v 值
+    value = cell.v
+  }
+  
+  if (value === null || value === undefined || value === '') return
+  
+  // 首先检查是否为日期或时间格式
+  const isDateFormat = ct && ct.fa && (
+    ct.fa === 'm/d/yy' || 
+    ct.fa === 'yyyy-mm-dd' || 
+    ct.fa === 'yyyy年mm月dd日' ||
+    (ct.fa.includes('d') && !ct.fa.includes('h')) // 包含d但不包含h
+  )
+  
+  const isTimeFormat = ct && ct.fa && (
+    ct.fa === 'h:mm' || 
+    ct.fa === 'h:mm:ss' ||
+    ct.fa.includes('h:')
+  )
+  
+  // 处理日期格式
+  if (isDateFormat) {
+    const numValue = parseFloat(value)
+    const textValue = displayText || ''
+    
+    // 检查是否为两位年份格式(如"3/3/26")
+    if (textValue && /^\d{1,2}\/\d{1,2}\/\d{2}$/.test(textValue)) {
+      const parts = textValue.split('/')
+      if (parts.length === 3) {
+        let [month, day, year] = parts
+        // 将两位年份转换为四位年份(假设2000-2099年)
+        const fullYear = parseInt(year) < 30 ? 2000 + parseInt(year) : 1900 + parseInt(year)
+        const formattedDate = `${fullYear}/${month}/${day}`
+        
+        excelCell.value = formattedDate
+        excelCell.numFmt = 'yyyy/m/d'  // 日期格式
+        return
+      }
+    }
+    
+    // 如果不是两位年份格式,使用Date对象
+    if (!isNaN(numValue)) {
+      // Excel日期序列号转JS Date
+      const excelEpoch = new Date(Date.UTC(1899, 11, 30)) // 1899-12-30 UTC
+      const days = Math.floor(numValue) - 1 // 减去1天修正闰年bug
+      const timeFraction = numValue - Math.floor(numValue) // 获取小数部分(时间)
+      
+      const date = new Date(excelEpoch.getTime() + days * 86400000 + timeFraction * 86400000)
+      excelCell.value = date
+      excelCell.numFmt = 'yyyy/m/d'
+      return
+    }
+  }
+  
+  // 处理时间格式
+  if (isTimeFormat) {
+    const numValue = parseFloat(value)
+    if (!isNaN(numValue)) {
+      // 使用UTC时间避免时区问题
+      const baseDate = new Date(Date.UTC(1899, 11, 30)) // 基准日期
+      const timeInMs = numValue * 86400000 // 一天的毫秒数
+      const timeDate = new Date(baseDate.getTime() + timeInMs)
+      
+      excelCell.value = timeDate
+      excelCell.numFmt = convertDateFormat(ct.fa)
+      return
+    }
+  }
+  
+  // 处理其他类型
+  if (ct && ct.t === 'n') {
+    // 数字类型
+    const numValue = parseFloat(value)
+    if (!isNaN(numValue)) {
+      if (ct.fa) {
+        excelCell.numFmt = convertDateFormat(ct.fa)
+      }
+      excelCell.value = numValue
+    } else {
+      excelCell.value = value
+    }
+  } else if (ct && ct.t === 's') {
+    // 字符串类型
+    excelCell.value = value
+    if (ct.fa === '@' || (!ct.fa && /^\d+$/.test(value))) {
+      excelCell.numFmt = '@'
+    }
+  } else if (ct && ct.t === 'g') {
+    // 一般类型(通常是文本)
+    excelCell.value = value
+    if (!ct.fa && /^\d+$/.test(value)) {
+      excelCell.numFmt = '@'
+    }
+  } else {
+    // 其他类型
+    excelCell.value = value
+  }
+}
+
+// 处理边框
+function applyBorders(excelCell, sheet, row, col) {
+  if (!sheet.config || !sheet.config.borderInfo) return
+  
+  const borderInfo = sheet.config.borderInfo.find(b => 
+    b.rangeType === 'cell' && 
+    b.value.row_index === row && 
+    b.value.col_index === col
+  )
+  
+  if (!borderInfo || !borderInfo.value) return
+  
+  const border = {}
+  const borderValue = borderInfo.value
+  
+  if (borderValue.t) {
+    border.top = { 
+      style: borderValue.t.style === 1 ? 'thin' : 'thin',
+      color: { argb: 'FF' + convertColorToHex(borderValue.t.color || '#000000') }
+    }
+  }
+  if (borderValue.r) {
+    border.right = { 
+      style: borderValue.r.style === 1 ? 'thin' : 'thin',
+      color: { argb: 'FF' + convertColorToHex(borderValue.r.color || '#000000') }
+    }
+  }
+  if (borderValue.b) {
+    border.bottom = { 
+      style: borderValue.b.style === 1 ? 'thin' : 'thin',
+      color: { argb: 'FF' + convertColorToHex(borderValue.b.color || '#000000') }
+    }
+  }
+  if (borderValue.l) {
+    border.left = { 
+      style: borderValue.l.style === 1 ? 'thin' : 'thin',
+      color: { argb: 'FF' + convertColorToHex(borderValue.l.color || '#000000') }
+    }
+  }
+  
+  if (Object.keys(border).length > 0) {
+    excelCell.border = border
+  }
+}
+
+
+
+// 主导出函数
+export async function exportToExcel(sheetData, fileName = null) {
+  try {
+    const workbook = new ExcelJS.Workbook()
+    
+    // 处理所有工作表
+    for (let sheetIndex = 0; sheetIndex < sheetData.length; sheetIndex++) {
+      const sheet = sheetData[sheetIndex]
+      const sheetName = sheet.name || `Sheet${sheetIndex + 1}`
+      const worksheet = workbook.addWorksheet(sheetName)
+      
+      let maxRow = 0
+      let maxCol = 0
+      
+      // 优先级:使用data数组(完整二维数组)
+      if (sheet.data && Array.isArray(sheet.data)) {
+        for (let r = 0; r < sheet.data.length; r++) {
+          const row = sheet.data[r]
+          
+          // 设置行高
+          if (sheet.rowlen && sheet.rowlen[r]) {
+            worksheet.getRow(r + 1).height = sheet.rowlen[r]
+          }
+          
+          if (Array.isArray(row)) {
+            for (let c = 0; c < row.length; c++) {
+              const cell = row[c]
+              if (cell) {
+                const excelCell = worksheet.getCell(r + 1, c + 1)
+                
+                // 设置值
+                setCellValue(excelCell, cell)
+                
+                // 设置样式
+                applyCellStyle(excelCell, cell)
+                
+                // 设置边框
+                applyBorders(excelCell, sheet, r, c)
+                
+                if (c + 1 > maxCol) maxCol = c + 1
+              }
+            }
+          }
+          
+          if (r + 1 > maxRow) maxRow = r + 1
+        }
+      } 
+      // 使用celldata稀疏格式
+      else if (sheet.celldata && Array.isArray(sheet.celldata)) {
+        sheet.celldata.forEach((cellData) => {
+          if (cellData && cellData.r !== undefined && cellData.c !== undefined) {
+            const excelCell = worksheet.getCell(cellData.r + 1, cellData.c + 1)
+            
+            // 设置值
+            setCellValue(excelCell, cellData)
+            
+            // 设置样式
+            applyCellStyle(excelCell, cellData)
+            
+            // 设置边框
+            applyBorders(excelCell, sheet, cellData.r, cellData.c)
+            
+            if (cellData.r + 1 > maxRow) maxRow = cellData.r + 1
+            if (cellData.c + 1 > maxCol) maxCol = cellData.c + 1
+          }
+        })
+        
+
+      }
+      
+      // 设置列宽
+      if (sheet.columnlen && Array.isArray(sheet.columnlen)) {
+        for (let c = 0; c < Math.min(sheet.columnlen.length, maxCol); c++) {
+          const width = sheet.columnlen[c]
+          if (width && width > 0) {
+            worksheet.getColumn(c + 1).width = width / 7
+          }
+        }
+      }
+    }
+    
+    // 生成文件名并保存
+    const finalFileName = fileName || `在线表格导出_${new Date().getTime()}.xlsx`
+    
+    const buffer = await workbook.xlsx.writeBuffer()
+    
+    FileSaver.saveAs(
+      new Blob([buffer], { type: 'application/octet-stream' }),
+      finalFileName
+    )
+    
+    return { success: true, fileName: finalFileName }
+    
+  } catch (error) {
+    console.error('Excel导出错误:', error)
+    return { success: false, error: error.message }
+  }
+}

+ 370 - 0
src/views/component/target/newFrequencyDate.vue

@@ -0,0 +1,370 @@
+<template>
+  <div class="frequency">
+    <!-- 频率选择单选按钮 -->
+    <el-row :gutter="20" class="page-row">
+      <el-col :span="24" class="frequency-select">
+        <el-radio-group v-model="selectedFrequency" @change="handleFrequencyChange" :disabled="readonly">
+          <el-radio label="month">月度</el-radio>
+          <el-radio label="quarter">季度</el-radio>
+          <el-radio label="halfYear">半年度</el-radio>
+          <el-radio label="year">年度</el-radio>
+        </el-radio-group>
+      </el-col>
+    </el-row>
+
+    <!-- 月度指标区块 -->
+    <el-row v-if="selectedFrequency === 'month'" :gutter="20" class="page-row">
+      <el-col :span="12" class="inline-item">
+        <div class="label">月度指标统计时间</div>
+        <el-select
+          v-model="pageData.month[1]"
+          clearable
+          :disabled="readonly"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="item in monthOption"
+            :key="item"
+            :label="`每月${item}日`"
+            :value="item"
+          />
+        </el-select>
+      </el-col>
+    </el-row>
+
+    <!-- 季度指标区块 -->
+    <el-row v-if="selectedFrequency === 'quarter'" :gutter="20" class="page-row">
+      <el-col :span="12" class="inline-item">
+        <div class="label">季度指标统计时间</div>
+        <el-select
+          v-model="pageData.quarter[0]"
+          clearable
+          :disabled="readonly"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="item in quarterOption"
+            :key="item"
+            :label="`第${item}月`"
+            :value="item"
+          />
+        </el-select>
+        <el-select
+          v-model="pageData.quarter[1]"
+          clearable
+          :disabled="readonly"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="item in monthOption"
+            :key="item"
+            :label="`第${item}天`"
+            :value="item"
+          />
+        </el-select>
+      </el-col>
+    </el-row>
+
+    <!-- 半年指标区块 -->
+    <el-row v-if="selectedFrequency === 'halfYear'" :gutter="20" class="page-row">
+      <el-col :span="12" class="inline-item">
+        <div class="label">半年指标统计时间</div>
+        <el-select
+          v-model="pageData.halfYear[0]"
+          clearable
+          :disabled="readonly"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="item in halfYearOption"
+            :key="item"
+            :label="`第${item}月`"
+            :value="item"
+          />
+        </el-select>
+        <el-select
+          v-model="pageData.halfYear[1]"
+          clearable
+          :disabled="readonly"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="item in monthOption"
+            :key="item"
+            :label="`第${item}天`"
+            :value="item"
+          />
+        </el-select>
+      </el-col>
+    </el-row>
+
+    <!-- 年度指标区块 -->
+    <el-row v-if="selectedFrequency === 'year'" :gutter="20" class="page-row">
+      <el-col :span="12" class="inline-item">
+        <div class="label">年度指标统计时间</div>
+        <el-select
+          v-model="pageData.year[0]"
+          clearable
+          :disabled="readonly"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="item in yearOption"
+            :key="item"
+            :label="`第${item}月`"
+            :value="item"
+          />
+        </el-select>
+        <el-select
+          v-model="pageData.year[1]"
+          clearable
+          :disabled="readonly"
+          placeholder="请选择"
+        >
+          <el-option
+            v-for="item in monthOption"
+            :key="item"
+            :label="`第${item}天`"
+            :value="item"
+          />
+        </el-select>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    formData: {
+      type: Object,
+      default: () => {}
+    },
+    readonly: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      pageData: {
+        month: [0, null],
+        quarter: [],
+        halfYear: [],
+        year: []
+      },
+      selectedFrequency: 'month', // 默认选中月度
+      isInitialized: false,
+      monthOption: Array.from({ length: 28 }, (_, index) => index + 1),
+      quarterOption: Array.from({ length: 3 }, (_, index) => index + 1),
+      halfYearOption: Array.from({ length: 6 }, (_, index) => index + 1),
+      yearOption: Array.from({ length: 12 }, (_, index) => index + 1)
+    }
+  },
+  watch: {
+    /*
+    这里可以看到父表单传过来的所有数据(前提是表单设计器里要把字段放进去才行)
+    formData:{
+      handler(val) {
+        console.log('父表单传过来的所有数据===>', val)
+      }
+    },
+    */
+    'formData.zhouQiCanShu1': {
+      handler(val) {
+        console.log('val===>', val)
+        if (val) {
+          this.pageData = JSON.parse(val)
+          // 根据数据内容确定当前选中的频率
+          this.determineSelectedFrequency()
+        } else {
+          this.pageData = {
+            month: [0, null],
+            quarter: [],
+            halfYear: [],
+            year: []
+          }
+          this.selectedFrequency = 'month' // 重置为默认
+        }
+      },
+      immediate: true  // 添加 immediate 确保初始时也触发
+    },
+    pageData: {
+      handler(val) {
+        this.$emit('change-data', 'zhouQiCanShu1', JSON.stringify(val))
+        this.$emit('change-data', 'zhouQiLeiXing', this.getFrequencyChineseLabel(this.selectedFrequency))
+        this.$emit('change-data', 'zhouQiXiangQing', this.getFrequencyDetailDescription(val))
+      },
+      deep: true
+    }
+  },
+  mounted() {
+    console.log('========= 质量指标计划表单,mounted方法,输出form ========')
+		console.log(this.formData)
+    // 由于watch中已经设置了immediate: true,这里不需要重复处理初始数据
+    this.isInitialized = true
+    
+  },
+
+  
+  methods: {
+    // 将频率枚举值转换为中文显示值
+    // 内部使用:'month', 'quarter', 'halfYear', 'year'
+    // 传递给父组件:'月度', '季度', '半年度', '年度'
+    getFrequencyChineseLabel(frequency) {
+      const frequencyMap = {
+        'month': '月度',
+        'quarter': '季度', 
+        'halfYear': '半年度',
+        'year': '年度'
+      }
+      return frequencyMap[frequency] || '月度' // 默认返回'月度'
+    },
+    
+    // 根据 pageData 生成周期详情文字描述
+    // 场景1:月度 - {"month":[0,12],"quarter":[],"halfYear":[],"year":[]} → "每月12日"
+    // 场景2:季度 - {"month":[0,null],"quarter":[2,5],"halfYear":[],"year":[]} → "每季度第2月第5天"
+    // 场景3:半年度 - {"month":[0,null],"quarter":[],"halfYear":[5,8],"year":[]} → "每半年第5月第8天"
+    // 场景4:年度 - {"month":[0,null],"quarter":[],"halfYear":[],"year":[6,16]} → "每年第6月第16天"
+    getFrequencyDetailDescription(pageData) {
+      // 判断哪个频率有有效数据
+      if (this.hasValidData(pageData.month)) {
+        // 月度:每月{day}日
+        const day = pageData.month[1]
+        return `每月第${day}日`
+      } else if (this.hasValidData(pageData.quarter)) {
+        // 季度:每季度第{month}月第{day}天
+        const month = pageData.quarter[0]
+        const day = pageData.quarter[1]
+        return `每季度第${month}月第${day}天`
+      } else if (this.hasValidData(pageData.halfYear)) {
+        // 半年度:每半年第{month}月第{day}天
+        const month = pageData.halfYear[0]
+        const day = pageData.halfYear[1]
+        return `每半年第${month}月第${day}天`
+      } else if (this.hasValidData(pageData.year)) {
+        // 年度:每年第{month}月第{day}天
+        const month = pageData.year[0]
+        const day = pageData.year[1]
+        return `每年第${month}月第${day}天`
+      } else {
+        // 默认情况:未设置
+        return '未设置周期'
+      }
+    },
+    
+    // 处理频率切换
+    handleFrequencyChange(frequency) {
+      // 清理未显示的区块数据
+      this.cleanUnselectedFrequencyData(frequency)
+    },
+    
+    // 清理未选中的频率数据
+    cleanUnselectedFrequencyData(selectedFrequency) {
+      const frequencies = ['month', 'quarter', 'halfYear', 'year']
+      
+      frequencies.forEach(freq => {
+        if (freq !== selectedFrequency) {
+          // 清理未选中的频率数据
+          if (freq === 'month') {
+            this.pageData.month = [0, null]
+          } else if (freq === 'quarter') {
+            this.pageData.quarter = []
+          } else if (freq === 'halfYear') {
+            this.pageData.halfYear = []
+          } else if (freq === 'year') {
+            this.pageData.year = []
+          }
+        }
+      })
+    },
+    
+    // 根据数据内容确定当前选中的频率
+    determineSelectedFrequency() {
+      // 判断哪个频率有有效数据
+      if (this.hasValidData(this.pageData.month)) {
+        this.selectedFrequency = 'month'
+      } else if (this.hasValidData(this.pageData.quarter)) {
+        this.selectedFrequency = 'quarter'
+      } else if (this.hasValidData(this.pageData.halfYear)) {
+        this.selectedFrequency = 'halfYear'
+      } else if (this.hasValidData(this.pageData.year)) {
+        this.selectedFrequency = 'year'
+      } else {
+        this.selectedFrequency = 'month' // 默认
+      }
+    },
+    
+    // 判断是否有有效数据
+    hasValidData(dataArray) {
+      if (!Array.isArray(dataArray)) return false
+      
+      // 对于月度数据,检查第二个元素是否为有效值(非null且大于0)
+      if (dataArray.length === 2 && dataArray[0] === 0) {
+        return dataArray[1] !== null && dataArray[1] > 0
+      }
+      
+      // 对于其他频率,检查数组是否有有效值
+      return dataArray.some(item => item !== null && item !== undefined && item !== '')
+    }
+  }
+  
+}
+</script>
+<style lang="scss" scoped>
+.frequency {
+  margin-top: 20px;
+  color: #606266;
+  padding: 16px;
+  border: 1.5px solid #c0c4cc;
+  border-radius: 6px;
+  background-color: #fff;
+  box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.06);
+  
+  &:hover {
+    border-color: #b1b4bb;
+    box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.1);
+  }
+  
+  .frequency-select {
+    margin-bottom: 20px;
+    padding-left: 14px;
+    
+    .el-radio-group {
+      .el-radio {
+        margin-right: 20px;
+        
+        &:last-child {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+  
+  .page-row {
+    margin-bottom: 12px;
+    
+    &:last-child {
+      margin-bottom: 4px;
+    }
+    
+    .inline-item {
+      display: flex;
+      padding-left: 14px !important;
+      
+      > div {
+        flex-grow: 1;
+        flex-shrink: 1;
+        margin-left: 10px;
+      }
+      
+      .label {
+        width: 130px;
+        flex-grow: 0;
+        flex-shrink: 0;
+        color: #606266;
+        font-weight: 500;
+      }
+    }
+  }
+}
+</style>

+ 1 - 1
vue.config.js

@@ -100,7 +100,7 @@ module.exports = {
     return configNew
   },
   // 默认情况下 babel-loader 会忽略所有 node_modules 中的文件。如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来。
-  transpileDependencies: ['signature_pad', 'vue-echarts', 'resize-detector'],
+  transpileDependencies: ['signature_pad', 'vue-echarts', 'resize-detector', '@revolist/vue-datagrid', '@revolist/revogrid'],
   // 默认设置: https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-service/lib/config/base.js
   chainWebpack: (config) => {
     /**