Browse Source

移入文档管理

yangg 8 months ago
parent
commit
148cb2fb53
78 changed files with 12694 additions and 2 deletions
  1. 1 1
      ui/.env.development
  2. 10 0
      ui/package.json
  3. 88 0
      ui/src/api/api.js
  4. 154 0
      ui/src/api/document.js
  5. 49 0
      ui/src/api/formula.js
  6. 49 0
      ui/src/api/params.js
  7. 108 0
      ui/src/api/product.js
  8. 102 0
      ui/src/api/project.js
  9. 131 0
      ui/src/api/sourceData.js
  10. 150 0
      ui/src/api/template.js
  11. 77 0
      ui/src/directive/el-drag-dialog/drag.js
  12. 13 0
      ui/src/directive/el-drag-dialog/index.js
  13. 11 0
      ui/src/views/system/document/category/components/dataInfo.scss
  14. 125 0
      ui/src/views/system/document/category/components/dataInfo.vue
  15. 55 0
      ui/src/views/system/document/category/components/dataList.scss
  16. 166 0
      ui/src/views/system/document/category/components/dataList.vue
  17. 3 0
      ui/src/views/system/document/category/components/dataSearch.scss
  18. 98 0
      ui/src/views/system/document/category/components/dataSearch.vue
  19. 3 0
      ui/src/views/system/document/category/components/index.js
  20. 4 0
      ui/src/views/system/document/category/search.scss
  21. 38 0
      ui/src/views/system/document/category/search.vue
  22. 81 0
      ui/src/views/system/document/com/components/Attribute/Directory/index.scss
  23. 69 0
      ui/src/views/system/document/com/components/Attribute/Directory/index.vue
  24. 108 0
      ui/src/views/system/document/com/components/Attribute/attributeVar/index.scss
  25. 289 0
      ui/src/views/system/document/com/components/Attribute/attributeVar/index.vue
  26. 69 0
      ui/src/views/system/document/com/components/Attribute/comList/index.scss
  27. 62 0
      ui/src/views/system/document/com/components/Attribute/comList/index.vue
  28. 12 0
      ui/src/views/system/document/com/components/Attribute/docAttr/index.scss
  29. 127 0
      ui/src/views/system/document/com/components/Attribute/docAttr/index.vue
  30. 3 0
      ui/src/views/system/document/com/components/Attribute/headerSetting/index.scss
  31. 50 0
      ui/src/views/system/document/com/components/Attribute/headerSetting/index.vue
  32. 108 0
      ui/src/views/system/document/com/components/Attribute/index.scss
  33. 344 0
      ui/src/views/system/document/com/components/Attribute/index.vue
  34. 16 0
      ui/src/views/system/document/com/components/Empty/index.vue
  35. 24 0
      ui/src/views/system/document/com/components/Formula/dataInfo.scss
  36. 542 0
      ui/src/views/system/document/com/components/Formula/dataInfo.vue
  37. 0 0
      ui/src/views/system/document/com/components/Formula/index.scss
  38. 148 0
      ui/src/views/system/document/com/components/Formula/index.vue
  39. 4 0
      ui/src/views/system/document/com/components/ProductAttr/index.scss
  40. 103 0
      ui/src/views/system/document/com/components/ProductAttr/index.vue
  41. 0 0
      ui/src/views/system/document/com/components/SourceData/index.scss
  42. 173 0
      ui/src/views/system/document/com/components/SourceData/index.vue
  43. 49 0
      ui/src/views/system/document/com/components/Table/index.scss
  44. 361 0
      ui/src/views/system/document/com/components/Table/index.vue
  45. 78 0
      ui/src/views/system/document/com/components/TextArea/index.scss
  46. 1082 0
      ui/src/views/system/document/com/components/TextArea/index.vue
  47. 74 0
      ui/src/views/system/document/com/components/TextView/index.scss
  48. 480 0
      ui/src/views/system/document/com/components/TextView/index.vue
  49. 11 0
      ui/src/views/system/document/com/components/Variable/dataInfo.scss
  50. 356 0
      ui/src/views/system/document/com/components/Variable/dataInfo.vue
  51. 0 0
      ui/src/views/system/document/com/components/Variable/index.scss
  52. 301 0
      ui/src/views/system/document/com/components/Variable/index.vue
  53. 0 0
      ui/src/views/system/document/com/components/sourceAi/index.scss
  54. 147 0
      ui/src/views/system/document/com/components/sourceAi/index.vue
  55. 171 0
      ui/src/views/system/document/com/components/sourceEs/index.vue
  56. 130 0
      ui/src/views/system/document/com/editor.scss
  57. 747 0
      ui/src/views/system/document/com/editor.vue
  58. 71 0
      ui/src/views/system/document/com/menus.scss
  59. 394 0
      ui/src/views/system/document/com/menus.vue
  60. 63 0
      ui/src/views/system/document/com/view.scss
  61. 867 0
      ui/src/views/system/document/com/view.vue
  62. 55 0
      ui/src/views/system/document/components/dataList.scss
  63. 231 0
      ui/src/views/system/document/components/dataList.vue
  64. 3 0
      ui/src/views/system/document/components/dataSearch.scss
  65. 141 0
      ui/src/views/system/document/components/dataSearch.vue
  66. 3 0
      ui/src/views/system/document/components/index.js
  67. 226 0
      ui/src/views/system/document/create.scss
  68. 2297 0
      ui/src/views/system/document/create.vue
  69. 4 0
      ui/src/views/system/document/search.scss
  70. 58 0
      ui/src/views/system/document/search.vue
  71. 55 0
      ui/src/views/system/document/temList/components/dataList.scss
  72. 222 0
      ui/src/views/system/document/temList/components/dataList.vue
  73. 3 0
      ui/src/views/system/document/temList/components/dataSearch.scss
  74. 115 0
      ui/src/views/system/document/temList/components/dataSearch.vue
  75. 3 0
      ui/src/views/system/document/temList/components/index.js
  76. 60 0
      ui/src/views/system/document/temList/temList.vue
  77. 68 0
      ui/src/views/system/document/test.vue
  78. 1 1
      ui/vue.config.js

+ 1 - 1
ui/.env.development

@@ -5,7 +5,7 @@ VUE_APP_TITLE = 若依管理系统
 ENV = 'development'
 
 # 若依管理系统/开发环境
-VUE_APP_BASE_API = '/dev-api'
+VUE_APP_BASE_API = 'http://120.46.190.49:8080'
 
 # 路由懒加载
 VUE_CLI_BABEL_TRANSPILE_MODULES = true

+ 10 - 0
ui/package.json

@@ -36,18 +36,28 @@
     "url": "https://gitee.com/y_project/RuoYi-Vue.git"
   },
   "dependencies": {
+    "@handsontable/vue": "^14.6.1",
     "@riophae/vue-treeselect": "0.4.0",
     "axios": "0.28.1",
+    "ckeditor4-vue": "^3.2.0",
     "clipboard": "2.0.8",
     "core-js": "3.37.1",
     "echarts": "5.4.0",
     "element-ui": "2.15.14",
     "file-saver": "2.0.5",
     "fuse.js": "6.4.3",
+    "handsontable": "^14.6.1",
     "highlight.js": "9.18.5",
+    "html-docx-js": "^0.3.1",
+    "html-to-docx": "^1.8.0",
+    "html2canvas": "^1.4.1",
+    "hyperformula": "^2.7.1",
+    "jquery": "^3.7.1",
     "js-beautify": "1.13.0",
     "js-cookie": "3.0.1",
     "jsencrypt": "3.0.0-rc.1",
+    "jspdf": "^2.5.2",
+    "luckyexcel": "^1.0.1",
     "nprogress": "0.2.0",
     "quill": "2.0.2",
     "screenfull": "5.0.2",

+ 88 - 0
ui/src/api/api.js

@@ -0,0 +1,88 @@
+import request from '@/utils/request'
+
+
+//用户登录
+export function userLogin(data) {
+  return request({
+    url: '/user/login',
+    method: 'post',
+    data
+  })
+}
+
+//获取当前登录用户
+export function getCurrentUserInfo(data) {
+  return request({
+    url: '/user/current',
+    method: 'post',
+    data
+  })
+}
+
+
+//用户登录
+export function userLogout(data) {
+  return request({
+    url: '/user/logout',
+    method: 'post',
+    data
+  })
+}
+
+/* 统计 */
+export function statistics(data) {
+  return request({
+    url: 'index/statistics',
+    method: 'post',
+    data
+  })
+}
+
+
+/* ai登录  */
+export function pcLoging(data) {
+  return request({
+    url: '/pcai/pcLoging',
+    method: 'post',
+    data
+  })
+}
+
+/* 发送验证码 */
+export function pcSMS(data) {
+  return request({
+    url: '/pcai/pcSMS',
+    method: 'post',
+    data
+  })
+}
+/* 校验验证码 */
+
+export function codeCheckout(data) {
+  return request({
+    url: '/pcai/codeCheckout',
+    method: 'post',
+    data
+  })
+}
+
+/* 内部链接 */
+
+export function pcInnerAi(data) {
+  return request({
+    url: '/pcai/pcInnerAi',
+    method: 'post',
+    data
+  })
+}
+
+/* 获取文件名 milvus/getMinioURl*/
+
+export function getMinioURl(data) {
+  return request({
+    url: '/milvus/getMinioURl',
+    method: 'post',
+    data
+  })
+}
+

+ 154 - 0
ui/src/api/document.js

@@ -0,0 +1,154 @@
+import request from '@/utils/request'
+
+/* 无data  文档编辑器中的文档及模版列表 */
+export function pageDocument(data) {
+  return request({
+    url: '/document/pageDocument',
+    method: 'post',
+    data
+  })
+}
+
+export function searchDocument(data) {
+  return request({
+    url: '/document/search',
+    method: 'post',
+    data
+  })
+}
+
+//导出
+export function exportDocument(data) {
+  return request({
+    url: '/document/export',
+    method: 'post',
+    data
+  })
+}
+
+
+//导出
+export function exportDemoDocument(data) {
+  return request({
+    url: '/document/demo',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+export function updateDocument(data) {
+  return request({
+    url: '/document/update',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function createDocument(data) {
+  return request({
+    url: '/document/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function getDocumentInfo(data) {
+  return request({
+    url: '/document/info',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function deleteDocument(data) {
+  return request({
+    url: '/document/delete',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+
+export function searchDocumentCategory(data) {
+  return request({
+    url: '/document/category/search',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+export function createDocumentCategory(data) {
+  return request({
+    url: '/document/category/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+export function updateDocumentCategory(data) {
+  return request({
+    url: '/document/category/update',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+export function getDocumentCategoryInfo(data) {
+  return request({
+    url: '/document/category/info',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+export function deleteDocumentCategory(data) {
+  return request({
+    url: '/document/category/delete',
+    method: 'post',
+    data
+  })
+}
+
+/* 获取模版列表 */
+
+export function searchList(data) {
+  return request({
+    url: '/template/searchList',
+    method: 'post',
+    data
+  })
+}
+
+/* 获取文档列表 */
+
+export function searchlistDoc(data) {
+  return request({
+    url: '/document/searchlistDoc',
+    method: 'post',
+    data
+  })
+}

+ 49 - 0
ui/src/api/formula.js

@@ -0,0 +1,49 @@
+import request from '@/utils/request'
+
+
+export function searchFormula(data) {
+  return request({
+    url: '/formula/search',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function createFormula(data) {
+  return request({
+    url: '/formula/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+ export function updateFormula(data) {
+   return request({
+     url: '/formula/update',
+     method: 'post',
+     data
+   })
+ }
+
+
+export function getFormulaInfo(data) {
+   return request({
+       url: '/formula/info',
+       method: 'post',
+       data
+   })
+}
+
+
+ export function deleteFormula(data) {
+   return request({
+     url: '/formula/delete',
+     method: 'post',
+     data
+   })
+ }
+

+ 49 - 0
ui/src/api/params.js

@@ -0,0 +1,49 @@
+import request from '@/utils/request'
+
+
+export function searchParams(data) {
+  return request({
+    url: '/params/search',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function createParams(data) {
+  return request({
+    url: '/params/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+ export function updateParams(data) {
+   return request({
+     url: '/params/update',
+     method: 'post',
+     data
+   })
+ }
+
+
+
+ export function deleteParams(data) {
+   return request({
+     url: '/params/delete',
+     method: 'post',
+     data
+   })
+ }
+
+
+ export function getParamsInfo(data) {
+   return request({
+     url: '/params/info',
+     method: 'post',
+     data
+   })
+ }

+ 108 - 0
ui/src/api/product.js

@@ -0,0 +1,108 @@
+import request from '@/utils/request'
+
+
+export function searchProductCategory(data) {
+  return request({
+    url: '/product/category/search',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function createProductCategory(data) {
+  return request({
+    url: '/product/category/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+ export function updateProductCategory(data) {
+   return request({
+     url: '/product/category/update',
+     method: 'post',
+     data
+   })
+ }
+
+
+
+ export function getProductCategoryInfo(data) {
+   return request({
+     url: '/product/category/info',
+     method: 'post',
+     data
+   })
+ }
+
+
+ export function deleteProductCategory(data) {
+   return request({
+     url: '/product/category/delete',
+     method: 'post',
+     data
+   })
+ }
+ 
+ 
+ export function searchAllCategories(data) {
+   return request({
+     url: '/product/category/all',
+     method: 'post',
+     data
+   })
+ }
+
+
+//处理产品信息
+
+
+ export function searchProduct(data) {
+   return request({
+     url: '/product/search',
+     method: 'post',
+     data
+   })
+ }
+
+
+
+ export function createProduct(data) {
+   return request({
+     url: '/product/create',
+     method: 'post',
+     data
+   })
+ }
+
+
+
+ export function updateProduct(data) {
+   return request({
+     url: '/product/update',
+     method: 'post',
+     data
+   })
+ }
+
+ export function getProductInfo(data) {
+   return request({
+     url: '/product/info',
+     method: 'post',
+     data
+   })
+ }
+
+
+  export function deleteProduct(data) {
+    return request({
+      url: '/product/delete',
+      method: 'post',
+      data
+    })
+  }
+

+ 102 - 0
ui/src/api/project.js

@@ -0,0 +1,102 @@
+import request from '@/utils/request'
+
+export function searchCategory(data) {
+  return request({
+    url: '/project/category/search',
+    method: 'post',
+    data
+  })
+}
+
+
+export function createCategory(data) {
+  return request({
+    url: '/project/category/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function updateCategory(data) {
+  return request({
+    url: '/project/category/update',
+    method: 'post',
+    data
+  })
+}
+
+
+export function getCategoryInfo(data) {
+  return request({
+    url: '/project/category/info',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function deleteCategory(data) {
+  return request({
+    url: '/project/category/delete',
+    method: 'post',
+    data
+  })
+}
+
+
+
+// 项目相关
+
+export function searchProject(data) {
+  return request({
+    url: '/project/search',
+    method: 'post',
+    data
+  })
+}
+
+
+export function createProject(data) {
+  return request({
+    url: '/project/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function updateProject(data) {
+  return request({
+    url: '/project/update',
+    method: 'post',
+    data
+  })
+}
+
+
+export function getProjectInfo(data) {
+  return request({
+    url: '/project/info',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function deleteProject(data) {
+  return request({
+    url: '/project/delete',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+

+ 131 - 0
ui/src/api/sourceData.js

@@ -0,0 +1,131 @@
+import request from '@/utils/request'
+
+
+
+export function searchSourceDataCategory(data) {
+  return request({
+    url: 'source/data/categories/',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function createSourceDataCategory(data) {
+  return request({
+    url: 'source/data/categories/create/',
+    method: 'post',
+    data
+  })
+}
+
+export function updateSourceDataCategory(data) {
+  return request({
+    url: '/source/data/category/update',
+    method: 'post',
+    data
+  })
+}
+
+
+export function deleteSourceDataCategory(data) {
+  return request({
+    url: '/source/data/category/delete',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function getSourceDataCategoryInfo(data) {
+  return request({
+    url: '/source/data/category/info',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+
+export function searchSourceData(data) {
+  return request({
+    url: '/source/data/search',
+    method: 'post',
+    data
+  })
+}
+
+
+export function createSourceData(data) {
+  return request({
+    url: '/source/data/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function updateSourceData(data) {
+  return request({
+    url: '/source/data/update',
+    method: 'post',
+    data
+  })
+}
+
+
+export function getSourceData(data) {
+  return request({
+    url: '/source/data/info',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function deleteSourceData(data) {
+  return request({
+    url: '/source/data/delete',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+/**搜索远程数据
+ * @param {Object} data
+ */
+export function findData(data) {
+  return request({
+    url: '/source/data/find',
+    method: 'post',
+    data
+  })
+}
+
+/* 上传图片 /upload/image */
+export function uploadImage(data) {
+  return request({
+    url: '/upload/image',
+    method: 'post',
+    data
+  })
+}
+
+/* 批量替换   */
+
+export function batch_content(data) {
+  return request({
+    url: '/project-raw-data/batch-content',
+    method: 'post',
+    data
+  })
+}

+ 150 - 0
ui/src/api/template.js

@@ -0,0 +1,150 @@
+import request from '@/utils/request'
+
+
+export function searchTemplateCategory(data) {
+  return request({
+    url: '/template/category/search',
+    method: 'post',
+    data
+  })
+}
+/* 获取全部分类接口 /template/category/selcetList*/
+export function selcetList(data) {
+  return request({
+    url: '/template/category/selcetList',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function getAllCategory(data) {
+  return request({
+    url: '/template/category/all',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function createTemplateCategory(data) {
+  return request({
+    url: '/template/category/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function updateTemplateCategory(data) {
+  return request({
+    url: '/template/category/update',
+    method: 'post',
+    data
+  })
+}
+
+
+export function getTemplateCategoryInfo(data) {
+  return request({
+    url: '/template/category/info',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function deleteTemplateCategory(data) {
+  return request({
+    url: '/template/category/delete',
+    method: 'post',
+    data
+  })
+}
+
+
+/* 模块列表 /template/pageTemplate*/
+export function pageTemplate(data) {
+  return request({
+    url: '/template/pageTemplate',
+    method: 'post',
+    data
+  })
+}
+
+export function searchTemplate(data) {
+  return request({
+    url: '/template/search',
+    method: 'post',
+    data
+  })
+}
+
+
+
+export function getTemplateInfo(data) {
+  return request({
+    url: '/template/info',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+export function deleteTemplate(data) {
+  return request({
+    url: '/template/delete',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+export function createTemplate(data) {
+  return request({
+    url: '/template/create',
+    method: 'post',
+    data
+  })
+}
+
+
+
+
+export function updateTemplate(data) {
+  return request({
+    url: '/template/update',
+    method: 'post',
+    data
+  })
+}
+
+
+/*template/category/allList  */
+
+
+export function getAllList(data) {
+  return request({
+    url: '/template/category/allList',
+    method: 'post',
+    data
+  })
+}
+
+/* 获取树状分类  /template/category/treeCategory */
+
+export function treeCategory(data) {
+  return request({
+    url: '/template/category/treeCategory',
+    method: 'post',
+    data
+  })
+}

+ 77 - 0
ui/src/directive/el-drag-dialog/drag.js

@@ -0,0 +1,77 @@
+export default {
+  bind(el, binding, vnode) {
+    const dialogHeaderEl = el.querySelector('.el-dialog__header')
+    const dragDom = el.querySelector('.el-dialog')
+    dialogHeaderEl.style.cssText += ';cursor:move;'
+    dragDom.style.cssText += ';top:0px;'
+
+    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
+    const getStyle = (function() {
+      if (window.document.currentStyle) {
+        return (dom, attr) => dom.currentStyle[attr]
+      } else {
+        return (dom, attr) => getComputedStyle(dom, false)[attr]
+      }
+    })()
+
+    dialogHeaderEl.onmousedown = (e) => {
+      // 鼠标按下,计算当前元素距离可视区的距离
+      const disX = e.clientX - dialogHeaderEl.offsetLeft
+      const disY = e.clientY - dialogHeaderEl.offsetTop
+
+      const dragDomWidth = dragDom.offsetWidth
+      const dragDomHeight = dragDom.offsetHeight
+
+      const screenWidth = document.body.clientWidth
+      const screenHeight = document.body.clientHeight
+
+      const minDragDomLeft = dragDom.offsetLeft
+      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
+
+      const minDragDomTop = dragDom.offsetTop
+      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
+
+      // 获取到的值带px 正则匹配替换
+      let styL = getStyle(dragDom, 'left')
+      let styT = getStyle(dragDom, 'top')
+
+      if (styL.includes('%')) {
+        styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
+        styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
+      } else {
+        styL = +styL.replace(/\px/g, '')
+        styT = +styT.replace(/\px/g, '')
+      }
+
+      document.onmousemove = function(e) {
+        // 通过事件委托,计算移动的距离
+        let left = e.clientX - disX
+        let top = e.clientY - disY
+
+        // 边界处理
+        if (-(left) > minDragDomLeft) {
+          left = -minDragDomLeft
+        } else if (left > maxDragDomLeft) {
+          left = maxDragDomLeft
+        }
+
+        if (-(top) > minDragDomTop) {
+          top = -minDragDomTop
+        } else if (top > maxDragDomTop) {
+          top = maxDragDomTop
+        }
+
+        // 移动当前元素
+        dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
+
+        // emit onDrag event
+        vnode.child.$emit('dragDialog')
+      }
+
+      document.onmouseup = function(e) {
+        document.onmousemove = null
+        document.onmouseup = null
+      }
+    }
+  }
+}

+ 13 - 0
ui/src/directive/el-drag-dialog/index.js

@@ -0,0 +1,13 @@
+import drag from './drag'
+
+const install = function(Vue) {
+  Vue.directive('el-drag-dialog', drag)
+}
+
+if (window.Vue) {
+  window['el-drag-dialog'] = drag
+  Vue.use(install); // eslint-disable-line
+}
+
+drag.install = install
+export default drag

+ 11 - 0
ui/src/views/system/document/category/components/dataInfo.scss

@@ -0,0 +1,11 @@
+ .sub-imgs{
+    padding:0px;
+    margin:0px;
+      li{
+        float:left;
+        margin-right:10px;
+        width:100px;
+        height:100px;
+        list-style: none;
+      }
+  }

+ 125 - 0
ui/src/views/system/document/category/components/dataInfo.vue

@@ -0,0 +1,125 @@
+<template>
+  <div class="data-info">
+    <el-card>
+      <el-form :model="dataForm" label-width="120px">
+
+        <el-form-item label="分类名称:">
+          <el-input v-model="dataForm.name"></el-input>
+        </el-form-item>
+
+        <el-form-item label="分类介绍:">
+          <el-input type="textArea" v-model="dataForm.intro"></el-input>
+        </el-form-item>
+
+        <el-form-item label="分类状态:">
+          <el-select v-model="dataForm.status"  placeholder="请选择分类状态" size="large" style="width:100%;">
+              <el-option
+                v-for="item in statusOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+        </el-form-item>
+
+      </el-form>
+    </el-card>
+    <div style="text-align:right; margin-top:20px;">
+        <el-button type="warning" @click="btnSave">确认保存</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+  import {createDocumentCategory,updateDocumentCategory,getDocumentCategoryInfo} from'@/api/document'
+  export default{
+    emits:['onClose'],
+    props:{
+      id:{
+        type:Number,
+        default:0
+      },
+    },
+    watch:{
+      id:{
+        handler(v){
+            let _this=this;
+            if(v==null || v<=0)return;
+            _this.getInfo(v);
+        },
+        immediate: true,
+        deep: true
+      }
+    },
+    data(){
+      return{
+        activeName:'base',
+        categoryList:[],
+        dataForm:{
+           id:0,
+           name:'',
+           intro:"",
+           status:5,
+        },
+        statusOptions:[
+         {
+           value:'',
+           label:"请选择状态"
+         },
+         {
+           value:5,
+           label:'启用'
+         },
+         {
+           value:6,
+           label:"停用"
+         },
+        ],
+      }
+    },
+    methods:{
+
+
+      //保存更新
+      btnSave(e){
+          let _this=this;
+
+        if(_this.dataForm.id>0){
+          updateDocumentCategory(_this.dataForm).then(res=>{
+            if(res.status!=200)return;
+            _this.$alert("文档分类更新成功");
+            _this.$emit("onClose")
+          })
+        }else{
+          createDocumentCategory(_this.dataForm).then(res=>{
+
+            if(res.status!=200)return;
+            _this.$alert("文档分类创建成功");
+            _this.$emit("onClose")
+          })
+        }
+
+      },
+
+      //产品详情
+      getInfo(id){
+          let _this=this;
+          let par={
+            id:id,
+          };
+          getDocumentCategoryInfo(par).then(res=>{
+              if(!res)return;
+              if(res.status!=200)return;
+              _this.dataForm=res.data;
+          })
+      },
+
+
+
+    }
+  }
+</script>
+
+<style lang="scss">
+ @import './dataInfo.scss'
+</style>

+ 55 - 0
ui/src/views/system/document/category/components/dataList.scss

@@ -0,0 +1,55 @@
+
+.data-list{
+
+
+  .btns{
+    display:flex;
+    flex-direction: row;
+  }
+
+
+
+
+  .page-info{
+    display:flex;
+    flex-direction: row;
+    margin-top:10px;
+    .opt{
+      width:50%;
+      display:flex;
+      flex-direction:row;
+      justify-content: flex-start;
+      align-items: center;
+
+      .pick{
+        width:100px;
+
+      }
+      .section{
+        margin-left:10px;
+      }
+    }
+
+    .page-data{
+
+      display:flex;
+      flex-direction: row;
+      justify-content: space-between;
+      align-items: center;
+      width:100%;
+      .page-item{
+          width:100%;
+          // border:1px solid red;
+      }
+      .page-opt{
+        width:100%;
+        // border:1px solid red;
+        display: flex;
+        justify-content: flex-end;
+      }
+    }
+
+
+  }
+
+}

+ 166 - 0
ui/src/views/system/document/category/components/dataList.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="data-list">
+
+
+      <el-table :data="dataList" style="width: 100%" header-row-class-name="headerBg" empty-text="没有分类信息">
+        <el-table-column prop="id" label="ID" align="center" width="80"/>
+        <el-table-column prop="name" label="分类名称" align="left" width="180"/>
+        <el-table-column prop="intro" label="介绍" align="left" min-width="180"/>
+
+        <el-table-column prop="createTime" label="创建时间" align="center" width="180"/>
+        <el-table-column prop="status" label="分类状态" align="center" width="180">
+          <template #default="scope">
+              <div v-if="scope.row.status==5">启用</div>
+              <div v-if="scope.row.status==6">停用</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" align="center" width="180">
+          <template #default="scope">
+            <div class="btns">
+               <el-button type="primary" size="small" @click="btnEdit(scope.row)"><svg-icon icon-class="edit"/>编辑</el-button>
+              <el-button type="danger" size="small" @click="btnDelete(scope.row.id)"><svg-icon icon-class="delete"/>删除</el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+
+
+      <div class="page-info">
+      	<el-pagination
+      	v-model:currentPage="queryForm.page"
+      	:page-size="queryForm.pageSize"
+      	:total="recordCount"
+      	:page-count="pageTotal"
+      	 background
+      	 layout="prev, pager, next"
+      	 @current-change="ChangePage"
+      	 >
+      	 </el-pagination>
+      </div>
+
+     <el-dialog  :visible.sync="dialogVisible" append-to-body v-el-drag-dialog  width="50%" custom-class="prod-verify" title="修改文档分类">
+          <dataInfo :id="currentDataId" @onClose="onClose"></dataInfo>
+      </el-dialog>
+
+  </div>
+
+
+</template>
+
+<script>
+  import{searchDocumentCategory,deleteDocumentCategory} from'@/api/document';
+  import dataInfo from './dataInfo.vue';
+  import elDragDialog from '@/directive/el-drag-dialog'
+  export default{
+    components:{
+      dataInfo,
+    },
+    directives: { elDragDialog },
+    props:{
+      queryForm:{
+        type:Object,
+        default:()=>{
+          return {
+            page:1,
+            pageSize:10,
+            name:'',
+            status:''
+          }
+        }
+      }
+    },
+    watch:{
+
+        'queryForm': {
+          handler: function(val) {
+
+            this.search();
+          },
+          // immediate: true,//立即执行
+          deep: true
+
+        },
+    },
+    data(){
+      return{
+        dialogVisible:false,//控制产品审核
+        currentDataId:0,
+        recordCount:0,
+        pageTotal:1,
+        dataList:[],
+        currentData:{},
+      }
+    },
+    mounted() {
+      this.search();
+    },
+    methods:{
+
+
+      onSearchSub(e){
+        let _this=this;
+        _this.queryForm.page=1;
+        _this.search();
+      },
+
+      searchData(){
+        let _this=this;
+        _this.dialogVisible=false;
+        _this.search();
+      },
+      btnDelete(e){
+        let _this=this;
+        let par={
+          id:e,
+        }
+        _this.$confirm("您是否确认删除该记录?","提示",{
+          confirmButtonText: '确认',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then((res)=>{
+          // console.log("AAA")
+             deleteDocumentCategory(par).then(res=>{
+               _this.search();
+             })
+        }).catch(()=>{
+        })
+      },
+
+      //编辑
+      btnEdit(e){
+        let _this=this;
+        _this.currentDataId=e.id;
+        _this.dialogVisible=true;
+      },
+
+      onClose(){
+        let _this=this;
+        _this.currentDataId=0;
+        _this.dialogVisible=false;
+        _this.search();
+      },
+
+      //搜索
+      search(){
+        let _this=this;
+        searchDocumentCategory(_this.queryForm).then(res=>{
+          if(!res)return;
+          _this.dataList=res.data.dataList;
+          _this.recordCount=res.data.totalRecord;
+          _this.pageTotal=res.data.pageTotal;
+        })
+      },
+      //修改分页
+      ChangePage(e){
+      	let _this=this;
+      	_this.queryForm.page=e;
+      	_this.search();
+      },
+    }
+  }
+</script>
+
+<style lang="scss">
+@import './dataList.scss';
+</style>

+ 3 - 0
ui/src/views/system/document/category/components/dataSearch.scss

@@ -0,0 +1,3 @@
+.data-search{ 
+  padding:20px 10px 0px 10px;
+}

+ 98 - 0
ui/src/views/system/document/category/components/dataSearch.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="data-search">
+    <el-form :inline="true" :model="queryForm" class="demo-form-inline">
+
+        <el-form-item label="分类名称:">
+          <el-input v-model="queryForm.name" placeholder="请选择分类名称"></el-input>
+        </el-form-item>
+        <el-form-item label="分类状态:">
+          <el-select v-model="queryForm.status" class="m-2" placeholder="请选择分类状态" size="large">
+              <el-option
+                v-for="item in statusOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" @click="onSubmit">  <svg-icon icon-class="search"/> 搜索</el-button>
+          <el-button type="success" @click="onCreate" >  <svg-icon icon-class="edit"/> 创建分类</el-button>
+        </el-form-item>
+      </el-form>
+
+
+
+      <el-dialog  :visible.sync="dialogVisible" append-to-body v-el-drag-dialog  width="50%" custom-class="prod-verify" title="创建文档分类">
+         <dataInfo @onClose="onClose" ></dataInfo>
+      </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import dataInfo from './dataInfo.vue';
+import elDragDialog from '@/directive/el-drag-dialog'
+export default{
+  components:{
+    dataInfo,
+  },
+  directives: { elDragDialog },
+  props:{
+    queryForm:{
+        type:Object,
+        default:()=>{
+          return {
+             page:1,
+             pageSize:10,
+             name:'',
+             status:''
+          }
+        }
+    },
+  },
+  data(){
+    return{
+        statusOptions:[
+          {
+            value:'',
+            label:"请选择状态"
+          },
+          {
+            value:5,
+            label:'启用'
+          },
+          {
+            value:6,
+            label:"停用"
+          },
+        ],
+        dialogVisible:false,
+    }
+  },
+  created() {
+
+  },
+  methods:{
+
+    onCreate(e){
+      this.dialogVisible=true;
+    },
+
+    onClose(e){
+      this.dialogVisible=false;
+      this.onSubmit();
+    },
+    //搜索
+    onSubmit(){
+      this.queryForm.page=1;
+      this.$emit("bindSetQuery",this.queryForm);
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@import "./dataSearch.scss"
+</style>

+ 3 - 0
ui/src/views/system/document/category/components/index.js

@@ -0,0 +1,3 @@
+export { default as dataSearch } from './dataSearch'
+export { default as dataList } from './dataList'
+export { default as dataInfo } from './dataInfo'

+ 4 - 0
ui/src/views/system/document/category/search.scss

@@ -0,0 +1,4 @@
+.project-search{
+  padding:30px;
+  font-size:12px;
+}

+ 38 - 0
ui/src/views/system/document/category/search.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="project-search">
+    <dataSearch @bindSetQuery="setQuery"></dataSearch>
+    <dataList :queryForm="queryForm"></dataList>
+  </div>
+
+</template>
+
+<script>
+import {dataSearch,dataList} from "./components"
+export default{
+  components:{
+    dataSearch,
+    dataList,
+  },
+  data(){
+    return{
+        queryForm:{
+          page:1,
+          pageSize:10,
+          name:'',
+          status:''
+        },
+    }
+  },
+  methods:{
+
+    setQuery(e){
+      this.queryForm={...e};
+    }
+
+  }
+}
+</script>
+
+<style lang="scss">
+@import './search.scss'
+</style>

+ 81 - 0
ui/src/views/system/document/com/components/Attribute/Directory/index.scss

@@ -0,0 +1,81 @@
+.com-directory{
+  display:flex;
+  width:100%;
+  flex-direction:column;
+  height:740px;
+  overflow: auto;
+  justify-content: flex-start;
+  align-items: flex-start;
+  margin-top:10px;
+  overflow: hidden;
+  .com-title{
+    padding-left:10px;
+    margin-top:10px;
+    font-size:12px;
+    height:30px;
+    font-weight: bold;
+  }
+  .com-items{
+
+    width:100%;
+    max-height:50%;
+    overflow: hidden;
+    overflow-y:auto;
+
+
+    .com-item{
+        width:90%;
+        height: 40px;
+        display: -webkit-box;
+        display: -ms-flexbox;
+        display: flex;
+        -webkit-box-align: center;
+        -ms-flex-align: center;
+        align-items: center;
+        -webkit-box-pack: justify;
+        -ms-flex-pack: justify;
+        justify-content: space-between;
+        padding: 0 10px;
+        font-size: 14px;
+        cursor: all-scroll;
+        color: #323233;
+        border-radius: 2px;
+        background-color: #fff;
+        -webkit-box-shadow: 0 0 4px 0 rgba(10,42,97,.2);
+        box-shadow: 0 0 4px 0 rgba(10,42,97,.2);
+
+        margin-top:10px;
+        margin-left:10px;
+        margin-right:10px;
+
+        .icon{
+          display:flex;
+          width:10%;
+          height:100%;
+          justify-content: center;
+          align-items: center;
+        }
+        .name{
+          display:flex;
+          width:90%;
+          height:100%;
+          justify-content: flex-start;
+          align-items: center;
+        }
+        .remove{
+          display:flex;
+          width:15%;
+          height:100%;
+          justify-content: center;
+          align-items: center;
+        }
+    }
+  }
+}
+
+.dirceForm{
+  width: 100%;
+  margin: 0 10px 0;
+  max-height: 50%;
+  overflow: auto;
+}

+ 69 - 0
ui/src/views/system/document/com/components/Attribute/Directory/index.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="com-directory" v-if="coms != null && coms.length > 0 && size > 0">
+    <el-form class="dirceForm">
+      <template v-for="(sub,itIndex) in coms"  >
+        <template v-for="(item,itemIndex) in sub.attrs">
+          <el-form-item :key="'item-' + itIndex + '-' + itemIndex"  v-if="item.type=='Directory'" :label="item.id+':'"><!-- + ' (引用名:[' + item.id + '])' -->
+            <el-input type="textarea" v-model="item.content"></el-input>
+          </el-form-item>
+        </template>
+        <!-- 文本开始-->
+      </template>
+    </el-form>
+    <div class="com-title">目录信息</div>
+    <div class="com-items">
+      <template v-for="(it,itIndex) in coms" >
+        <template
+          v-for="(item, itemIndex) in it.attrs"  
+        >
+          <div class="com-item" :key="'item-' + itIndex + '-' + itemIndex" v-if="item.type == 'Directory'">
+            <div class="icon"><svg-icon icon-class="directory" /></div>
+            <div class="name">
+              {{ item.content != "" ? item.content : item.name }}
+            </div>
+            <!-- <div class="remove" @click="onRemove(index)"><svg-icon icon-class="delete" /></div> -->
+          </div>
+        </template>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    coms: {
+      type: Array,
+      default: null,
+    },
+  },
+  watch: {
+    coms: {
+      handler(val) {
+        console.log(val);
+        if (val == null) return;
+        this.size = 0;
+        for (var i = 0; i < val.length; i++) {
+          for (var j = 0; j < val[i].attrs.length; j++) {
+            if (val[i].attrs[j].type == "Directory") {
+              this.size++;
+            }
+          }
+        }
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      size: 0,
+    };
+  },
+};
+</script>
+
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 108 - 0
ui/src/views/system/document/com/components/Attribute/attributeVar/index.scss

@@ -0,0 +1,108 @@
+.editor-attribute{
+  display:flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: flex-start;
+  padding:10px;
+  width:100%;
+  height:740px;
+  overflow: hidden;
+  overflow-y: auto;
+  .el-collapse{
+    width:100%;
+  }
+
+  .el-collapse-item{
+    border:1px solid #e5e5e5;
+    width:100%;
+    padding:10px;
+    margin-top:10px;
+    border-radius: 10px;
+  }
+  .el-collapse-item__header,.el-collapse-item__wrap{
+      border:none!important;
+  }
+
+
+  .el-form{
+    width:100%;
+    .line{
+
+      width:100%;
+    }
+
+
+    .attr-items{
+      display:flex;
+      flex-direction: column;
+      .attr-item{
+        height:50px;
+        font-size:12px;
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+      }
+    }
+
+    .el-form-item{
+      margin-bottom:0px;
+      .el-form-item__label{
+        text-align:left;
+      }
+      .el-form-item__content{
+        text-align:left;
+        .input-item{
+          width:100%;
+        }
+        .input-num{
+          width:40%;
+          margin-left:5px;
+        }
+        .btn-save{
+          display:flex;
+          justify-content:flex-end;
+          align-items: center;
+          width:100%;
+          height:50px;
+        }
+
+        .header-list{
+           border:1px solid #CCC;
+           border-radius: 10px;
+           height:100px;
+          clear:both;
+          display:flex;
+          flex-direction: column;
+          justify-content: flex-start;
+          align-items: flex-start;
+          overflow: hidden;
+          overflow-y: auto;
+
+          .item{
+            padding-left:10px;
+            width:100%;
+            height:20px;
+            line-height: 20px;
+            color:#000;
+          }
+          .active-item{
+            background:#f1f1f1;
+            color:#cfcfcf!important;
+          }
+        }
+        .opt{
+          width:100%;
+          height:50px;
+          display:flex;
+          flex-direction: row;
+          justify-content: space-evenly;
+          align-items: center;
+        }
+
+      }
+    }
+  }
+
+}
+
+

+ 289 - 0
ui/src/views/system/document/com/components/Attribute/attributeVar/index.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="editor-attribute" v-if="com != null">
+    <el-form>
+      <template v-for="(sub, subIndex) in filteredAttrs">
+        <template v-if="com.type == 'TextArea'">
+          <template v-if="isTextAreaType(sub.type)">
+            <el-form-item :label="sub.id + ':'"
+              ><!--  + ' (引用名:[' + sub.id + '])' -->
+              <!-- <el-input
+                type="textarea"
+                @input="updateAllReferences(sub, $event)"
+                v-model="sub.content"
+              ></el-input> -->
+              <span @click="toggleBackground(sub.id)" style="cursor: pointer">{{
+                sub.content
+              }}</span>
+            </el-form-item>
+          </template>
+          <template v-if="sub.type == 'formual'">
+            <el-form-item :label="sub.id + ':'"
+              ><!-- + ' (引用名:[' + sub.id + '])' -->
+              <!-- <el-input type="textarea" v-model="sub.formula"></el-input> -->
+              <span @click="toggleBackground(sub.id)" style="cursor: pointer">{{
+                sub.formula
+              }}</span>
+            </el-form-item>
+          </template>
+          <template v-if="sub.type == 'sourceData'">
+            <el-form-item :label="sub.id + ':'"
+              ><!-- + ' (引用名:[' + sub.id + '])' -->
+              {{ formatSourceData(sub.id, sub.formula) }}
+            </el-form-item>
+          </template>
+        </template>
+      </template>
+    </el-form>
+
+    <el-dialog
+      :visible.sync="dialogVisible"
+      append-to-body
+      v-el-drag-dialog
+      width="300"
+      custom-class="prod-verify"
+      title="编辑表头"
+    >
+      <headerSetting
+        :headerItemName="headerItemName"
+        @onSetHeader="onSetHeader"
+      ></headerSetting>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import headerSetting from "../headerSetting";
+import elDragDialog from "@/directive/el-drag-dialog";
+import { getAllCategory, createTemplate, updateTemplate } from "@/api/template";
+import { searchSourceDataCategory } from "@/api/sourceData";
+import { searchDocumentCategory } from "@/api/document";
+import { searchTemplateCategory, searchTemplate } from "@/api/template";
+
+export default {
+  name: "attributes",
+  components: { headerSetting },
+  directives: { elDragDialog },
+  emits: ["onRefresh"],
+  props: {
+    com: {
+      type: Object,
+      default: () => null,
+    },
+  },
+  watch: {
+    com: {
+      handler(newVal) {
+        if (newVal && newVal.content) {
+          this.processContent();
+        }
+      },
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      activeNames: "0",
+      categoryList: [],
+      articleCategoryList: [],
+      activeHeaderIndex: -1,
+      headerItemName: "",
+      props: {
+        value: "id",
+        label: "name",
+        children: "children",
+      },
+      processedAttrs: [],
+    };
+  },
+  computed: {
+    filteredAttrs() {
+      console.log("processedAttrs", this.processedAttrs);
+      return this.processedAttrs;
+    },
+  },
+  mounted() {
+    this.initCategoryList();
+    if (this.com && this.com.content) {
+      this.processContent();
+    }
+  },
+  methods: {
+    toggleBackground(id) {
+      const element = document.getElementById(id);
+      if (element) {
+        if (element.style.backgroundColor === "yellow") {
+          element.style.backgroundColor = "";
+        } else {
+          element.style.backgroundColor = "yellow";
+        }
+      }
+    },
+    updateAllReferences(val, newValue) {
+      this.com.attrs.map((el) => {
+        if (el.name == val.name) {
+          el.content = newValue;
+        }
+      });
+
+      /*  this.processedAttrs.forEach((attr) => {
+        if (attr.id === id) {
+          attr.content = newValue;
+        }
+      });
+
+      // Update the original com.attrs as well
+      if (this.com && this.com.attrs) {
+        this.com.attrs.forEach((attr) => {
+          if (attr.id === id) {
+            attr.content = newValue;
+          }
+        });
+      }
+
+      // Update the content of the component
+      if (this.com && this.com.content) {
+        const regex = new RegExp(`\\{\\{${id}\\}\\}`, "g");
+        this.com.content = this.com.content.replace(regex, `{{${newValue}}}`);
+      } */
+    },
+    isTextAreaType(type) {
+      return ![
+        "pager",
+        "constant",
+        "attr",
+        "formual",
+        "sourceData",
+        "Directory",
+      ].includes(type);
+    },
+
+    extractTemplates(content) {
+      const templateRegex = /\{\{(.*?)\}\}/g;
+      let match;
+      const templates = [];
+
+      while ((match = templateRegex.exec(content)) !== null) {
+        templates.push(match[1].trim());
+      }
+
+      return templates;
+    },
+
+    processContent() {
+      if (this.com && this.com.content) {
+        const templates = this.extractTemplates(this.com.content);
+
+        if (this.com.attrs && Array.isArray(this.com.attrs)) {
+          this.processedAttrs = this.com.attrs.filter(
+            (el) => templates.includes(el.id) || el.type === "formual"
+          );
+        }
+      } else {
+        this.processedAttrs = [];
+      }
+    },
+    formatSourceData(id, e) {
+      const pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      const items = e.match(pattern);
+      const dataIndex = items[4].split(",");
+      return `${id}=${items[2]}.${items[3]}.${String.fromCharCode(
+        65 + parseInt(dataIndex[1])
+      )}${parseInt(dataIndex[0]) + 1}`;
+    },
+    async onSaveTemplate(e) {
+      const data = { ...e, attrs: JSON.stringify(e.attrs), status: 5 };
+      delete data.category;
+
+      if (data.id === undefined || this.saveAs) {
+        const res = await createTemplate(data);
+        if (res.status === 200) {
+          data.id = res.data;
+          this.com.id = res.data;
+          this.$alert("模板信息保存成功");
+          this.$emit("onRefresh");
+          this.saveAs = false;
+        }
+      } else {
+        const res = await updateTemplate(data);
+        if (res.status === 200) {
+          this.$alert("模板信息更新成功");
+          this.$emit("onRefresh");
+        }
+      }
+    },
+    async initCategoryList() {
+      const res = await searchTemplateCategory({
+        page: 1,
+        pageSize: 99,
+        status: 5,
+      });
+      if (res.status === 200) {
+        this.categoryList = res.data.dataList || [];
+        for (const category of this.categoryList) {
+          category.dataList = await this.getTemplateList(category.id);
+        }
+      }
+    },
+    async getTemplateList(categoryId) {
+      const res = await searchTemplate({
+        page: 1,
+        pageSize: 999,
+        category_id: categoryId,
+        status: 5,
+      });
+      if (res.status === 200) {
+        return res.data.dataList.map((item) => ({
+          ...item,
+          attrs: JSON.parse(item.attrs),
+        }));
+      }
+      return [];
+    },
+    onModify(index, subIndex, headerIndex) {
+      this.currentIndex = index;
+      this.componentIndex = subIndex;
+      this.activeHeaderIndex = headerIndex;
+      this.headerItemName =
+        this.components[index].components[subIndex].attrs.tableHeader[
+          headerIndex
+        ];
+      this.dialogVisible = true;
+    },
+    onSetHeader(name) {
+      if (this.currentIndex >= 0) {
+        const header =
+          this.components[this.currentIndex].components[this.componentIndex]
+            .attrs.tableHeader;
+        if (this.activeHeaderIndex >= 0) {
+          header[this.activeHeaderIndex] = name;
+        } else {
+          header.push(name);
+        }
+      }
+      this.$emit("onSetComponents", [...this.components]);
+      this.dialogVisible = false;
+    },
+    onInsert(index, subIndex) {
+      this.currentIndex = index;
+      this.componentIndex = subIndex;
+      this.headerItemName = "";
+      this.dialogVisible = true;
+    },
+    onDeleteHeaderItem(index, subIndex, headerIndex) {
+      this.components[index].components[subIndex].attrs.tableHeader.splice(
+        headerIndex,
+        1
+      );
+      this.$emit("onSetComponents", [...this.components]);
+    },
+    onSetActiveIndex(e) {
+      this.activeHeaderIndex = e === this.activeHeaderIndex ? -1 : e;
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 69 - 0
ui/src/views/system/document/com/components/Attribute/comList/index.scss

@@ -0,0 +1,69 @@
+.com-list{
+  display:flex;
+  width:100%;
+  flex-direction:column;
+  height:400px;
+  overflow: hidden;
+
+  .com-list-title{
+    font-size:14px;
+    font-weight:bold;
+    padding-left:10px;
+  }
+  .com-items{
+     display:flex;
+     width:100%;
+    flex-direction:column;
+    justify-content: flex-start;
+    align-items: flex-start;
+    overflow: hidden;
+    overflow-y: auto;
+    .com-item{
+        width:90%;
+        height: 40px;
+        display: -webkit-box;
+        display: -ms-flexbox;
+        display: flex;
+        -webkit-box-align: center;
+        -ms-flex-align: center;
+        align-items: center;
+        -webkit-box-pack: justify;
+        -ms-flex-pack: justify;
+        justify-content: space-between;
+        padding: 0 10px;
+        font-size: 14px;
+        cursor: all-scroll;
+        color: #323233;
+        border-radius: 2px;
+        background-color: #fff;
+        -webkit-box-shadow: 0 0 4px 0 rgba(10,42,97,.2);
+        box-shadow: 0 0 4px 0 rgba(10,42,97,.2);
+
+        margin-top:10px;
+        margin-left:10px;
+        margin-right:10px;
+
+        .icon{
+          display:flex;
+          width:10%;
+          height:100%;
+          justify-content: center;
+          align-items: center;
+        }
+        .name{
+          display:flex;
+          width:75%;
+          height:100%;
+          justify-content: flex-start;
+          align-items: center;
+        }
+        .remove{
+          display:flex;
+          width:15%;
+          height:100%;
+          justify-content: center;
+          align-items: center;
+        }
+    }
+   }
+}

+ 62 - 0
ui/src/views/system/document/com/components/Attribute/comList/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="com-list" v-if="coms.length>0">
+    <div class="com-list-title">模块管理</div>
+    <div class="com-items">
+      <draggable v-model="coms" @update="datadragEnd" :options="{animation:200}" style="width:100%;">
+        <template v-for="(it,index) in coms">
+          <div :key="index" class="com-item" draggable>
+            <div class="icon"><svg-icon icon-class='dashBoard1' /></div>
+            <div class="name" @click="onSetActive(index)">{{it.name}}</div>
+            <div class="remove" v-if="type!=='document'" @click="onRemove(index)"><svg-icon style="cursor: pointer;" icon-class="delete" /></div>
+          </div>
+        </template>
+      </draggable>
+    </div>
+
+  </div>
+</template>
+
+<script>
+  import draggable from 'vuedraggable'
+  export default {
+    name: "comList",
+    emits: ['onRemove','onRebuild','onSetActive'],
+    components: {
+      draggable
+    },
+    props: {
+      coms: {
+        type: Array,
+        default: [],
+      },
+    },
+    data() {
+      return {
+        type:''
+      }
+    },
+    mounted(){
+      this.type=this.$route.query.type
+    },
+    methods: {
+
+
+      async datadragEnd(event) {
+        event.preventDefault();
+        let item=this.coms;
+        this.$emit("onRebuild",item);
+      },
+
+      onSetActive(e){
+        this.$emit("onSetActive",e);
+      },
+      onRemove(e) {
+        this.$emit("onRemove", e);
+      },
+    }
+  }
+</script>
+
+<style lang="scss">
+  @import './index.scss';
+</style>

+ 12 - 0
ui/src/views/system/document/com/components/Attribute/docAttr/index.scss

@@ -0,0 +1,12 @@
+.doc-attr{
+  display:flex;
+  flex-direction:column;
+  padding:10px;
+  width:100%;
+  height:500px;
+ overflow: hidden;
+ overflow-y: auto;
+  .el-form{
+      width:100%;
+  }
+}

+ 127 - 0
ui/src/views/system/document/com/components/Attribute/docAttr/index.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="doc-attr">
+    <el-form>
+      <el-form-item label="标题:">
+        <el-input
+          v-model="attrs.title"
+          style="width: 100%"
+          class="input-item"
+          placeholder="请填写模版名称"
+          :disabled="articleId"
+        />
+      </el-form-item>
+
+      <!--<el-form-item label="选择分类:">
+        <el-select v-model="attrs.category_id" style="width:100%;" placeholder="请选择所属分类">
+          <el-option
+                v-for="(item,index) in articleCategoryList"
+                :key="index"
+                :label="item.name"
+                :value="item.id"
+              />
+        </el-select>
+      </el-form-item>
+       <el-form-item label="适用产品:">
+        <el-select v-model="attrs.linkProduct" :multiple="true" style="width:100%;" @change="onChangeProduct"  placeholder="请选择适用产品">
+          <el-option
+                v-for="(item,index) in productList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="适用项目:">
+        <el-select v-model="attrs.linkProject" :multiple="true" style="width:100%;" @change="onChangeProject"  placeholder="请选择适用项目">
+          <el-option
+                v-for="(item,index) in projectList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+        </el-select>
+      </el-form-item> -->
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { searchDocumentCategory } from "@/api/document";
+import { searchProduct } from "@/api/product";
+import { searchProject } from "@/api/project";
+export default {
+  name: "docAttr",
+  props: {
+    attrs: {
+      type: Object,
+      default: () => {
+        return {
+          category_id: "",
+          title: "",
+          linkProduct: [],
+          linkProject: [],
+        };
+      },
+    },
+  },
+  watch: {
+    attrs: {
+      handler(val) {},
+      immediate: true, //立即执行
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      articleCategoryList: [],
+      productList: [],
+      projectList: [],
+      articleId:0
+    };
+  },
+  created() {
+    this.initArticleCategoryList();
+    this.initProjectList();
+    this.initGoodsList();
+    this.articleId=this.$route.query.articleId
+  },
+  methods: {
+    onChangeProduct(e) {
+      let _this = this;
+    },
+
+    onChangeProject(e) {},
+    //初始化产品列表
+    initProjectList() {
+      let _this = this;
+      searchProject({ page: 1, pageSize: 9999, status: 5 }).then((res) => {
+        if (res.status != 200) return;
+        _this.projectList = res.data.dataList;
+      });
+    },
+    //初始化产品列表
+    initGoodsList() {
+      let _this = this;
+      searchProduct({ page: 1, pageSize: 9999, status: 5 }).then((res) => {
+        if (res.status != 200) return;
+        _this.productList = res.data.dataList;
+      });
+    },
+    //加载文档分类
+    initArticleCategoryList() {
+      let _this = this;
+      searchDocumentCategory({ page: 1, pageSize: 99, status: 5 }).then(
+        (res) => {
+          if (res.status != 200) return;
+          _this.articleCategoryList = res.data.dataList;
+        }
+      );
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 3 - 0
ui/src/views/system/document/com/components/Attribute/headerSetting/index.scss

@@ -0,0 +1,3 @@
+.header-setting{
+  
+}

+ 50 - 0
ui/src/views/system/document/com/components/Attribute/headerSetting/index.vue

@@ -0,0 +1,50 @@
+<template>
+  <div class="header-setting">
+    <el-input v-model="form.name" placeholder="请填写表头"></el-input>
+    <div style="text-align:right; margin-top:20px;">
+        <el-button type="warning" @click="btnSave">确认保存</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+  export default{
+    emits:['onSetHeader'],
+    props:{
+      headerItemName:{
+        type:String,
+        default:"",
+      }
+    },
+    watch:{
+       'headerItemName':{
+         handler(val){
+          if(val==undefined)return;
+           this.form.name=val;
+         },
+         immediate:true,
+         deep:true,
+       }
+    },
+    data(){
+     return {
+       form:{
+          name:"",
+       }
+     }
+    },
+    methods:{
+
+      btnSave(e){
+        let _this=this;
+        _this.$emit("onSetHeader",_this.form.name);
+        // console.log(_this.form.name);
+      }
+    }
+  }
+</script>
+
+
+<style lang="scss">
+@import './index.scss';
+</style>

+ 108 - 0
ui/src/views/system/document/com/components/Attribute/index.scss

@@ -0,0 +1,108 @@
+.editor-attribute{
+  display:flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: flex-start;
+  padding:10px;
+  width:100%;
+  height:500px;
+  overflow: hidden;
+  overflow-y: auto;
+  .el-collapse{
+    width:100%;
+  }
+
+  .el-collapse-item{
+    border:1px solid #e5e5e5;
+    width:100%;
+    padding:10px;
+    margin-top:10px;
+    border-radius: 10px;
+  }
+  .el-collapse-item__header,.el-collapse-item__wrap{
+      border:none!important;
+  }
+
+
+  .el-form{
+    width:100%;
+    .line{
+
+      width:100%;
+    }
+
+
+    .attr-items{
+      display:flex;
+      flex-direction: column;
+      .attr-item{
+        height:50px;
+        font-size:12px;
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+      }
+    }
+
+    .el-form-item{
+      margin-bottom:0px;
+      .el-form-item__label{
+        text-align:left;
+      }
+      .el-form-item__content{
+        text-align:left;
+        .input-item{
+          width:100%;
+        }
+        .input-num{
+          width:40%;
+          margin-left:5px;
+        }
+        .btn-save{
+          display:flex;
+          justify-content:flex-end;
+          align-items: center;
+          width:100%;
+          height:50px;
+        }
+
+        .header-list{
+           border:1px solid #CCC;
+           border-radius: 10px;
+           height:100px;
+          clear:both;
+          display:flex;
+          flex-direction: column;
+          justify-content: flex-start;
+          align-items: flex-start;
+          overflow: hidden;
+          overflow-y: auto;
+
+          .item{
+            padding-left:10px;
+            width:100%;
+            height:20px;
+            line-height: 20px;
+            color:#000;
+          }
+          .active-item{
+            background:#f1f1f1;
+            color:#cfcfcf!important;
+          }
+        }
+        .opt{
+          width:100%;
+          height:50px;
+          display:flex;
+          flex-direction: row;
+          justify-content: space-evenly;
+          align-items: center;
+        }
+
+      }
+    }
+  }
+
+}
+
+

+ 344 - 0
ui/src/views/system/document/com/components/Attribute/index.vue

@@ -0,0 +1,344 @@
+<template>
+  <div class="editor-attribute" v-if="com != null">
+    <el-form>
+      <el-form-item label="模块分类:">
+        <!--  <el-select
+          v-model="com.category_id"
+          placeholder="请选择模块分类"
+          size="large"
+          style="width: 100%"
+          >
+          <el-option
+            v-for="item in categoryList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select> -->
+        <!-- @change="onChangeCategory" -->
+        <el-cascader
+          v-model="com.category_id"
+          :options="categoryList"
+          clearable
+          :props="props"
+          :show-all-levels="false"
+          @change="onChangeCategory"
+          placeholder="请选择模块分类"
+        />
+      </el-form-item>
+     <!--  <el-form-item label="模块引用名:">
+        <el-input
+          v-model="com.code"
+          class="input-item"
+          placeholder="请填写模块引用名"
+        />
+      </el-form-item> -->
+      <el-form-item label="模块名称:">
+        <el-input
+          v-model="com.name"
+          class="input-item"
+          placeholder="请填写模块名称"
+        />
+      </el-form-item>
+      <el-form-item label="模块描述:">
+        <el-input
+          type="textarea"
+          class="input-item"
+          v-model="com.intro"
+          placeholder="请填写模块介绍"
+        />
+      </el-form-item>
+      <el-form-item>
+        <div class="btn-save">
+          <el-button type="primary" size="mini" @click="onSaveTemplate(com)"
+            >保存模块</el-button
+          >
+          <el-button
+            type="primary"
+            size="mini"
+            @click="onSaveAs(com)"
+            v-if="com.id > 0"
+            >另存为</el-button
+          >
+        </div>
+      </el-form-item>
+      <!-- 文本开始-->
+      <!-- <template v-for="(sub, subIndex) in com.attrs">
+       
+        <template v-if="com.type == 'TextArea'">
+          <template
+            v-if="
+              sub.type != 'pager' &&
+              sub.type != 'constant' &&
+              sub.type != 'attr' &&
+              sub.type != 'formual' &&
+              sub.type != 'sourceData'&&
+              sub.type !='Directory'
+            "
+          >
+            <el-form-item :label="sub.name + ' (引用名:[' + sub.id + '])'">
+              <el-input type="textarea" v-model="sub.content"></el-input>
+            </el-form-item>
+          </template>
+
+          <template v-if="sub.type == 'formual'">
+            <el-form-item :label="sub.name + ' (引用名:[' + sub.id + '])'">
+              <el-input type="textarea" v-model="sub.formula"></el-input>
+            </el-form-item>
+          </template>
+
+          <template v-if="sub.type == 'sourceData'">
+            <el-form-item :label="sub.name + ' (引用名:[' + sub.id + '])'">
+              {{ formatSourceData(sub.id, sub.formula) }}
+            </el-form-item>
+          </template>
+        </template>
+      </template> -->
+    </el-form>
+
+    <el-dialog
+      :visible.sync="dialogVisible"
+      append-to-body
+      v-el-drag-dialog
+      width="300"
+      custom-class="prod-verify"
+      title="编辑表头"
+    >
+      <headerSetting
+        :headerItemName="headerItemName"
+        @onSetHeader="onSetHeader"
+      ></headerSetting>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import headerSetting from "./headerSetting";
+import elDragDialog from "@/directive/el-drag-dialog";
+
+import {
+  getAllCategory,
+  createTemplate,
+  updateTemplate,
+  getAllList,
+} from "@/api/template";
+import { searchSourceDataCategory } from "@/api/sourceData";
+import { searchDocumentCategory } from "@/api/document";
+import { searchTemplateCategory, searchTemplate } from "@/api/template";
+export default {
+  name: "attributes",
+  components: {
+    headerSetting,
+  },
+  directives: { elDragDialog },
+  emits: ["onRefresh"],
+  props: {
+    com: {
+      type: Object,
+      default: () => {
+        return null;
+      },
+    },
+  },
+  watch: {},
+  data() {
+    return {
+      dialogVisible: false,
+      activeNames: "0",
+      categoryList: [],
+      articleCategoryList: [],
+      activeHeaderIndex: -1,
+      headerItemName: "",
+      props: {
+        value: "id",
+        label: "name",
+        children: "children",
+        checkStrictly: true,
+      },
+    };
+  },
+  mounted() {
+    this.initCategoryList();
+  },
+  methods: {
+    formatSourceData(id, e) {
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      let items = e.match(reg);
+      let data = id + "=" + items[2] + "." + items[3];
+
+      var dataIndex = items[4];
+      dataIndex = items[4].split(",");
+      data +=
+        "." +
+        String.fromCharCode(65 + parseInt(dataIndex[1])) +
+        (parseInt(dataIndex[0]) + 1);
+      return data;
+    },
+
+    onSaveAs(e) {
+      this.saveAs = true;
+      this.onSaveTemplate();
+    },
+
+    onChangeCategory(value) {
+      let _this = this;
+      /* _this.com.category_id = e[1]; */
+      this.com.category_id = value[value.length - 1];
+    },
+
+    onSaveTemplate(e) {
+      let _this = this;
+      /* e.category_id='1' */
+      let data = JSON.parse(JSON.stringify(e));
+      data.attrs = JSON.stringify(data.attrs);
+      data.code=data.name
+      data.status = 5;
+      data.category_id = this.com.category_id;
+      if (data.id == undefined || this.saveAs) {
+        createTemplate(data).then((res) => {
+          if (res.status != 200) return;
+          data.id = res.data;
+          _this.com.id = res.data;
+          _this.$alert("模板信息保存成功");
+          _this.$emit("onRefresh");
+          this.saveAs = false;
+        });
+      } else {
+        updateTemplate(data).then((res) => {
+          if (res.status != 200) return;
+          _this.$alert("模板信息更新成功");
+          _this.$emit("onRefresh");
+        });
+      }
+    },
+
+    /* initCategoryList(){
+          let _this=this;
+          searchSourceDataCategory({page:1,pageSize:99,status:''}).then(res=>{
+            _this.categoryList=res.data.sourcecategories;
+            
+          })
+      }, */
+    processDataForCascader(data) {
+      console.log("Raw data:", data); // 调试原始数据
+      const idMap = new Map();
+      const rootItems = [];
+
+      // First pass: create a map of all items
+      data.forEach((item) => {
+        const processedItem = { ...item, children: [] };
+        idMap.set(item.id, processedItem);
+      });
+
+      // Second pass: build the tree structure
+      data.forEach((item) => {
+        if (item.parent_id === "0" || item.parent_id === 0) {
+          rootItems.push(idMap.get(item.id));
+        } else {
+          const parent = idMap.get(parseInt(item.parent_id));
+          if (parent) {
+            parent.children.push(idMap.get(item.id));
+          } else {
+            console.warn(
+              `Parent with id ${item.parent_id} not found for item:`,
+              item
+            );
+            rootItems.push(idMap.get(item.id)); // 如果找不到父项,作为根项添加
+          }
+        }
+      });
+
+      console.log("Processed data:", rootItems); // 调试处理后的数据
+      return rootItems;
+    },
+    //获取模板分类信息
+    async initCategoryList() {
+      let _this = this;
+      let res = await getAllList();
+      this.categoryList = this.processDataForCascader(res.data);
+      /* let res = await searchTemplateCategory({
+        page: 1,
+        pageSize: 99,
+        status: 5,
+      });
+      if (res.status != 200) return;
+      _this.categoryList = res.data.dataList || [];
+      for (var i = 0; i < _this.categoryList.length; i++) {
+        _this.categoryList[i].dataList = await _this.getTemplateList(
+          _this.categoryList[i].id
+        );
+      } */
+    },
+
+    onModify(index, subIndex, headerIndex) {
+      this.currentIndex = index;
+      this.componentIndex = subIndex;
+      this.activeHeaderIndex = headerIndex;
+      this.headerItemName =
+        this.components[index].components[subIndex].attrs.tableHeader[
+          headerIndex
+        ];
+      this.dialogVisible = true;
+    },
+    //设置名称
+    onSetHeader(name) {
+      if (this.currentIndex >= 0) {
+        if (this.activeHeaderIndex >= 0) {
+          this.components[this.currentIndex].components[
+            this.componentIndex
+          ].attrs.tableHeader[this.activeHeaderIndex] = name;
+        } else {
+          this.components[this.currentIndex].components[
+            this.componentIndex
+          ].attrs.tableHeader.push(name);
+        }
+      }
+      this.$emit("onSetComponents", [...this.components]);
+      this.dialogVisible = false;
+    },
+    onInsert(index, subIndex) {
+      this.currentIndex = index;
+      this.componentIndex = subIndex;
+      this.headerItemName = "";
+      this.dialogVisible = true;
+    },
+    //删除表头项
+    onDeleteHeaderItem(index, subIndex, headerIndex) {
+      this.components[index].components[subIndex].attrs.tableHeader.splice(
+        headerIndex,
+        1
+      );
+      this.$emit("onSetComponents", [...this.components]);
+    },
+
+    onSetActiveIndex(e) {
+      if (e == this.activeHeaderIndex) {
+        this.activeHeaderIndex = -1;
+      } else {
+        this.activeHeaderIndex = e;
+      }
+    },
+    //获取模板列表
+    async getTemplateList(categoryId) {
+      let _this = this;
+      let res = await searchTemplate({
+        page: 1,
+        pageSize: 999,
+        category_id: categoryId,
+        status: 5,
+      });
+      if (res.status != 200) return [];
+      let dataList = res.data.dataList.map((item) => {
+        item.attrs = JSON.parse(item.attrs);
+        return item;
+      });
+      return dataList;
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 16 - 0
ui/src/views/system/document/com/components/Empty/index.vue

@@ -0,0 +1,16 @@
+<template>
+  <div class="com-empty" >
+
+  </div>
+</template>
+
+<script>
+</script>
+
+<style>
+  .com-empty{
+    border:1px dashed #CCC;
+    width:100%;
+    height:40px;
+  }
+</style>

+ 24 - 0
ui/src/views/system/document/com/components/Formula/dataInfo.scss

@@ -0,0 +1,24 @@
+ .sub-imgs{
+    padding:0px;
+    margin:0px;
+      li{
+        float:left;
+        margin-right:10px;
+        width:100px;
+        height:100px;
+        list-style: none;
+      }
+
+
+  }
+
+  .data-info{
+    .intro{
+      >div{
+        >span{
+          color:red;
+          margin-left:20px;
+        }
+      }
+    }
+  }

+ 542 - 0
ui/src/views/system/document/com/components/Formula/dataInfo.vue

@@ -0,0 +1,542 @@
+<template>
+  <div class="data-info">
+    <el-card>
+      <el-form :model="dataForm" label-width="120px">
+        <el-form-item label="公式名称:">
+          <el-input v-model="dataForm.name"></el-input>
+        </el-form-item>
+
+        <el-form-item label="公式:">
+          <el-input
+            type="textarea"
+            v-model="dataForm.formula"
+            @input="handleFormulaInput"
+            ref="formulaInput"
+          ></el-input>
+          <div style="display: flex; justify-content: flex-end">
+            <el-button style="margin-top: 10px" @click="InsertTableData"
+              >插入表格数据</el-button
+            >
+            <el-button
+              style="margin-top: 10px"
+              type="primary"
+              @click="InsertModule"
+              >插入模块变量</el-button
+            >
+          </div>
+          <div class="intro">
+            公式说明:
+            <div>
+              格式一:[T][模块引用名][属性引用名] <span>*</span>其中
+              [T]表示的是模块引用。
+            </div>
+            <div>目前仅支持+、-、*、/ 四种计算符合</div>
+            <div>示例: [T][模块01][attr01]+[T][模块01][attr02]</div>
+            <div>
+              格式二:[T][模块引用名][属性引用名]
+              IF(判断条件,true值,false值)<span>*</span>其中
+              [T]表示的是模块引用。
+            </div>
+            <div>
+              示例: [T][图文][测试1]+IF([T][图文][测试1]>30,[T][图文][测试2],11)
+            </div>
+            <div>
+              嵌套示例: [T][图文][测试1] + IF([T][图文][测试2] > 50,
+              IF([T][图文][测试1] > 30, [T][图文][测试3], 20),
+              IF([T][图文][测试3] > 40, 15, [T][图文][测试2]) )
+            </div>
+          </div>
+        </el-form-item>
+        <el-form-item label="保留小数点:">
+          <el-input
+            v-model.number="dataForm.point"
+            type="number"
+            min="0"
+            max="4"
+            :step="1"
+            @input="validatePoint"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="介绍:">
+          <el-input type="textarea" v-model="dataForm.intro"></el-input>
+        </el-form-item>
+
+        <el-form-item label="公式状态:">
+          <el-select
+            v-model="dataForm.status"
+            placeholder="请选择状态"
+            size="large"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </el-card>
+    <div style="text-align: right; margin-top: 20px">
+      <el-button type="warning" @click="btnSave">确认保存</el-button>
+    </div>
+    <el-dialog
+      :visible.sync="variablesVisible"
+      append-to-body
+      width="300"
+      custom-class="prod-verify"
+      :title="title"
+      :close-on-click-modal="false"
+      @close="close"
+    >
+      <el-form :model="insertForm" :rules="insertRules" ref="insertRef">
+        <el-form-item label="模块名称:">
+          <el-select
+            v-model="insertForm.moduleName"
+            placeholder="请选择模块"
+            size="large"
+            style="width: 100%"
+            @change="changeModule"
+          >
+            <el-option
+              v-for="(item, index) in list"
+              :key="`${item.id}-${index}`"
+              :label="item.name"
+              :value="item.attrs"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="属性名称:" prop="Variable">
+          <el-select
+            v-model="insertForm.Variable"
+            placeholder="请选择属性"
+            size="large"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="(item, index) in filteredAttrList"
+              :key="`${item.id}-${index}`"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button type="warning" @click="btnInster">确认插入</el-button>
+      </span>
+    </el-dialog>
+    <el-dialog
+      :visible.sync="tableDataVisible"
+      append-to-body
+      width="400"
+      custom-class="prod-verify"
+      title="插入表格数据"
+      :close-on-click-modal="false"
+      @close="close"
+    >
+      <div class="com-formual">
+        <el-form>
+          <el-form-item>
+            <el-select
+              v-model="tableCode"
+              placeholder="请选择状态"
+              @change="onChangeTab"
+              size="large"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="(item, index) in tableList"
+                :key="item.id"
+                :label="item.name"
+                :value="index"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item style="min-height: 500px" v-if="fileLoadStatus">
+            <div
+              id="luckysheets"
+              ref="luckysheets"
+              style="width: 100%; height: 500px"
+            ></div>
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-dialog>
+    <el-dialog
+      :visible.sync="showConfirm"
+      title="数据确认"
+      width="500"
+      align-center
+      :append-to-body="true"
+      :close-on-click-modal="false"
+    >
+      <div>当前工作表:{{ position.sheet }}</div>
+      <div>当前取值:{{ position.value }}</div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="showConfirm = false">关闭</el-button>
+          <el-button type="primary" @click="onConfirm">确认插入</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { createFormula, updateFormula, getFormulaInfo } from "@/api/formula";
+import { searchSourceData, deleteSourceData } from "@/api/sourceData";
+import LuckyExcel from "luckyexcel";
+export default {
+  emits: ["onClose"],
+  props: {
+    id: {
+      type: Number,
+      default: 0,
+    },
+    list: {
+      type: Array,
+      default: [],
+    },
+  },
+  watch: {
+    id: {
+      handler(newVal, oldVal) {
+        if (newVal == null) return;
+        if (newVal == 0) {
+          this.resetForm();
+        } else {
+          // 添加条件以确保在id相同但需要重新获取数据时也能触发
+          if (newVal !== oldVal || this.dataForm.id !== newVal) {
+            this.getInfo(newVal);
+          }
+        }
+      },
+      immediate: true,
+    },
+    list: {
+      handler(newValue, oldValue) {
+        console.log("list changed:", newValue);
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      activeName: "base",
+      currentCategory: [],
+      allCategories: [],
+
+      dataForm: {
+        id: 0,
+        name: "",
+        formula: "",
+        intro: "",
+        status: 5,
+        use_status: 5,
+        params: "[]",
+        point: 0,
+      },
+      statusOptions: [
+        {
+          value: "",
+          label: "请选择状态",
+        },
+        {
+          value: 5,
+          label: "使用中",
+        },
+        {
+          value: 6,
+          label: "已停用",
+        },
+      ],
+      variablesVisible: false,
+      title: "",
+      insertForm: {
+        moduleName: "",
+        Variable: "",
+      },
+      insertRules: {
+        Variable: [
+          { required: true, message: "请选择变量", trigger: "change" },
+        ],
+      },
+      tableDataVisible: false,
+      tableCode: "",
+      tableList: [],
+      showConfirm: false,
+      fileLoadStatus: false,
+      position: {
+        tb: "",
+        c: 0,
+        r: 0,
+        sheet: "",
+        value: "",
+      },
+      attrList: [],
+    };
+  },
+
+  mounted() {
+    let _this = this;
+    this.initTableList();
+  },
+  computed: {
+    uniqueAttrList() {
+      const uniqueNames = new Set();
+      return this.attrList.filter(attr => {
+        if (!uniqueNames.has(attr.name)) {
+          uniqueNames.add(attr.name);
+          return true;
+        }
+        return false;
+      });
+    },
+    filteredAttrList() {
+      return this.uniqueAttrList.filter(attr => 
+         attr.type !== 'Directory'
+      );
+    }
+  },
+  methods: {
+    validatePoint(value) {
+      const intValue = parseInt(value);
+      if (isNaN(intValue) || intValue < 0 || intValue > 4) {
+        this.dataForm.point = "";
+      } else {
+        this.dataForm.point = intValue;
+      }
+    },
+    /* 关闭 */
+    close() {
+      this.insertForm = {};
+    },
+    /*  */
+    handleFormulaInput(value) {
+      // 更新 dataForm.formula 的值
+      this.dataForm.formula = value;
+    },
+    resetForm() {
+      this.dataForm = {
+        id: 0,
+        name: "",
+        formula: "",
+        intro: "",
+        status: 5,
+        use_status: 5,
+        params: "[]",
+        point: 0,
+      };
+    },
+
+    btnSave(e) {
+      let _this = this;
+
+      const saveAction = _this.dataForm.id > 0 ? updateFormula : createFormula;
+
+      saveAction(_this.dataForm)
+        .then((res) => {
+          _this.$message.success(
+            _this.dataForm.id > 0 ? "公式更新成功" : "公式创建成功"
+          );
+          _this.$emit("onClose", true); // Pass a boolean to indicate successful save
+
+          // 添加这行来强制重新获取数据
+          _this.getInfo(_this.dataForm.id);
+          _this.resetFormAfterSave();
+        })
+        .catch((error) => {
+          console.error("保存失败:", error);
+          _this.$message.error("保存失败,请重试");
+        });
+    },
+    resetFormAfterSave() {
+      this.dataForm = {
+        id: 0,
+        name: "",
+        formula: "",
+        intro: "",
+        status: 5,
+        use_status: 5,
+        params: "[]",
+        point: 0,
+      };
+      // If you have any other data that needs to be reset, do it here
+    },
+
+    //产品详情
+    getInfo(id) {
+      let _this = this;
+      let par = {
+        id: id,
+      };
+      getFormulaInfo(par).then((res) => {
+        if (!res) return;
+        if (res.status != 200) return;
+        _this.dataForm = res.data;
+      });
+    },
+
+    onChangeStatus(e) {
+      this.dataForm.status = e;
+    },
+    //修改分类信息
+    onChangeCategory(e) {
+      let _this = this;
+      _this.dataForm.categoryId = e;
+    },
+    /* 插入模块变量 */
+    InsertModule() {
+      this.variablesVisible = true;
+      this.title = "插入模块变量";
+    },
+    extractChineseTemplates(content) {
+      const templateRegex = /\{\{([\s\S]*?)\}\}/g; // 修改正则表达式以匹配多行内容
+      const chineseRegex = /[\u4e00-\u9fa5]/;
+      let match;
+      const chineseTemplates = [];
+      while ((match = templateRegex.exec(content)) !== null) {
+        const trimmedMatch = match[1].trim();
+        chineseTemplates.push(trimmedMatch);
+      }
+      return chineseTemplates;
+    },
+
+    processContent(val) {
+      if (val && typeof val.content === "string") {
+        const chineseTemplates = this.extractChineseTemplates(val.content);
+        console.log("Extracted Chinese templates:", chineseTemplates);
+
+        if (val.attrs && Array.isArray(val.attrs)) {
+          this.attrList = val.attrs.filter((el) =>
+            chineseTemplates.some((template) => template.includes(el.id))
+          );
+        }
+      } else {
+        this.attrList = [];
+      }
+    },
+    /* 模块名称 */
+    changeModule(value) {
+      const selectedIndex = this.list.findIndex(
+        (option) => option.attrs === value
+      );
+      this.processContent(this.list[selectedIndex]);
+      //this.attrList = value; // 假设 attrs 属性包含了属性列表
+    },
+    /* 插入变量 */
+    btnInster() {
+      this.$refs.insertRef.validate((valid) => {
+        if (valid) {
+          const selectedModule = this.list.find(
+            (module) => module.attrs === this.insertForm.moduleName
+          );
+          if (selectedModule) {
+            const moduleName = selectedModule.name;
+            const variableName = this.attrList.find(
+              (attr) => attr.id === this.insertForm.Variable
+            )?.name;
+
+            if (variableName) {
+              const insertText = `[T][${moduleName}][${variableName}]`;
+              this.dataForm.formula += insertText;
+              this.variablesVisible = false; // 关闭对话框
+              this.$message.success("变量插入成功");
+            } else {
+              this.$message.error("无法找到选中的变量");
+            }
+          } else {
+            this.$message.error("无法找到选中的模块");
+          }
+        } else {
+          /* this.$message.error("请选择变量!"); */
+          return false;
+        }
+      });
+    },
+    /* 插入表格数据 */
+    InsertTableData() {
+      this.tableDataVisible = true;
+    },
+
+    onConfirm(e) {
+      this.showConfirm = false;
+      const insertText = `[R][${this.position.tb}][${this.position.sheet}][${
+        this.position.r + 1
+      },${this.position.c + 1}]`;
+      this.dataForm.formula += insertText;
+      this.tableDataVisible = false;
+      this.$message.success("表格数据插入成功");
+      this.insertForm = {
+        moduleName: {},
+        Variable: "",
+      };
+    },
+
+    onChangeTab(e) {
+      let _this = this;
+      let item = _this.tableList[e];
+      _this.fileLoadStatus = true;
+
+      item.sourceData = JSON.parse(item.source_data);
+      console.log(item.sourceData.exportJson.sheets);
+
+      _this.position.tb = item.code;
+      _this.$nextTick(() => {
+        luckysheet.destroy();
+        luckysheet.create({
+          container: "luckysheets",
+          data: item.sourceData.exportJson.sheets,
+          lang: "zh",
+          showinfobar: false, // 是否显示顶部信息栏
+          showstatisticBar: false, // 是否显示底部计数栏
+          sheetBottomConfig: false, // sheet页下方的添加行按钮和回到顶部按钮配置
+          allowEdit: false, // 是否允许前台编辑
+          enableAddRow: false, // 是否允许增加行
+          enableAddCol: false, // 是否允许增加列
+          sheetFormulaBar: false, // 是否显示公式栏
+          enableAddBackTop: false, // 返回头部按钮
+          showsheetbar: true, // 是否显示底部sheet页按钮
+          enableAddRow: false, //允许添加行
+          showsheetbarConfig: {
+            add: false,
+            menu: false,
+          },
+          hook: {
+            cellMousedown: function (cell, position, sheetFile, ctx) {
+              _this.position.sheet = sheetFile.name;
+              _this.position.r = position.r;
+              _this.position.c = position.c;
+
+              if (cell.v) {
+                _this.position.value = cell.v;
+              } else {
+                _this.position.value = "";
+                cell.ct.s.map((res) => {
+                  _this.position.value += res.v;
+                });
+              }
+              _this.showConfirm = true;
+            },
+          },
+        });
+      });
+    },
+
+    initTableList() {
+      let _this = this;
+      searchSourceData({ page: 1, pageSize: 999, status: 5 }).then((res) => {
+        if (res.status != 200) return;
+        _this.tableList = res.data.dataList;
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./dataInfo.scss";
+</style>

+ 0 - 0
ui/src/views/system/document/com/components/Formula/index.scss


+ 148 - 0
ui/src/views/system/document/com/components/Formula/index.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="com-formual">
+        <el-form :inline="true" :model="queryForm" class="demo-form-inline">
+            <el-form-item label="公式名称:">
+              <el-input v-model="queryForm.name" placeholder="请填写公式名称"></el-input>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="onBtnSearch">  <svg-icon icon-class="search"/> 搜索</el-button>
+              <el-button type="primary" @click="onBtnCreate">  <svg-icon icon-class="plus"/> 新增公式</el-button>
+            </el-form-item>
+          </el-form>
+
+          <el-table :data="dataList" style="width: 100%" header-row-class-name="headerBg" empty-text="没有公式信息">
+            <el-table-column prop="id" label="ID" align="center" width="50"/>
+            <el-table-column prop="name" label="名称" align="left"/>
+            <el-table-column prop="intro" label="介绍" align="center" />
+            <el-table-column prop="formula" label="内容" align="center" min-width="150"/>
+            <el-table-column label="操作" align="center" width="350">
+              <template #default="scope">
+                <div class="btns">
+                  <el-button type="primary" size="mini" @click="btnPicked(scope.row)"><svg-icon icon-class="confirm"/>使用</el-button>
+                  <el-button type="primary" size="mini" @click="btnOnEdit(scope.row.id)"><svg-icon icon-class="edit"/>编辑</el-button>
+                  <el-button type="danger" size="mini" @click="btnDel(scope.row.id)"><svg-icon icon-class="delete"/>删除</el-button>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+
+
+
+          <div class="page-info">
+          	<el-pagination
+          	:currentPage="queryForm.page"
+          	:page-size="queryForm.pageSize"
+          	:total="recordCount"
+          	:page-count="pageTotal"
+          	 background
+          	 layout="prev, pager, next"
+          	 @current-change="ChangePage"
+          	 >
+          	 </el-pagination>
+          </div>
+
+
+    <el-dialog  :visible.sync="dialogVisible" append-to-body v-el-drag-dialog :close-on-click-modal="false"  width="300" custom-class="prod-verify" :title="title">
+        <dataInfo :id="dataId" @onClose="onClose" :list="comList"></dataInfo>
+    </el-dialog>
+
+
+  </div>
+</template>
+
+<script>
+  import{searchFormula,deleteFormula} from'@/api/formula';
+  import dataInfo from "./dataInfo";
+  import elDragDialog from '@/directive/el-drag-dialog'
+
+
+  export default{
+    name:"Formula",
+    emits:['onPicked'],
+    directives: { elDragDialog },
+    components:{
+      dataInfo,
+    },
+    props:{
+      comList:{
+        type:Array,
+        default:[]
+      }
+    },
+    data(){
+      return{
+          dataId:0,
+          dialogVisible:false,
+          title:"创建公式",
+          queryForm:{
+            page:1,
+            pageSize:10,
+            name:'',
+          },
+          recordCount:0,
+          pageTotal:1,
+          dataList:[],
+      }
+    },
+    mounted(){
+      this.onBtnSearch();
+    },
+    methods:{
+
+      onBtnCreate(e){
+        let _this=this;
+        _this.title="创建公式";
+        _this.dataId=0;
+        _this.dialogVisible=true;
+      },
+
+      btnOnEdit(id){
+        this.title="编辑公式";
+        this.dataId=id;
+        this.dialogVisible=true;
+      },
+
+      onClose(e){
+        let _this=this;
+        _this.dialogVisible=false;
+        _this.onBtnSearch();
+      },
+      onBtnSearch(e){
+        let _this=this;
+        _this.queryForm.page=1;
+        _this.search();
+      },
+      btnPicked(e){
+        this.$emit("onPicked",e);
+      },
+     //搜索
+     search(){
+       let _this=this;
+       searchFormula(_this.queryForm).then(res=>{
+         if(res.status!=200)return;
+         _this.dataList=res.data.dataList;
+         _this.recordCount=res.data.totalPage;
+         _this.pageTotal=res.data.totalRecord;
+       })
+     },
+     //修改分页
+     ChangePage(e){
+     	let _this=this;
+     	_this.queryForm.page=e;
+     	_this.search();
+     },
+     /* 删除 */
+     btnDel(e){
+      let _this=this;
+      deleteFormula({id:e}).then(res=>{
+        _this.$message.success("删除成功!")
+        _this.search();
+      })
+     }
+    }
+  }
+</script>
+
+<style lang="scss">
+@import './index.scss';
+</style>

+ 4 - 0
ui/src/views/system/document/com/components/ProductAttr/index.scss

@@ -0,0 +1,4 @@
+.com-product-attr{
+  width:100%;
+  min-height: 300px;
+}

+ 103 - 0
ui/src/views/system/document/com/components/ProductAttr/index.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="com-product-attr">
+        <el-tree :data="dataList" :props="defaultProps" @node-click="handleNodeClick" />
+  </div>
+</template>
+
+<script>
+  import{searchProduct} from'@/api/product';
+
+  export default{
+    name:"searchParams",
+    emits:['onInsertProductAttr'],
+    props:{
+       docAttr:{
+         type:Object,
+         default:()=>{
+           return {
+             linkProduct:[],
+           }
+         },
+       }
+    },
+    watch:{
+      docAttr:{
+        handler(val){
+          if(val==undefined && val==null)return;
+          this.queryForm.ids=JSON.stringify(val.linkProduct);
+          this.search();
+        },
+        immediate: true,
+        deep: true
+      }
+    },
+    data(){
+      return{
+        defaultProps:{
+          children: 'children',
+          label: 'label',
+        },
+        dataList:[],
+        queryForm:{
+          page:1,
+          pageSize:999,
+          status:5,
+          ids:'',
+        }
+      }
+    },
+    mounted(){
+      // this.onBtnSearch();
+    },
+    methods:{
+
+      onClose(e){
+         let _this=this;
+         _this.dialogVisible=false;
+         _this.onBtnSearch();
+      },
+
+      handleNodeClick(e){
+
+        if(e && e.children==undefined){
+          this.$emit("onInsertProductAttr",e);
+        }
+      },
+
+     //搜索
+     search(){
+       let _this=this;
+       searchProduct(_this.queryForm).then(res=>{
+         if(!res)return;
+         _this.dataList=res.data.dataList.map((item)=>{
+           var tempJs=JSON.parse(item.attrs);
+           var subItem=tempJs.map((tempItem)=>{
+             return{
+               'label':tempItem.name,
+              ...tempItem,
+              'parent':{
+                'id':item.id,
+                'name':item.name,
+              }
+             }
+           })
+           return{
+             'label':item.name,
+             'children':subItem,
+           }
+         });
+       })
+     },
+     //修改分页
+     ChangePage(e){
+     	let _this=this;
+     	_this.queryForm.page=e;
+     	_this.search();
+     },
+    }
+  }
+</script>
+
+<style lang="scss">
+@import './index.scss';
+</style>

+ 0 - 0
ui/src/views/system/document/com/components/SourceData/index.scss


+ 173 - 0
ui/src/views/system/document/com/components/SourceData/index.vue

@@ -0,0 +1,173 @@
+<template>
+  <div
+    class="com-formual"
+    v-loading="loading"
+    element-loading-text="数据加载中..."
+  >
+    <el-form>
+      <el-form-item>
+        <el-select
+          v-model="tableCode"
+          placeholder="请选择数据表"
+          @change="onChangeTab"
+          size="large"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="(item, index) in tableList"
+            :key="item.id"
+            :label="item.name"
+            :value="index"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item style="min-height: 500px" v-if="fileLoadStatus">
+        <div
+          id="luckysheet"
+          ref="luckysheet"
+          style="width: 100%; height: 500px"
+        ></div>
+      </el-form-item>
+    </el-form>
+
+    <el-dialog
+      :visible.sync="showConfirm"
+      :close-on-click-modal="false"
+      title="数据确认"
+      width="500"
+      align-center
+      :append-to-body="true"
+    >
+      <div>当前工作表:{{ position.sheet }}</div>
+      <!-- <div>当前选择的位置:ROW:{{position.r}}, COL:{{position.c}} </div> -->
+      <div>当前取值:{{ position.value }}</div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="showConfirm = false">关闭</el-button>
+          <el-button type="primary" @click="onConfirm"> 确认选择 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  searchSourceData,
+  deleteSourceData,
+  getSourceData,
+} from "@/api/sourceData";
+import LuckyExcel from "luckyexcel"; //引入LuckyExcel
+
+export default {
+  name: "SourceData",
+  emits: ["onPicked"],
+
+  components: {},
+  data() {
+    return {
+      tableCode: "",
+      tableList: [],
+      showConfirm: false,
+      fileLoadStatus: false,
+      position: {
+        tb: "",
+        c: 0,
+        r: 0,
+        sheet: "",
+        value: "",
+      },
+      loading: false, // 添加 loading 状态控制
+    };
+  },
+  mounted() {
+    this.initTableList();
+  },
+  methods: {
+    onConfirm(e) {
+      this.showConfirm = false;
+      this.$emit("onPicked", this.position);
+    },
+
+    onChangeTab(e) {
+      let _this = this;
+      let item = _this.tableList[e];
+      _this.loading = true; // 开始加载
+      getSourceData({ id: item.id })
+        .then((res) => {
+          if (res.status !== 200) return;
+          _this.fileLoadStatus = true;
+
+          item.sourceData = JSON.parse(res.data.source_data);
+
+          _this.position.tb = item.code;
+          _this.$nextTick(() => {
+            luckysheet.destroy();
+            luckysheet.create({
+              container: "luckysheet", //dom id
+              data: item.sourceData.exportJson.sheets, //excel数据
+              lang: "zh",
+              showinfobar: false, // 是否显示顶部信息栏
+              showstatisticBar: false, // 是否显示底部计数栏
+              sheetBottomConfig: false, // sheet页下方的添加行按钮和回到顶部按钮配置
+              allowEdit: false, // 是否允许前台编辑
+              enableAddRow: false, // 是否允许增加行
+              enableAddCol: false, // 是否允许增加列
+              sheetFormulaBar: false, // 是否显示公式栏
+              enableAddBackTop: false, // 返回头部按钮
+              showsheetbar: true, // 是否显示底部sheet页按钮
+              enableAddRow: false, //允许添加行
+              // 自定义配置底部sheet页按钮
+              showsheetbarConfig: {
+                add: false,
+                menu: false,
+              },
+              hook: {
+                cellMousedown: function (cell, position, sheetFile, ctx) {
+                  _this.position.sheet = sheetFile.name;
+
+                  _this.position.r = position.r;
+                  _this.position.c = position.c;
+
+                  if (cell.v) {
+                    _this.position.value = cell.v;
+                  } else {
+                    _this.position.value = "";
+                    cell.ct.s.map((res) => {
+                      _this.position.value += res.v;
+                    });
+                  }
+                  _this.showConfirm = true;
+                },
+                cellUpdated: function (r, c, oldValue, newValue, isRefresh) {
+                  // console.log("r",r);
+                  // console.log("c",c);
+                  // console.log("oldVal",oldValue);
+                  // console.log("newVal",newValue);
+                  //   console.log('exportJson',_this.dataForm.source_data.exportJson)
+                },
+              },
+            });
+          });
+        })
+        .finally(() => {
+          _this.loading = false; // 结束加载
+        });
+
+      //        _this.dataForm=res.data;
+      //        _this.dataForm.sourceData=JSON.parse(_this.dataForm.source_data);
+    },
+    initTableList() {
+      let _this = this;
+      searchSourceData({ page: 1, pageSize: 999, status: 5 }).then((res) => {
+        if (res.status != 200) return;
+        _this.tableList = res.data.dataList;
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 49 - 0
ui/src/views/system/document/com/components/Table/index.scss

@@ -0,0 +1,49 @@
+.template-table{
+  width:100%;
+  min-height:30px;
+  .menus{
+    width:100%;
+    height:30px;
+    display:flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    align-items: center;
+    >span{
+      display:flex;
+      flex-direction: row;
+      margin-right: 10px;
+      justify-content: center;
+      align-items: center;
+      padding-left:10px;
+      padding-right: 10px;
+      border:1px solid red;
+    }
+  }
+
+}
+
+
+.whiteBg{
+  background:#FFF!important;
+}
+.redBg{
+  background-color:#FF0000!important;
+}
+
+.grayBg{
+  background-color:#e6e6e6!important;
+
+}
+
+
+
+.whiteFont{
+  color:#FFF!important;
+}
+.blackFont{
+  color:#000!important;
+}
+
+.redFont{
+  color:#FF0000!important;
+}

+ 361 - 0
ui/src/views/system/document/com/components/Table/index.vue

@@ -0,0 +1,361 @@
+<template>
+  <div class="template-table" v-if="com!=null">
+    <!-- <div class="menus" v-if="isActive">
+      <span @click="onExport">导出</span>
+
+    </div> -->
+    <div class="table">
+      <hot-table ref="hotTable" :settings="settings" licenseKey="non-commercial-and-evaluation"
+        style="width:100%;"></hot-table>
+    </div>
+
+  </div>
+</template>
+
+<script>
+  import Handsontable from "handsontable";
+
+  import {
+    HotTable
+  } from '@handsontable/vue';
+  import {
+    ContextMenu
+  } from 'handsontable/plugins/contextMenu'
+  import {
+    registerAllModules
+  } from 'handsontable/registry'
+  import {
+    HyperFormula
+  } from 'hyperformula';
+  import "handsontable/dist/handsontable.full.css";
+
+  // define options
+  const hyperformulaInstance = HyperFormula.buildEmpty({
+    licenseKey: 'internal-use-in-handsontable',
+    precisionRounding: 10,
+    nullDate: {
+      year: 1900,
+      month: 1,
+      day: 1
+    },
+  });
+
+
+  function customRenderer(instance, td) {
+    Handsontable.renderers.TextRenderer.apply(this, arguments);
+    td.style.backgroundColor = 'yellow';
+  }
+
+
+  import "handsontable/languages/zh-CN";
+  import 'handsontable/dist/handsontable.full.css';
+  export default {
+    name: "table",
+    components: {
+      HotTable
+    },
+    emits: ['onUploadAttr', 'onUploadItemIndex'],
+    props: {
+      isActive: {
+        type: Boolean,
+        default: false,
+      },
+      currentIndex: {
+        type: Number,
+        default: 0,
+      },
+      com: {
+        type: Object,
+        default: null,
+      },
+      insertCmd: {
+        type: Object,
+        default: null,
+      }
+    },
+    watch: {
+      'com': {
+        handler(val) {
+          if (val == null || val == undefined) return;
+          let _this = this;
+
+          console.log("watch com", val);
+          // _this.dataAttr=JSON.parse(JSON.stringify(val));
+          _this.initSetting();
+        },
+        immediate: true, //立即执行
+        deep: true
+      },
+      'isActive': {
+        handler(val) {
+          if (val == undefined && val != null) return;
+          this.settings.readOnly = !val;
+          this.settings.colHeaders = val;
+          this.settings.rowHeaders = val;
+        },
+        immediate: true, //立即执行
+        deep: true
+      },
+
+      'insertCmd': {
+        handler(val) {
+          if (val == null || !this.isActive) return;
+          console.log("insertCmd table val", val);
+
+        },
+        immediate: true,
+        deep: true,
+      },
+    },
+
+    data() {
+      return {
+        cellStyle:[],
+        settings: {
+          language: "zh-CN",
+          startRows: 5,
+          startCols: 3
+        },
+        currentFocus: {
+          row: 0,
+          col: 0,
+        },
+
+      }
+    },
+    created() {
+
+    },
+    methods: {
+
+      //导出
+      onExport(e) {
+        const exportPlugin = this.$refs.hotTable.hotInstance.getPlugin('exportFile');
+        let str = exportPlugin.exportAsString('csv', {
+          filename: 'MyFile'
+        });
+
+        console.log("str", str);
+      },
+      //初始化设置
+      initSetting() {
+        let _this = this;
+        _this.settings = {
+          language: "zh-CN",
+          startRows: 1,
+          startCols: 6,
+          // readOnly: true,
+          height: 'auto',
+          stretchH: 'all',
+          customBorders: true,
+          licenseKey: 'non-commercial-and-evaluation',
+          comments: true,
+          mergeCells: true,
+          allowRemoveColumn:true,
+          allowRemoveRow:true,
+          copyPaste: {
+            columnsLimit: 25,
+            rowsLimit: 50,
+            pasteMode: 'shift_down',
+            uiContainer: document.body,
+          },
+          cell:_this.cellStyle,
+          contextMenu: {
+            callback(key, selection, clickEvent) {
+              // Common callback for all options
+              console.log("callback", key, selection, clickEvent);
+              // console.log("selection",selection);
+              if(_this.cellStyle==undefined)_this.cellStyle=[];
+              if(key=="importExcel"){
+                //导入excel
+
+                return;
+              }
+
+              if(key=="clearColor"){
+                //清除样式
+                 for(var i=selection[0].start.col;i<=selection[0].end.col;i++){
+                   for(var j=selection[0].start.row;j<=selection[0].end.row;j++){
+                     _this.cellStyle=_this.cellStyle.filter(o=>o.col!=i  && o.row!=j)
+                   }
+                 }
+                 _this.initSetting();
+                 return;
+              }
+              if(key.length>=8){
+                if(key.substr(0,7)=='bgColor'){
+                    let subKey=key.replace("bgColor:","");
+                    let bgClass="whiteBg";
+                    switch(subKey){
+                        case "red":
+                          bgClass="redBg";
+                          break;
+                        case "gray":
+                          bgClass="grayBg";
+                          break;
+                    }
+
+                    for(var i=selection[0].start.col;i<=selection[0].end.col;i++){
+                      for(var j=selection[0].start.row;j<=selection[0].end.row;j++){
+                        let item=_this.cellStyle.filter(o=>o.col==i && o.row==j);
+                        if(item.length>0){
+                          if(item[0].className.indexOf(bgClass)<0){
+                            item[0].className+=" "+bgClass;
+                          }
+                        }else{
+                          _this.cellStyle.push({
+                            col:i,
+                            row:j,
+                            className:bgClass,
+                          })
+                        }
+                      }
+                    }
+                  return;
+                }
+                if(key.substr(0,9)=="fontColor"){
+                  //修改字体颜色
+                  let subKey=key.replace("fontColor:","");
+                  let fontClass="blackFont";
+                    switch(subKey){
+                        case "white":
+                          fontClass="whiteFont";
+                          break;
+                        case "black":
+                          fontClass="blackFont";
+                          break;
+                    }
+
+                    if(_this.cellStyle==undefined)_this.cellStyle=[];
+                    for(var i=selection[0].start.col;i<=selection[0].end.col;i++){
+                      for(var j=selection[0].start.row;j<=selection[0].end.row;j++){
+                        let item=_this.cellStyle.filter(o=>o.col==i && o.row==j);
+                        if(item.length>0){
+                          if(item[0].className.indexOf(fontClass)<0){
+                            item[0].className+=" "+fontClass;
+                          }
+                        }else{
+                          _this.cellStyle.push({
+                            col:i,
+                            row:j,
+                            className:fontClass,
+                          })
+                        }
+                      }
+                    }
+                    return;
+                }
+              }
+            },
+            items: {
+              row_above: {
+                name: "上方插入行",
+                disabled() { // `disabled` can be a boolean or a function
+                  // Disable option when first row was clicked
+                  return this.getSelectedLast()[0] === 0; // `this` === hot
+                }
+              },
+              row_below: {
+                name: "下方插入行",
+              },
+              col_left: {
+                name: "左侧插入列",
+              },
+              col_right: {
+                name: "右侧插入列",
+              },
+              remove_row: {
+                name: "删除行"
+              },
+              remove_col: {
+                name: "删除列"
+              },
+              sp1: '---------',
+              mergeCells: {
+                name: "合并单元格"
+              },
+              alignment: {
+                name: '对齐'
+              },
+              borders: {
+                name: "边框"
+              },
+              sp2: '---------',
+              importExcel:{
+                name:"导入Excel",
+                key:"importExcel"
+              },
+              sp3: '---------',
+              clearColor:{
+                name:"清除样式",
+                key:"clearColor"
+              },
+              bgColor: { // Own custom option
+                name: '背景颜色',
+                submenu: {
+                  // Custom option with submenu of items
+                  items: [{
+                      // Key must be in the form 'parent_key:child_key'
+                      key: 'bgColor:red',
+                      name: '红色',
+                    },
+                    {
+                        // Key must be in the form 'parent_key:child_key'
+                        key: 'bgColor:gray',
+                        name: '灰色',
+                      },
+                  ],
+
+                }
+              },
+              fontColor: { // Own custom option
+                name: '文字颜色',
+                submenu: {
+                  // Custom option with submenu of items
+                  items: [
+                    {
+                      // Key must be in the form 'parent_key:child_key'
+                      key: 'fontColor:white',
+                      name: '白色',
+                    },
+                    {
+                      // Key must be in the form 'parent_key:child_key'
+                      key: 'fontColor:black',
+                      name: '黑色',
+                    },
+                  ],
+
+                }
+              },
+
+            }
+          },
+          colHeaders: true,
+          rowHeaders: true,
+          width: '100%',
+          height: 'auto',
+          colWidths: 'auto',
+          allowHtml: true,
+          filters: true,
+          manualRowResize: true, // 行高调整
+          manualColumnResize: true, // 列宽调整
+          manualRowMove: true, // 行移动
+          manualColumnMove: true, // 列移动
+          formulas: {
+            engine: HyperFormula,
+          },
+          tableClassName: "tb-class",
+          afterOnCellMouseDown: (event, coords, Td) => {
+            this.currentFocus = coords
+          }
+        }
+        registerAllModules()
+      },
+
+    }
+  }
+</script>
+
+<style lang="scss">
+  @import './index.scss';
+</style>

+ 78 - 0
ui/src/views/system/document/com/components/TextArea/index.scss

@@ -0,0 +1,78 @@
+.template-textarea{
+  width:100%;
+  display:flex;
+  flex-direction:column;
+  position: relative;
+  .editor-area{
+    min-height: 800px!important;
+  }
+  .rich-editor{
+    width:100%;
+    min-height: 20px;
+   /*  h1{
+      font-size:14px;
+      font-weight: bold;
+    } */
+  }
+
+  .content{
+    padding:10px;
+  }
+  .editor{
+
+    width:100%;
+    display:flex;
+    flex-direction:column;
+    justify-content: flex-start;
+    align-items: flex-start;
+  }
+  .menus{
+    position:absolute;
+    bottom:0px;
+    right:0px;
+    font-size:12px;
+    display:flex;
+    width:50px;
+    height:100px;
+    border:1px dashed #CCC;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    .attr{
+      display:flex;
+      width:100%;
+      height:50px;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      .attr-icon-size{
+        width:28px;
+        height:28px;
+      }
+    }
+    .delete{
+      display:flex;
+      width:100%;
+      height:50px;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      .attr-icon-size{
+          width:28px;
+          height:28px;
+        }
+    }
+  }
+
+}
+
+.attr-icon-size{
+  width:48px;
+  height:48px;
+}
+
+.min-height{
+  min-height:300px;
+}
+
+

+ 1082 - 0
ui/src/views/system/document/com/components/TextArea/index.vue

@@ -0,0 +1,1082 @@
+<template>
+  <div class="template-textarea">
+    <template v-if="isEdit == 1">
+      <div class="editor-area sticky-editor">
+        <ckeditor
+          ref="editor"
+          v-model="com.content"
+          @focus="onFocus"
+          @blur="onBlur"
+          @input="onInputText"
+          @ready="onEditorReady"
+          :config="editorConfig"
+          :editorUrl="editorUrl"
+        ></ckeditor>
+      </div>
+    </template>
+
+    <template v-else>
+      <div
+        class="rich-editor"
+        ref="richEditor"
+        v-html="content"
+        @click="handleImageClick"
+      ></div>
+    </template>
+    <div v-if="loading" class="overlay">
+      <el-progress
+        :percentage="progress"
+        class="full-width-progress"
+      ></el-progress>
+    </div>
+  </div>
+</template>
+
+<script>
+// import CKEditor from "ckeditor4-vue";
+import { findData, uploadImage } from "@/api/sourceData";
+import axios from "axios"; // 确保导入 axios
+import { data } from "jquery";
+export default {
+  name: "app",
+  emits: ["onUpdate", "onUpdateAttr", "onUpdateProdAttr"],
+  compnents: {
+    // ckeditor: CKEditor.component
+  },
+  props: {
+    coms: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+    isEdit: {
+      type: Number,
+      default: 2,
+    },
+    isAdmin: {
+      type: Number,
+      default: 2,
+    },
+    currentIndex: {
+      type: Number,
+      default: 0,
+    },
+    com: {
+      type: Object,
+      default: null,
+    },
+    insertCmd: {
+      type: Object,
+      default: null,
+    },
+  },
+  watch: {
+    isEdit: {
+      handler(val) {
+        let _this = this;
+        if (_this.com != null) return;
+        _this.replaceData(_this.com.content).then((res) => {
+          _this.content = res;
+          _this.$nextTick(() => {
+            _this.bindEvents();
+          });
+        });
+      },
+      immediate: true,
+      deep: true,
+    },
+    com: {
+      async handler(val) {
+        let _this = this;
+        if (val == null) return;
+        if (val.content == undefined || val.content == null) return;
+        try {
+          let res = await _this.replaceData(val.content);
+          _this.content = res;
+          _this.$nextTick(() => {
+            _this.bindEvents();
+            _this.initializeInputWidths();
+          });
+        } catch (error) {
+          console.error("处理 com 时出错:", error);
+        }
+      },
+      immediate: true,
+      deep: true,
+    },
+    insertCmd: {
+      handler(val) {
+        if (val == null || this.isEdit != 1) return;
+        let data = this.$refs.editor.instance.getSelection().getSelectedText();
+        if (val.content.indexOf("Directory", 0) >= 0) {
+          //执行的是目录
+          this.$emit(
+            "onUpdateAttr",
+            this.currentIndex,
+            this.com.attrs.length - 1,
+            data
+          );
+          this.$refs.editor.instance.execCommand("delete");
+        }
+        this.$refs.editor.instance.insertHtml(val.content);
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      editorUrl: "/ckeditor/ckeditor.js",
+      editorConfig: {
+        language: "zh-cn",
+        height: "650px",
+      },
+
+      content: "",
+      editor: null,
+      loading: false,
+      progress: 0,
+      isEditing: false,
+      focusedInputId: null,
+      variableNullInputs: {},
+    };
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initializeInputWidths();
+    });
+  },
+  // 记得在组件销毁时移除事件监听器
+  beforeDestroy() {
+    this.$el.removeEventListener("input", this.handleInputChange);
+    this.$el.removeEventListener("input", this.handleVariableNullInput);
+    this.$el.removeEventListener("blur", this.handleVariableNullBlur, true);
+  },
+  methods: {
+    async replaceData(data) {
+      let _this = this;
+      // 定义数学函数和 IF 函数
+      const mathFunctions = {
+        abs: Math.abs,
+        ceil: Math.ceil,
+        floor: Math.floor,
+        max: Math.max,
+        min: Math.min,
+        round: Math.round,
+        sqrt: Math.sqrt,
+        IF: (condition, trueValue, falseValue) =>
+          condition ? trueValue : falseValue,
+        // 添加其他需要的数学函数
+      };
+      for (var i = 0; i < _this.com.attrs.length; i++) {
+        let attrId = _this.com.attrs[i].id;
+        if (_this.com.attrs[i].type == "variable") {
+          let item = _this.com.attrs[i];
+          if (item.data.value_type == 2) {
+            //多选
+            let dataItem = item.data.value_item.split(",");
+            let selectHtml =
+              '<select id="' +
+              attrId +
+              '"  data-index="' +
+              i +
+              '" class="text-input-box">';
+            for (var l = 0; l < dataItem.length; l++) {
+              if (item.content == dataItem[l]) {
+                selectHtml +=
+                  '<option value="' +
+                  dataItem[l] +
+                  '" selected>' +
+                  dataItem[l] +
+                  "</option>";
+              } else {
+                selectHtml +=
+                  '<option value="' +
+                  dataItem[l] +
+                  '">' +
+                  dataItem[l] +
+                  "</option>";
+              }
+            }
+            selectHtml += "</select>";
+            data = data.replace("{{" + item.id + "}}", selectHtml);
+          } else {
+            //文本
+            data = data.replace(
+              "{{" + attrId + "}}",
+              /* `<input type="text" class="text-input-box"  data-index="${i}" value="${_this.com.attrs[i].content}" @input="updateContent('${_this.com.attrs[i].id}', $event)" id="${attrId}">`, */
+              '<input type="text" ref="input_' +
+                attrId +
+                '" name="' +
+                _this.com.attrs[i].name +
+                '" id="' +
+                attrId +
+                '"  data-index="' +
+                i +
+                '" class="text-input-box auto-width" value="' +
+                _this.com.attrs[i].content +
+                '">'
+            );
+          }
+        } else if (_this.com.attrs[i].type == "variableNull") {
+          let item = _this.com.attrs[i];
+          // 使用唯一的标识符来初始化每个输入框的值
+          this.variableNullInputs[attrId] = item.content;
+          data = data.replace(
+            "{{" + attrId + "}}",
+            '<input type="text" ' +
+              'ref="input_' +
+              attrId +
+              '" ' +
+              'name="' +
+              item.name +
+              '" ' +
+              'id="' +
+              attrId +
+              '" ' +
+              'data-index="' +
+              i +
+              '" ' +
+              'data-attr-id="' +
+              attrId +
+              '" ' +
+              'class="text-input-boxs auto-width" ' +
+              'value="' +
+              item.content +
+              '">'
+          );
+        } else if (_this.com.attrs[i].type == "ProductAttr") {
+          //处理产品属性值
+          let item = _this.com.attrs[i];
+          let prodAttrId = item.id + "_" + i;
+          if (item.content == "") {
+            item.content = item.attrs.value;
+          }
+          if (item.attrs.type == 1) {
+            //输入框
+            data = data.replace(
+              "{{" + item.id + "}}",
+              '<input type="text" id="' +
+                prodAttrId +
+                '"  data-index="' +
+                i +
+                '" class="text-input-box" value="' +
+                item.content +
+                '">'
+            );
+          } else {
+            //选择框
+            let dataItem = item.attrs.valueItems.split(",");
+            let selectHtml =
+              '<select id="' +
+              prodAttrId +
+              '"  data-index="' +
+              i +
+              '" class="text-input-box">';
+            for (var l = 0; l < dataItem.length; l++) {
+              if (item.content == dataItem[l]) {
+                selectHtml +=
+                  '<option value="' +
+                  dataItem[l] +
+                  '" selected>' +
+                  dataItem[l] +
+                  "</option>";
+              } else {
+                selectHtml +=
+                  '<option value="' +
+                  dataItem[l] +
+                  '">' +
+                  dataItem[l] +
+                  "</option>";
+              }
+            }
+            selectHtml += "</select>";
+            data = data.replace("{{" + item.id + "}}", selectHtml);
+          }
+        } else if (_this.com.attrs[i].type == "formual") {
+          //处理计算公式 不处理
+          let formual = await _this.analysisFormual(_this.com.attrs[i]);
+          let point = _this.com.attrs[i].data.point;
+          formual = await _this.getRemote(formual);
+
+          /* console.log("处理的公式:", formual); // 添加日志 */
+
+          // 收集公式中使用的变量
+          const variables = {};
+          const variableRegex = /\b[a-zA-Z_][a-zA-Z0-9_]*\b/g;
+          const matches = formual.match(variableRegex);
+          if (matches) {
+            for (const varName of matches) {
+              if (
+                varName !== "IF" &&
+                !Object.keys(mathFunctions).includes(varName)
+              ) {
+                variables[varName] = `${varName}`; // 默认值设为0,您可以根据需要修改
+              }
+            }
+          }
+
+          /*   console.log("识别的变量:", variables); // 添加日志 */
+
+          // 定义一个安全的评估函数
+          const safeEval = (formula, variables) => {
+            // 创建一个新的函数,将数学函数和变量作为参数传入
+            const func = new Function(
+              ...Object.keys(mathFunctions),
+              ...Object.keys(variables),
+              `return ${formula}`
+            );
+
+            // 执行函数,传入数学函数和变量作为参数
+            return func(
+              ...Object.values(mathFunctions),
+              ...Object.values(variables)
+            );
+          };
+
+          try {
+            const result = safeEval(formual, variables);
+            /*  console.log("计算结果:", result); // 添加日志 */
+
+            let formattedResult;
+            if (typeof result === "number" && !isNaN(result)) {
+              formattedResult = result.toFixed(point);
+            } else if (typeof result === "boolean") {
+              formattedResult = result ? "1" : "0";
+            } else {
+              formattedResult = String(result);
+            }
+
+            _this.com.attrs[i].content = formattedResult;
+            data = data.replace(
+              "{{" + _this.com.attrs[i].id + "}}",
+              formattedResult
+            );
+          } catch (error) {
+            console.error("处理公式时出错:", error);
+            console.error("错误的公式:", formual);
+            _this.com.attrs[i].content = "计算错误";
+            data = data.replace(
+              "{{" + _this.com.attrs[i].id + "}}",
+              "计算错误"
+            );
+          }
+
+          /* let formual = await _this.analysisFormual(_this.com.attrs[i]);
+          formual = await _this.getRemote(formual);
+          // 使用 try-catch 来处理可能的计算错误
+          try {
+            let result = eval(formual);
+            data = data.replace("{{" + attrId + "}}", result);
+          } catch (error) {
+            data = data.replace("{{" + attrId + "}}", "计算错误");
+          } */
+        } else if (_this.com.attrs[i].type == "sourceData") {
+          let result = await _this.getRemote1(_this.com.attrs[i].formula);
+          data = data.replace("{{" + attrId + "}}", result);
+        } else if (_this.com.attrs[i].type == "Directory") {
+          const directoryContent =
+            _this.com.attrs[i].number + ". " + _this.com.attrs[i].content;
+          const level = _this.com.attrs[i].level || 1; // 默认为1级目录
+          const attrId = _this.com.attrs[i].id;
+
+          // 创建一个更灵活的正则表达式模式,匹配可能存在的div包裹
+          const directoryRegex = new RegExp(
+            `<div[^>]*>\\s*{{\\s*${attrId}\\s*}}\\s*</div>|{{\\s*${attrId}\\s*}}`,
+            "g"
+          );
+
+          // 替换内容,直接生成标题标签
+          data = data.replace(directoryRegex, (match, offset, string) => {
+            // 根据级别创建适当的HTML标签
+            const tag = `h${Math.min(level, 6)}`; // 限制最大级别为h6
+            const className = `directory-level-${level}`;
+            return `<${tag} class="${className}">${directoryContent}</${tag}>`;
+          });
+          //处理目录
+          /* data = data.replace(
+            "<p>{{" + attrId + "}}</p>",
+            "<h1>" + _this.com.attrs[i].content + "</h1>"
+          );
+          data = data.replace(
+            "<div>{{" + attrId + "}}</div>",
+            "<h1>" + _this.com.attrs[i].content + "</h1>"
+          );
+          data = data.replace(
+            "<span>{{" + attrId + "}}</span>",
+            "<h1>" + _this.com.attrs[i].content + "</h1>"
+          ); */
+          //处理目录
+          /*  const directoryContent = _this.com.attrs[i].content;
+          const directoryRegex = new RegExp(
+            `<(p|div|span)>{{${attrId}}}</(p|div|span)>`,
+            "g"
+          );
+          data = data.replace(directoryRegex, `<h1>${directoryContent}</h1>`); */
+        } else {
+          //处理常量及其它
+          data = data.replace("{{" + attrId + "}}", _this.com.attrs[i].content);
+        }
+      }
+      this.$nextTick(() => {
+        this.addInputListeners();
+        this.initializeInputWidths();
+        this.addVariableNullListeners();
+      });
+      return data;
+    },
+    // 新增方法:为 variableNull 类型的输入框添加事件监听器
+    addVariableNullListeners() {
+      this.$el.addEventListener("input", this.handleVariableNullInput);
+      this.$el.addEventListener("blur", this.handleVariableNullBlur, true);
+    },
+
+    handleVariableNullInput(event) {
+      if (event.target.classList.contains("text-input-boxs")) {
+        const attrId = event.target.dataset.attrId;
+        const newValue = event.target.value;
+        // 只更新特定输入框的值
+        this.$set(this.variableNullInputs, attrId, newValue);
+      }
+    },
+
+    handleVariableNullBlur(event) {
+      if (event.target.classList.contains("text-input-boxs")) {
+        const index = parseInt(event.target.dataset.index, 10);
+        const attrId = event.target.dataset.attrId;
+        const newValue = this.variableNullInputs[attrId];
+        // 只更新特定属性的内容
+        this.updateVariableNullContent(index, attrId, newValue, true);
+      }
+    },
+
+    updateVariableNullContent(index, attrId, newValue, emitEvent = false) {
+      if (this.com && this.com.attrs && this.com.attrs[index]) {
+        if (this.com.attrs[index].id === attrId) {
+          // 只更新特定属性的内容
+          this.$set(this.com.attrs[index], "content", newValue);
+
+          // 更新输入框的值
+          this.$nextTick(() => {
+            const input = this.$el.querySelector(`#${attrId}`);
+            if (input) {
+              input.value = newValue;
+            }
+          });
+
+          // 触发事件
+          if (emitEvent) {
+            this.$emit("onUpdata", this.currentIndex, index, attrId, newValue);
+          }
+        }
+      }
+    },
+    addInputListeners() {
+      // 使用事件委托来处理所有的 .text-input-box 元素
+      this.$el.addEventListener("blur", this.handleInputChange, true);
+      this.$el.addEventListener("input", this.adjustInputWidth, true);
+    },
+    initializeInputWidths() {
+      const inputs = this.$el.querySelectorAll(
+        ".text-input-box, .text-input-boxs"
+      );
+      inputs.forEach((input) => this.adjustInputWidth({ target: input }));
+    },
+
+    // 修改这个方法,使其同时处理 variableNull 类型的输入框
+    adjustInputWidth(event) {
+      if (
+        event.target.classList.contains("text-input-box") ||
+        event.target.classList.contains("text-input-boxs")
+      ) {
+        const input = event.target;
+        const sizeCalculator = document.createElement("span");
+        sizeCalculator.className = "size-calculator";
+        sizeCalculator.textContent = input.value || input.placeholder || "0";
+        document.body.appendChild(sizeCalculator);
+
+        const styles = window.getComputedStyle(input);
+        sizeCalculator.style.font = styles.font;
+        sizeCalculator.style.fontSize = styles.fontSize;
+        sizeCalculator.style.fontWeight = styles.fontWeight;
+        sizeCalculator.style.letterSpacing = styles.letterSpacing;
+
+        const width = sizeCalculator.offsetWidth;
+        input.style.width = `${width + 10}px`; // Add some padding
+
+        document.body.removeChild(sizeCalculator);
+      }
+    },
+
+    // 修改这个方法,使其同时处理 variableNull 类型的输入框
+    handleInputChange(event) {
+      if (
+        event.target.classList.contains("text-input-box") ||
+        event.target.classList.contains("text-input-boxs")
+      ) {
+        const attrId = event.target.id;
+        const attrName = event.target.name;
+        const dataIndex = parseInt(event.target.dataset.index, 10);
+        const newValue = event.target.value;
+        // 更新数据
+        if (this.com && this.com.attrs && this.com.attrs[dataIndex]) {
+          this.$set(this.com.attrs[dataIndex], "content", newValue);
+
+          this.com.attrs.forEach((el) => {
+            if (el.name === attrName) {
+              this.$set(el, "content", newValue);
+            }
+          });
+          this.$emit("onUpdateAttr", this.currentIndex, dataIndex, newValue);
+        } else {
+          console.warn(`Unable to find attribute at index ${dataIndex}`);
+        }
+      }
+    },
+
+    bindEvents() {
+      let _this = this;
+      for (var i = 0; i < _this.com.attrs.length; i++) {
+        let attrId = _this.com.attrs[i].id;
+        let item = _this.com.attrs[i];
+        if (_this.com.attrs[i].type == "variable") {
+          //变量事件
+          let input = _this.$el.querySelector("#" + attrId);
+          if (input) {
+            if (item.data.value_type == 2) {
+              //下拉选择
+              input.addEventListener("change", _this.handleInput);
+            } else {
+              //文本输入
+              input.addEventListener("blur", _this.handleInput);
+            }
+          }
+        } else if (_this.com.attrs[i].type == "ProductAttr") {
+          //绑定产品属性事件
+          let prodAttrId = item.id + "_" + i;
+          let input = _this.$el.querySelector("#" + prodAttrId);
+          if (input) {
+            if (item.attrs.type == 1) {
+              input.addEventListener("blur", _this.handleInputProduct);
+            } else {
+              input.addEventListener("change", _this.handleChangeProduct);
+            }
+          }
+        }
+      }
+    },
+
+    //分析公式
+    async analysisFormual(attrs) {
+      let _this = this;
+      /*  console.log("开始分析公式:", attrs.formula); */
+      let formual = attrs.formula;
+
+      // 处理 [T][...][...] 引用
+      const tPattern = /\[T\]\[(.*?)\]\[(.*?)\]/g;
+      formual = await this.replaceAsync(
+        formual,
+        tPattern,
+        async (match, p1, p2) => {
+          try {
+            let data = await _this.getModuleData(p1, p2);
+            /*   console.log(`获取到的数据: ${match} = ${data}`); */
+            if (data === null || data === undefined) {
+              console.warn(`获取到的数据无效: ${match}`);
+              return "''"; // 返回空字符串而不是 0
+            } else {
+              return typeof data === "string" ? `${data}` : data;
+            }
+          } catch (error) {
+            console.error(`处理 ${match} 时出错:`, error);
+            return "''";
+          }
+        }
+      );
+
+      // 处理嵌套的 IF 语句
+      const ifPattern = /IF\s*\((.*?),(.*?),(.*?)\)/gi;
+      let depth = 0;
+      while (formual.match(ifPattern) && depth < 10) {
+        formual = formual.replace(
+          ifPattern,
+          (match, condition, trueValue, falseValue) => {
+            // 递归处理嵌套的 IF 语句
+            if (
+              trueValue.includes("IF(") ||
+              falseValue.includes("IF(") ||
+              trueValue.includes("if(") ||
+              falseValue.includes("if(")
+            ) {
+              return `(${condition} ? (${trueValue}) : (${falseValue}))`;
+            }
+            return `(${condition} ? ${trueValue} : ${falseValue})`;
+          }
+        );
+        depth++;
+      }
+
+      /* console.log("分析后的公式:", formual); */
+      return formual;
+    },
+
+    // 辅助方法:异步替换
+    async replaceAsync(str, regex, asyncFn) {
+      const promises = [];
+      str.replace(regex, (match, ...args) => {
+        const promise = asyncFn(match, ...args);
+        promises.push(promise);
+      });
+      const data = await Promise.all(promises);
+      return str.replace(regex, () => data.shift());
+    },
+
+    // 辅助方法:异步替换
+    async replaceAsync(str, regex, asyncFn) {
+      const promises = [];
+      str.replace(regex, (match, ...args) => {
+        const promise = asyncFn(match, ...args);
+        promises.push(promise);
+      });
+      const data = await Promise.all(promises);
+      return str.replace(regex, () => data.shift());
+    },
+
+    async getFormualData(formualItem, data) {
+      let _this = this;
+      // console.log("开始获取公式数据:", formualItem);
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      let items = formualItem.match(reg);
+      // console.log("解析结果:", items);
+      let calResult = 0;
+      if (items && items[1] == "T") {
+        calResult = await _this.getModuleData(items[2], items[3]);
+      } else {
+        console.warn("无法解析公式项:", formualItem);
+      }
+      /*     console.log("计算结果:", formualItem,data); */
+      return calResult;
+    },
+
+    async getRemote(formual) {
+      let _this = this;
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      while (true) {
+        let items = formual.match(reg);
+        if (items == null) break;
+        if (items[1] == "R") {
+          //表示获取远程数据
+          var dataIndex = items[4];
+          dataIndex = items[4].split(",");
+          let calResult = await _this.getRemoteData(
+            items[2],
+            items[3],
+            dataIndex[0],
+            dataIndex[1]
+          );
+          let itemName = items[0];
+          // 检查calResult是否为数字
+          if (!isNaN(calResult)) {
+            formual = formual.replace(
+              itemName,
+              "(" + parseFloat(calResult) + ")"
+            );
+          } else {
+            formual = formual.replace(itemName, `"${calResult}"`);
+          }
+          formual = formual.replace(
+            itemName,
+            "(" + parseFloat(calResult) + ")"
+          );
+        }
+      }
+      return formual;
+    },
+
+    async getRemote1(formual) {
+      let _this = this;
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      while (true) {
+        let items = formual.match(reg);
+        if (items == null) break;
+        if (items[1] == "R") {
+          //表示获取远程数据
+          var dataIndex = items[4];
+          dataIndex = items[4].split(",");
+          let calResult = await _this.getRemoteData(
+            items[2],
+            items[3],
+            dataIndex[0],
+            dataIndex[1]
+          );
+          let itemName = items[0];
+          // 检查calResult是否为数字
+          if (!isNaN(calResult)) {
+            formual = formual.replace(
+              itemName,
+              "(" + parseFloat(calResult) + ")"
+            );
+          } else {
+            formual = formual.replace(itemName, `"${calResult}"`);
+          }
+          /* formual=formual.replace(itemName,parseFloat(calResult)); */
+        }
+      }
+      return formual;
+    },
+    onFocus() {
+      this.isEditing = true;
+    },
+
+    onBlur() {
+      this.isEditing = false;
+    },
+
+    async getRemoteData(code, sheet, row, col) {
+      let _this = this;
+      let result = 0;
+      let par = {
+        code: code,
+        sheetName: sheet,
+        row: row,
+        col: col,
+      };
+
+      /*  if (!this.isEditing) {
+        this.loading = true;
+        this.progress = 0;
+
+        try {
+          const updateProgress = setInterval(() => {
+            if (this.progress < 90) {
+              this.progress += 10;
+            }
+          }, 100);
+
+          let res = await findData(par);
+          if (res.status == 200) {
+            result = res.data.result;
+          }
+
+          this.progress = 100;
+          clearInterval(updateProgress);
+        } catch (error) {
+          console.error("Error fetching remote data:", error);
+        } finally {
+          setTimeout(() => {
+            this.loading = false;
+            this.progress = 0;
+          }, 1000);
+        }
+      } else { */
+      // 如果正在编辑,直接获取数据而不显示进度条
+      try {
+        let res = await findData(par);
+        // 添加响应检查
+        if (!res) {
+          console.warn("No response received from findData");
+          return result;
+        }
+
+        // 检查响应格式
+        if (res.status === 200 && res.data) {
+          result = res.data.result;
+          // 添加数据类型检查和转换
+          if (typeof result === "string" && !isNaN(result)) {
+            result = parseFloat(result);
+          }
+        } else {
+          console.warn("Invalid response format:", res);
+        }
+      } catch (error) {
+        console.error("获取远程数据错误:", error);
+        // 可以在这里添加错误通知
+        this.$message && this.$message.error("获取远程数据失败");
+      }
+
+      return result;
+    },
+    //获取本地文档模块数据
+    async getModuleData(code, attrName) {
+      let _this = this;
+      /* console.log(`尝试获取模块数据: code=${code}, attrName=${attrName}`);
+      console.log("当前 coms:", _this.coms); */
+
+      let item = _this.coms.filter((o) => o.name == code);
+      /*   console.log(`找到的模块:`, item); */
+
+      if (item.length > 0) {
+        let attr = item[0].attrs.filter((o) => o.name == attrName);
+        /* console.log(`找到的属性:`, attr); */
+
+        if (attr.length > 0) {
+          /* console.log(`返回的值:`, attr[0].content); */
+          return attr[0].content; // 返回原始值,不进行转换
+        }
+      }
+      /*  console.log("未找到匹配的模块或属性,返回空字符串"); */
+      return ""; // 返回空字符串而不是 0
+    },
+
+    handleChangeProduct(e) {
+      let dataIndex = e.target.dataset.index;
+      this.$emit(
+        "onUpdateProdAttr",
+        this.currentIndex,
+        dataIndex,
+        e.target.value
+      );
+    },
+
+    handleInputProduct(e) {
+      let dataIndex = e.target.dataset.index;
+      this.$emit(
+        "onUpdateProdAttr",
+        this.currentIndex,
+        dataIndex,
+        e.target.value
+      );
+    },
+    handleInput(e) {
+      let dataIndex = e.target.dataset.index;
+      this.$emit("onUpdateAttr", this.currentIndex, dataIndex, e.target.value);
+    },
+
+    onEditorReady(editor) {},
+
+    onInputText(e) {
+      this.$emit("onUpdate", this.currentIndex, e);
+    },
+
+    /* 点击替换图片 */
+    handleImageClick(event) {
+      if (event.target.tagName === "IMG") {
+        this.replaceImage(event.target);
+      }
+    },
+    selectImage() {
+      return new Promise((resolve) => {
+        const input = document.createElement("input");
+        input.type = "file";
+        input.accept = "image/*";
+        input.onchange = (e) => resolve(e.target.files[0]);
+        input.click();
+      });
+    },
+
+    async uploadImage(formData) {
+      try {
+        const response = await axios.post(
+          "http://58.246.234.210:8084/upload/image",
+          formData,
+          {
+            headers: {
+              "Content-Type": "multipart/form-data",
+            },
+          }
+        );
+        console.log("Upload response:", response);
+        if (response.status === 200 && response.data && response.data.url) {
+          return response.data.url;
+        } else {
+          throw new Error("Invalid upload response");
+        }
+      } catch (error) {
+        console.error("Error uploading image:", error);
+        throw error;
+      }
+    },
+    /* updateContentWithNewImage(imgElement) {
+      // 更新 content 中的图片 URL
+      const tempDiv = document.createElement('div');
+      tempDiv.innerHTML = this.content;
+      const images = tempDiv.getElementsByTagName('img');
+      for (let img of images) {
+        if (img.src === imgElement.src) {
+          img.src = imgElement.src;
+          break;
+        }
+      }
+      this.content = tempDiv.innerHTML;
+      this.$emit('onUpdate', this.currentIndex, this.content);
+    }, */
+
+    async replaceImage(imgElement) {
+      try {
+        const file = await this.selectImage();
+        if (file) {
+          const formData = new FormData();
+          formData.append("upload", file);
+          const uploadedUrl = await this.uploadImage(formData);
+
+          // Update the image element in the DOM
+          imgElement.src = uploadedUrl;
+
+          // Update the content and notify parent component
+          this.$nextTick(() => {
+            this.updateContentWithNewImage(imgElement, uploadedUrl);
+          });
+        }
+      } catch (error) {
+        console.error("Error replacing image:", error);
+      }
+    },
+
+    updateContentWithNewImage(imgElement, newImageUrl) {
+      // Get the rich-editor div
+      const richEditor = this.$refs.richEditor;
+
+      // Create a new Image element with the new URL
+      const newImg = document.createElement("img");
+      newImg.src = newImageUrl;
+
+      // Copy attributes from the old image to the new one
+      for (let attr of imgElement.attributes) {
+        if (attr.name !== "src") {
+          newImg.setAttribute(attr.name, attr.value);
+        }
+      }
+
+      // Replace the old image with the new one
+      imgElement.parentNode.replaceChild(newImg, imgElement);
+
+      // Update this.content with the latest DOM content
+      this.content = richEditor.innerHTML;
+
+      // Update the com object
+      this.com.content = this.content;
+
+      // Emit an event to update the parent component
+      this.$emit("updateComContent", this.currentIndex, this.com);
+    },
+
+    /*  updateComsData(newImageUrl) {
+      // 找到当前组件在 coms 数组中的索引
+      const comIndex = this.coms.findIndex((com) => com.name === this.com.name);
+      if (comIndex !== -1) {
+        // 创建 coms 的深拷贝
+        const updatedComs = JSON.parse(JSON.stringify(this.coms));
+
+        // 更新特定组件的 content
+        updatedComs[comIndex].content = this.content;
+
+        // 如果有特定的图片属性,也更新它
+        const imgAttr = updatedComs[comIndex].attrs.find(
+          (attr) => attr.type === "image"
+        );
+        if (imgAttr) {
+          imgAttr.content = newImageUrl;
+        }
+
+        // 更新 coms
+        this.$emit("update:coms", updatedComs);
+      }
+    }, */
+    /* onFocus(e) {
+      // var range = e.editor.getSelection().getRanges()[0];
+      // range.collapse( true );
+      // range.setStartAt( e.editor.editable(), CKEDITOR.POSITION_AFTER_START );
+      // e.editor.insertText("AAA");
+    },
+    onBlur(e) {
+      //// console.log(' onBlur e',e);
+      // this.$emit("onUploadAttr",this.dataIndex,this.dataAttr);
+    }, */
+  },
+};
+</script>
+<style lang="scss" scoped>
+@import "./index.scss";
+.template-textarea {
+  position: relative;
+}
+
+.overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5); // Semi-transparent background
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10; // Ensure it is above other content
+}
+
+.full-width-progress {
+  width: 80%; // Adjust as needed
+  color: inherit; // Ensure the color remains unchanged
+}
+.auto-width-wrapper {
+  display: inline-block;
+  position: relative;
+}
+
+.size-calculator {
+  position: absolute;
+  top: -9999px;
+  left: -9999px;
+  white-space: pre;
+  font-family: inherit;
+  font-size: inherit;
+  font-weight: inherit;
+  padding: 2px 4px;
+}
+.editor-wrapper {
+  position: relative;
+}
+
+.sticky-toolbar {
+  position: sticky;
+  top: 0;
+  z-index: 1000;
+  background-color: #fff;
+  border-bottom: 1px solid #d1d1d1;
+}
+
+.editor-area {
+  ::v-deep .cke_top {
+    display: block; // Hide the original toolbar
+  }
+}
+::v-deep .directory-level-1 {
+  font-size: 24px;
+  font-weight: bold;
+  margin-left: 0;
+}
+
+::v-deep .directory-level-2 {
+  font-size: 20px;
+  font-weight: bold;
+  margin-left: 20px;
+}
+
+::v-deep .directory-level-3 {
+  font-size: 18px;
+  font-weight: bold;
+  margin-left: 40px;
+}
+
+::v-deep .directory-level-4 {
+  font-size: 16px;
+  font-weight: bold;
+  margin-left: 60px;
+}
+
+::v-deep .directory-level-5 {
+  font-size: 14px;
+  font-weight: bold;
+  margin-left: 80px;
+}
+
+::v-deep .directory-level-6 {
+  font-size: 12px;
+  font-weight: normal;
+  margin-left: 100px;
+}
+</style>

+ 74 - 0
ui/src/views/system/document/com/components/TextView/index.scss

@@ -0,0 +1,74 @@
+
+
+.template-textarea{
+
+  width:100%;
+  display:flex;
+  flex-direction:column;
+  position: relative;
+  .rich-editor{
+    width:100%;
+    min-height: 50px;
+
+  }
+  .content{
+    padding:10px;
+  }
+  .editor{
+
+    width:100%;
+    display:flex;
+    flex-direction:column;
+    justify-content: flex-start;
+    align-items: flex-start;
+  }
+  .menus{
+    position:absolute;
+    bottom:0px;
+    right:0px;
+    font-size:12px;
+    display:flex;
+    width:50px;
+    height:100px;
+    border:1px dashed #CCC;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    .attr{
+      display:flex;
+      width:100%;
+      height:50px;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      .attr-icon-size{
+        width:28px;
+        height:28px;
+      }
+    }
+    .delete{
+      display:flex;
+      width:100%;
+      height:50px;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      .attr-icon-size{
+          width:28px;
+          height:28px;
+        }
+    }
+  }
+
+}
+
+.attr-icon-size{
+  width:48px;
+  height:48px;
+}
+
+.min-height{
+  min-height:300px;
+}
+
+

+ 480 - 0
ui/src/views/system/document/com/components/TextView/index.vue

@@ -0,0 +1,480 @@
+<template>
+  <div class="template-textarea">
+    <div class="rich-editor" v-html="processedContent"></div>
+  </div>
+</template>
+
+<script>
+import CKEditor from "ckeditor4-vue"; //@input="handleInput"
+import { findData } from "@/api/sourceData";
+export default {
+  name: "app",
+  emits: ["onUpdateData"],
+  compnents: {
+    // ckeditor: CKEditor.component
+  },
+  props: {
+    coms: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+    currentIndex: {
+      type: Number,
+      default: 0,
+    },
+    com: {
+      type: Object,
+      default: null,
+    },
+  },
+  watch: {
+    com: {
+      handler(val) {
+        if (val && val.content) {
+          this.updateProcessedContent();
+        }
+        /*  let _this = this;
+        if (val == null) return;
+        if (val.content == undefined || val.content == null) return;
+        _this.replaceData(val.content).then((res) => {
+          _this.content = res;
+          _this.$emit("onUpdateData", this.currentIndex, res);
+        }); */
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      content: "",
+      processedContent: "",
+    };
+  },
+  methods: {
+    async updateProcessedContent() {
+      this.processedContent = await this.replaceData(this.com.content);
+    },
+    async replaceData(data) {
+      let _this = this;
+      for (var i = 0; i < _this.com.attrs.length; i++) {
+        let attrId = _this.com.attrs[i].id;
+        if (_this.com.attrs[i].type == "variable") {
+          let item = _this.com.attrs[i];
+          if (item.data.value_type == 2) {
+            //多选
+            let dataItem = item.data.value_item.split(",");
+            let selectHtml =
+              '<select id="' +
+              attrId +
+              '"  data-index="' +
+              i +
+              '" class="text-input-box">';
+            for (var l = 0; l < dataItem.length; l++) {
+              if (item.content == dataItem[l]) {
+                selectHtml +=
+                  '<option value="' +
+                  dataItem[l] +
+                  '" selected>' +
+                  dataItem[l] +
+                  "</option>";
+              } else {
+                selectHtml +=
+                  '<option value="' +
+                  dataItem[l] +
+                  '">' +
+                  dataItem[l] +
+                  "</option>";
+              }
+            }
+            selectHtml += "</select>";
+            data = data.replace("{{" + item.id + "}}", selectHtml);
+          } else {
+            //文本
+            data = data.replace(
+              "{{" + attrId + "}}",
+              /* `<input type="text" class="text-input-box"  data-index="${i}" value="${_this.com.attrs[i].content}" @input="updateContent('${_this.com.attrs[i].id}', $event)" id="${attrId}">`, */
+              '<input type="text" ref="input_' +
+                attrId +
+                '" name="' +
+                _this.com.attrs[i].name +
+                '" id="' +
+                attrId +
+                '"  data-index="' +
+                i +
+                '" class="text-input-box" value="' +
+                _this.com.attrs[i].content +
+                '">'
+            );
+          }
+        }else if(_this.com.attrs[i].type == "variableNull"){
+          let item = _this.com.attrs[i];
+          data = data.replace(
+              "{{" + attrId + "}}",
+              '<input type="text" ref="input_' +
+                attrId +
+                '" name="' +
+                _this.com.attrs[i].name +
+                '" id="' +
+                attrId +
+                '"  data-index="' +
+                i +
+                '" class="text-input-boxs auto-width" value="' +
+                _this.com.attrs[i].content +
+                '">'
+            );
+
+        }  else if (_this.com.attrs[i].type == "ProductAttr") {
+          //处理产品属性值
+          let item = _this.com.attrs[i];
+          let prodAttrId = item.id + "_" + i;
+          if (item.content == "") {
+            item.content = item.attrs.value;
+          }
+          if (item.attrs.type == 1) {
+            //输入框
+            data = data.replace(
+              "{{" + item.id + "}}",
+              '<input type="text" id="' +
+                prodAttrId +
+                '"  data-index="' +
+                i +
+                '" class="text-input-box" value="' +
+                item.content +
+                '">'
+            );
+          } else {
+            //选择框
+            let dataItem = item.attrs.valueItems.split(",");
+            let selectHtml =
+              '<select id="' +
+              prodAttrId +
+              '"  data-index="' +
+              i +
+              '" class="text-input-box">';
+            for (var l = 0; l < dataItem.length; l++) {
+              if (item.content == dataItem[l]) {
+                selectHtml +=
+                  '<option value="' +
+                  dataItem[l] +
+                  '" selected>' +
+                  dataItem[l] +
+                  "</option>";
+              } else {
+                selectHtml +=
+                  '<option value="' +
+                  dataItem[l] +
+                  '">' +
+                  dataItem[l] +
+                  "</option>";
+              }
+            }
+            selectHtml += "</select>";
+            data = data.replace("{{" + item.id + "}}", selectHtml);
+          }
+        } else if (_this.com.attrs[i].type == "formual") {
+          //// console.log('attrs',_this.com.attrs[i]);
+          //处理计算公式 不处理
+          let formual = await _this.analysisFormual(_this.com.attrs[i]);
+
+          formual = await _this.getRemote(formual);
+          _this.com.attrs[i].content = eval(formual);
+          data = data.replace("{{" + attrId + "}}", eval(formual));
+        } else if (_this.com.attrs[i].type == "sourceData") {
+          let result = await _this.getRemote1(_this.com.attrs[i].formula);
+          data = data.replace("{{" + attrId + "}}", result);
+        } else if (_this.com.attrs[i].type == "Directory") {
+          const directoryContent = _this.com.attrs[i].number+'. '+_this.com.attrs[i].content;;
+          const level = _this.com.attrs[i].level || 1; // 默认为1级目录
+          const attrId = _this.com.attrs[i].id;
+
+          // 创建一个更灵活的正则表达式模式
+          const directoryRegex = new RegExp(`{{\\s*${attrId}\\s*}}`, "g");
+
+          // 替换内容,直接在标题标签上应用类名
+          data = data.replace(directoryRegex, (match, offset, string) => {
+            // 根据级别创建适当的HTML标签
+            const tag = `h${Math.min(level, 6)}`; // 限制最大级别为h6
+            const className = `directory-level-${level}`;
+            return `<${tag} class="${className}">${directoryContent}</${tag}>`;
+          });
+          /* const directoryContent = _this.com.attrs[i].content;
+          const attrId = _this.com.attrs[i].id;
+
+          // Create a more flexible regex pattern
+          const directoryRegex = new RegExp(`{{\\s*${attrId}\\s*}}`, "g");
+
+          // Replace the content, preserving surrounding HTML tags
+          data = data.replace(directoryRegex, (match, offset, string) => {
+            // Check if the match is inside a tag
+            const precedingText = string.substring(0, offset);
+            const followingText = string.substring(offset + match.length);
+            const precedingTag = precedingText.match(/<([^>]+)>$/);
+            const followingCloseTag = followingText.match(/^<\/([^>]+)>/);
+
+            if (
+              precedingTag &&
+              followingCloseTag &&
+              precedingTag[1] === followingCloseTag[1]
+            ) {
+              // If it's inside matching tags, replace the entire element
+              return `<h1>${directoryContent}</h1>`;
+            } else {
+              // Otherwise, just replace the placeholder
+              return directoryContent;
+            }
+          }); */
+        } else {
+          //处理常量及其它
+          data = data.replace("{{" + attrId + "}}", _this.com.attrs[i].content);
+        }
+      }
+      this.$nextTick(() => {
+        this.addInputListeners()
+        this.autoWidths()
+      });
+      return data;
+    },
+    addInputListeners() {
+      // 使用事件委托来处理所有的 .text-input-box 元素
+      this.$el.addEventListener("input", this.adjustAutoWidth, true);
+    },
+    autoWidths(){
+      const inputs = this.$el.querySelectorAll(".text-input-boxs");
+      inputs.forEach((input) => this.adjustAutoWidth({ target: input }));
+    },
+    adjustAutoWidth(event){
+      if (event.target.classList.contains("text-input-boxs")) {
+        const input = event.target;
+        const sizeCalculator = document.createElement("span");
+        sizeCalculator.className = "size-calculator";
+        sizeCalculator.textContent = input.value || input.placeholder || "0";
+        document.body.appendChild(sizeCalculator);
+
+        const styles = window.getComputedStyle(input);
+        sizeCalculator.style.font = styles.font;
+        sizeCalculator.style.fontSize = styles.fontSize;
+        sizeCalculator.style.fontWeight = styles.fontWeight;
+        sizeCalculator.style.letterSpacing = styles.letterSpacing;
+
+        const width = sizeCalculator.offsetWidth;
+        input.style.width = `${width + 10}px`; // Add some padding
+
+        document.body.removeChild(sizeCalculator);
+      }
+    },
+    async analysisFormual(attrs) {
+      let _this = this;
+      // console.log("开始分析公式:", attrs.formula);
+      var pattern = /(\[.*?\]){3}/;
+      var formual = attrs.formula;
+      var reg = new RegExp(pattern);
+      while (true) {
+        var items = formual.match(reg);
+        if (items == null) break;
+        let itemName = items[0];
+        // console.log("处理项:", itemName);
+        try {
+          let data = await _this.getFormualData(itemName);
+          // console.log(`获取到的数据: ${itemName} = ${data}`);
+          if (data === null || data === undefined || isNaN(data)) {
+            console.warn(`获取到的数据无效: ${itemName}`);
+            formual = formual.replace(itemName, "(0)");
+          } else {
+            formual = formual.replace(itemName, `(${parseFloat(data)})`);
+          }
+        } catch (error) {
+          console.error(`处理 ${itemName} 时出错:`, error);
+          formual = formual.replace(itemName, "(0)");
+        }
+      }
+      return formual;
+    },
+    async getFormualData(formualItem) {
+      let _this = this;
+      // console.log("开始获取公式数据:", formualItem);
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      let items = formualItem.match(reg);
+      // console.log("解析结果:", items);
+      let calResult = 0;
+      if (items && items[1] == "T") {
+        calResult = await _this.getModuleData(items[2], items[3]);
+      } else {
+        console.warn("无法解析公式项:", formualItem);
+      }
+      // console.log("计算结果:", calResult);
+      return calResult;
+    },
+    updateContent(id, event) {
+      const attr = this.com.attrs.find((attr) => attr.id === id);
+      if (attr) {
+        attr.content = event.target.value;
+      }
+    },
+    /* async replaceData(data) {
+      let _this = this;
+      for (var i = 0; i < _this.com.attrs.length; i++) {
+        let attrId = _this.com.attrs[i].id;
+        if (_this.com.attrs[i].type == "formula") {
+          let formula = await _this.analysisformula(_this.com.attrs[i]);
+          formula = await _this.getRemote(formula);
+          let evalResult = eval(formula);
+          if (!isNaN(evalResult)) {
+            data = data.replace("{{" + attrId + "}}", evalResult);
+          }
+        } else if (_this.com.attrs[i].type == "sourceData") {
+          let result = await _this.getRemote1(_this.com.attrs[i].formula);
+          if (!isNaN(result)) {
+            data = data.replace("{{" + attrId + "}}", result);
+          }
+        } else if (_this.com.attrs[i].type == "Directory") {
+          //处理目录
+          data = data.replace(
+            "<p>{{" + attrId + "}}</p>",
+            "<h1>" + _this.com.attrs[i].content + "</h1>"
+          );
+          data = data.replace(
+            "<div>{{" + attrId + "}}</div>",
+            "<h1>" + _this.com.attrs[i].content + "</h1>"
+          );
+          data = data.replace(
+            "<span>{{" + attrId + "}}</span>",
+            "<h1>" + _this.com.attrs[i].content + "</h1>"
+          );
+        } else {
+          data = data.replace("{{" + attrId + "}}", _this.com.attrs[i].content);
+        }
+      }
+      return data;
+    }, */
+
+    //分析公式
+    async analysisformula(attrs) {
+      let _this = this;
+      var pattern = /(\[.*?\]){3}/;
+      var formula = attrs.formula;
+      var reg = new RegExp(pattern);
+      while (true) {
+        var items = formula.match(reg);
+        if (items == null) break;
+        let itemName = items[0];
+        let data = await _this.getformulaData(itemName);
+        data = await _this.getRemote(data);
+        formula = formula.replace(itemName, "(" + parseFloat(data) + ")");
+      }
+      return formula;
+    },
+    async getformulaData(formulaItem) {
+      let _this = this;
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      let items = formulaItem.match(reg);
+      let calResult = 0;
+      if (items[1] == "T") {
+        //表示本地模板数据
+        calResult = await _this.getModuleData(items[2], items[3]);
+      }
+      return calResult;
+    },
+    async getRemote(formula) {
+      let _this = this;
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      while (true) {
+        let items = formula.match(reg);
+        if (items == null) break;
+        if (items[1] == "R") {
+          //表示获取远程数据
+          var dataIndex = items[4];
+          dataIndex = items[4].split(",");
+          let calResult = await _this.getRemoteData(
+            items[2],
+            items[3],
+            dataIndex[0],
+            dataIndex[1]
+          );
+          let itemName = items[0];
+          formula = formula.replace(
+            itemName,
+            "(" + parseFloat(calResult) + ")"
+          );
+        }
+      }
+      return formula;
+    },
+    async getRemote1(formula) {
+      let _this = this;
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      while (true) {
+        let items = formula.match(reg);
+        if (items == null) break;
+        if (items[1] == "R") {
+          //表示获取远程数据
+          var dataIndex = items[4];
+          dataIndex = items[4].split(",");
+          let calResult = await _this.getRemoteData(
+            items[2],
+            items[3],
+            dataIndex[0],
+            dataIndex[1]
+          );
+          let itemName = items[0];
+          formula = formula.replace(itemName, parseFloat(calResult));
+        }
+      }
+      return formula;
+    },
+
+    //获取远程数据
+    async getRemoteData(code, sheet, row, col) {
+      debugger;
+      let _this = this;
+      let result = 0;
+      let par = {
+        code: code,
+        sheetName: sheet,
+        row: row,
+        col: col,
+      };
+      let res = await findData(par);
+      if (res.status == 200) {
+        console.log("result:", res.data);
+        let dataResult = parseFloat(res.data.result);
+        if (!isNaN(dataResult)) {
+          return dataResult;
+        }
+      }
+      return 0;
+    },
+
+    //获取本地文档模块数据
+    async getModuleData(code, attrName) {
+      let _this = this;
+      // console.log(`尝试获取模块数据: code=${code}, attrName=${attrName}`);
+      // console.log("当前 coms:", _this.coms);
+
+      let item = _this.coms.filter((o) => o.name == code);
+      // console.log(`找到的模块:`, item);
+
+      if (item.length > 0) {
+        let attr = item[0].attrs.filter((o) => o.name == attrName);
+        // console.log(`找到的属性:`, attr);
+
+        if (attr.length > 0) {
+          // console.log(`返回的值:`, attr[0].content);
+          return parseFloat(attr[0].content);
+        }
+      }
+      // console.log("未找到匹配的模块或属性,返回 0");
+      return 0;
+    },
+  },
+};
+</script>
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 11 - 0
ui/src/views/system/document/com/components/Variable/dataInfo.scss

@@ -0,0 +1,11 @@
+ .sub-imgs{
+    padding:0px;
+    margin:0px;
+      li{
+        float:left;
+        margin-right:10px;
+        width:100px;
+        height:100px;
+        list-style: none;
+      }
+  }

+ 356 - 0
ui/src/views/system/document/com/components/Variable/dataInfo.vue

@@ -0,0 +1,356 @@
+<template>
+  <div class="data-info">
+    <el-card>
+      <el-form
+        :model="dataForm"
+        :rules="dataRule"
+        ref="dataFormRef"
+        label-width="120px"
+      >
+        <!--   <el-form-item label="编码:">
+          <el-input v-model="dataForm.code"></el-input>
+        </el-form-item> -->
+        <el-form-item label="名称 :">
+          <el-input v-model="dataForm.name"></el-input>
+        </el-form-item>
+        <el-form-item label="取值类型:">
+          <el-select
+            v-model="dataForm.value_type"
+            placeholder="请选择取值类型"
+            size="large"
+            style="width: 100%"
+            clearable
+          >
+            <el-option
+              v-for="item in valueTypeList"
+              :key="item.value"
+              :label="item.name"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="取值:" v-if="dataForm.value_type == 1">
+          <el-input v-model="dataForm.value"></el-input>
+        </el-form-item>
+        <el-form-item label="取值:" v-if="dataForm.value_type == 2">
+          <el-input v-model="dataForm.value"></el-input>
+        </el-form-item>
+        <el-form-item
+          label="取值:"
+          v-if="dataForm.value_type == 3"
+          prop="value"
+        >
+          <el-input v-model="dataForm.value"></el-input>
+        </el-form-item>
+        <el-form-item
+          label="取值:"
+          v-if="dataForm.value_type == 4"
+          prop="value"
+        >
+          <el-input v-model="dataForm.value">
+            <template slot="append">%</template></el-input
+          >
+        </el-form-item>
+        <el-form-item
+          label="取值:"
+          v-if="dataForm.value_type == 5"
+          prop="value"
+        >
+          <el-input v-model="dataForm.value"></el-input>
+        </el-form-item>
+        <el-form-item label="取值范围:" v-if="dataForm.value_type == 2">
+          <el-select
+            ref="categorySelect"
+            v-model="dataForm.value_item"
+            multiple
+            filterable
+            allow-create
+            default-first-option
+            placeholder="点击enter创建选择标签"
+            style="width: 100%"
+            popper-class="custom-select-dropdown"
+          >
+          </el-select>
+          <!-- <el-input type="textarea" v-model="dataForm.value_item"   @keydown.native.enter="createTag"></el-input> -->
+        </el-form-item>
+        <el-form-item label="介绍:">
+          <el-input type="textarea" v-model="dataForm.intro"></el-input>
+        </el-form-item>
+
+        <el-form-item label="状态:">
+          <el-select
+            v-model="dataForm.status"
+            placeholder="请选择状态"
+            size="large"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </el-card>
+    <div style="text-align: right; margin-top: 20px">
+      <el-button type="warning" @click="btnSave">确认保存</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import { createParams, updateParams, getParamsInfo } from "@/api/params";
+import { status } from "nprogress";
+export default {
+  emits: ["onClose"],
+  props: {
+    id: {
+      type: Number,
+      default: 0,
+    },
+    type: {
+      type: Number,
+      default: 1,
+    },
+  },
+  watch: {
+    type: {
+      handler(val) {
+        if (val == null && val <= 0) return;
+        this.dataForm.type = val;
+      },
+      immediate: true,
+      deep: true,
+    },
+    id: {
+      handler(v) {
+        console.log(v);
+        let _this = this;
+        if (v == null || v <= 0) return;
+        _this.getInfo(v);
+      },
+      immediate: true,
+      deep: true,
+    },
+    hideDropdown(newVal) {
+      const dropdown = document.querySelector(".custom-select-dropdown");
+      if (dropdown) {
+        dropdown.style.display = newVal ? "none" : "";
+      }
+    },
+  },
+  data() {
+    const validateBucketName = (rule, value, callback) => {
+      if (this.dataForm.value_type == 5) {
+        const regex = /^\d+$/;
+        if (!regex.test(value)) {
+          callback(new Error("请输入整数"));
+        } else {
+          callback();
+        }
+      } else if (this.dataForm.value_type == 4) {
+        const regex = /^(?:100|\d{1,2}(?:\.\d+)?)$/;
+        if (!regex.test(value)) {
+          callback(new Error("请输入有效的百分数(0-100%)"));
+        } else {
+          callback();
+        }
+      } else if (this.dataForm.value_type == 3) {
+        const regex = /^\d+(\.\d+)?$/;
+        if (!regex.test(value)) {
+          callback(new Error("请输入有效的小数"));
+        } else {
+          callback();
+        }
+      }
+    };
+    return {
+      activeName: "base",
+      currentCategory: [],
+      valueTypeList: [
+        {
+          value: 0,
+          name: "请选择取值类型",
+        },
+        {
+          value: 1,
+          name: "输入框",
+        },
+        {
+          value: 2,
+          name: "下拉选择",
+        },
+        {
+          value: 3,
+          name: "小数",
+        },
+        {
+          value: 4,
+          name: "百分比",
+        },
+        {
+          value: 5,
+          name: "整数",
+        },
+      ],
+      allCategories: [],
+      typeList: [
+        {
+          value: 1,
+          name: "变量",
+        },
+        {
+          value: 2,
+          name: "常量",
+        },
+      ],
+      dataForm: {
+        id: 0,
+        type: 1,
+        name: "",
+        value: "",
+        value_type: 0,
+        value_item: " ",
+        intro: "",
+        status: 5,
+      },
+      statusOptions: [
+        {
+          value: "",
+          label: "请选择状态",
+        },
+        {
+          value: 5,
+          label: "使用中",
+        },
+        {
+          value: 6,
+          label: "已停用",
+        },
+      ],
+      dataRule: {
+        value: [
+          { required: true, message: "请输入取值", trigger: "blur" },
+          { validator: validateBucketName, trigger: "blur" },
+        ],
+      },
+      tags: [],
+      shouldPreventDropdown: true,
+      hideDropdown: false,
+    };
+  },
+
+  mounted() {
+    let _this = this;
+  },
+  methods: {
+    handleSelectFocus(event) {
+      this.$refs.categorySelect.blur();
+    },
+    createTag() {
+      // Logic to create a tag
+      console.log(
+        "Enter key pressed, create a tag with:",
+        this.dataForm.value_item
+      );
+      // Example: Add the value to a tags array
+      if (this.dataForm.value_item.trim() !== "") {
+        this.tags.push(this.dataForm.value_item.trim());
+        this.dataForm.value_item = ""; // Clear the input
+      }
+      console.log(this.tags);
+    },
+    resetForm() {
+      // 重置所有表单字段
+      this.dataForm = {
+        id: 0,
+        type: this.type, // 保留传入的 type
+        name: "",
+        value: "",
+        value_type: 0,
+        value_item: "",
+        intro: "",
+        status: 5,
+      };
+      // 如果使用 el-form,可以调用其 resetFields 方法
+      this.$nextTick(() => {
+        if (this.$refs.dataFormRef) {
+          this.$refs.dataFormRef.resetFields();
+        }
+      });
+    },
+    //保存更新
+    btnSave(e) {
+      let _this = this;
+      _this.$refs.dataFormRef.validate((valid) => {
+        if (valid) {
+          if (_this.dataForm.value_type == 1) {
+            _this.dataForm.value_item = _this.dataForm.value;
+          } else if (_this.dataForm.value_type == 4) {
+            // 将输入值转换为数字
+            const num = parseFloat(_this.dataForm.value);
+            // 格式化为百分数,保留两位小数
+            const percentage = num.toFixed(2) + "%";
+            _this.dataForm.value = percentage;
+          } else if (_this.dataForm.value_type == 2) {
+            _this.dataForm.value_item = _this.dataForm.value_item.join(",");
+          }
+          _this.dataForm.code = _this.dataForm.name;
+          if (_this.dataForm.id > 0) {
+            updateParams(_this.dataForm).then((res) => {
+              if (res.status != 200) return;
+              _this.resetForm(); // 添加这行
+              _this.$alert("信息更新成功");
+              _this.$emit("onClose");
+            });
+          } else {
+            createParams(_this.dataForm).then((res) => {
+              if (res.status != 200) return;
+              _this.resetForm(); // 添加这行
+              _this.$alert("信息创建成功");
+              _this.$emit("onClose");
+            });
+          }
+          _this.$emit("onClose");
+        } else {
+          console.log("错误");
+        }
+      });
+    },
+
+    //产品详情
+    getInfo(id) {
+      let _this = this;
+      let par = {
+        id: id,
+      };
+      getParamsInfo(par).then((res) => {
+        if (!res) return;
+        if (res.status != 200) return;
+        _this.dataForm = res.data;
+        if (_this.dataForm.value_type == 2 && _this.dataForm.value_item) {
+          _this.dataForm.value_item = _this.dataForm.value_item.split(',');
+        }
+      });
+    },
+
+    onChangeStatus(e) {
+      this.dataForm.status = e;
+    },
+    //修改分类信息
+    onChangeCategory(e) {
+      let _this = this;
+      _this.dataForm.categoryId = e;
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./dataInfo.scss";
+.custom-select-dropdown {
+  display: none; /* Initially hide the dropdown */
+}
+</style>

+ 0 - 0
ui/src/views/system/document/com/components/Variable/index.scss


+ 301 - 0
ui/src/views/system/document/com/components/Variable/index.vue

@@ -0,0 +1,301 @@
+<template>
+  <div class="com-formual">
+    <el-form
+      :inline="true"
+      :model="queryForm"
+      v-if="type == 1"
+      class="demo-form-inline"
+    >
+      <el-form-item label="类型:">
+        <el-select
+          v-model="queryForm.type"
+          placeholder="请选择类型"
+          size="large"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in typeList"
+            :key="item.value"
+            :label="item.name"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="变名称:">
+        <el-input
+          v-model="queryForm.name"
+          placeholder="请填写变量名称"
+        ></el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onBtnSearch">
+          <svg-icon icon-class="search" /> 搜索</el-button
+        >
+        <el-button type="primary" @click="onBtnCreate">
+          <svg-icon icon-class="plus" /> 新增变量</el-button
+        >
+      </el-form-item>
+    </el-form>
+    <el-form
+      :inline="true"
+      :model="queryForm"
+      v-if="type == 2"
+      class="demo-form-inline"
+    >
+      <!-- <el-form-item label="类型:">
+            <el-select v-model="queryForm.type"  placeholder="请选择类型" size="large" style="width:100%;">
+                <el-option
+                  v-for="item in typeList"
+                  :key="item.value"
+                  :label="item.name"
+                  :value="item.value"
+                />
+              </el-select>
+          </el-form-item>
+      <el-form-item label="变名称:">
+        <el-input
+          v-model="queryForm.name"
+          placeholder="请填写变量名称"
+        ></el-input>
+      </el-form-item> -->
+      <el-form-item>
+        <!-- <el-button type="primary" @click="onBtnSearch">
+          <svg-icon icon-class="search" /> 搜索</el-button
+        > -->
+        <el-button type="primary" @click="onBtnConst">
+          <svg-icon icon-class="plus" /> 新增常量</el-button
+        >
+      </el-form-item>
+    </el-form>
+    <el-table
+      :data="dataList"
+      style="width: 100%"
+      header-row-class-name="headerBg"
+      empty-text="没有数据信息"
+    >
+      <el-table-column prop="id" label="ID" align="center" />
+      <el-table-column prop="code" label="编码" align="left" />
+      <el-table-column prop="name" label="名称" align="left" />
+      <el-table-column
+        prop="value"
+        label="取值"
+        v-if="type == 1"
+        show-overflow-tooltip
+        align="left"
+        min-width="150"
+      />
+      <!-- <el-table-column prop="intro" label="介绍" show-overflow-tooltip	 align="left" min-width="150"/> -->
+      <el-table-column label="操作" align="center" width="250">
+        <template #default="scope">
+          <div class="btns">
+            <el-button type="primary" size="mini" @click="btnPicked(scope.row)"
+              ><svg-icon icon-class="confirm" />使用</el-button
+            >
+            <el-button
+              type="primary"
+              size="mini"
+              @click="btnOnEdit(scope.row.id, scope.row)"
+              ><svg-icon icon-class="edit" />编辑</el-button
+            >
+            <el-button
+              type="danger"
+              size="mini"
+              @click="btnOnDelete(scope.row.id)"
+              ><svg-icon icon-class="delete" />删除</el-button
+            >
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="page-info">
+      <el-pagination
+        :currentPage="queryForm.page"
+        :page-size="queryForm.pageSize"
+        :total="recordCount"
+        :page-count="pageTotal"
+        background
+        layout="prev, pager, next"
+        @current-change="ChangePage"
+      >
+      </el-pagination>
+    </div>
+
+    <el-dialog
+      :visible.sync="dialogVisible"
+      append-to-body
+      v-el-drag-dialog
+      width="300"
+      :close-on-click-modal="false"
+      custom-class="prod-verify"
+      :title="title"
+      @closed="handleDialogClosed"
+    >
+      <dataInfo
+        ref="dataInfoRef"
+        :id="dataId"
+        :type="type"
+        @onClose="onClose"
+      ></dataInfo>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { searchParams, deleteParams } from "@/api/params";
+import elDragDialog from "@/directive/el-drag-dialog";
+import dataInfo from "./dataInfo";
+
+export default {
+  name: "searchParams",
+  emits: ["onPicked", "onData"],
+  directives: { elDragDialog },
+  components: {
+    dataInfo,
+  },
+  props: {
+    type: {
+      type: Number,
+      default: 1,
+    },
+  },
+  watch: {
+    type: {
+      handler(val) {
+        console.log(val);
+        this.queryForm.type = val;
+        this.onBtnSearch();
+      },
+      immediate: true,
+      deep: true,
+    },
+    dataList: {
+      handler(newVal, oldVal) {
+        newVal.forEach((item, index) => {
+          if (JSON.stringify(item) !== JSON.stringify(oldVal[index])) {
+            this.$emit("onData", item);
+          }
+        });
+        // Add any additional logic you need here
+      },
+      deep: true,
+    },
+    /* onData */
+  },
+  data() {
+    return {
+      dataId: 0,
+      dialogVisible: false,
+      title: "创建变量",
+      queryForm: {
+        page: 1,
+        pageSize: 10,
+        name: "",
+        status: 5,
+        type: 1,
+      },
+      typeList: [
+        {
+          value: 1,
+          name: "变量",
+        },
+        {
+          value: 2,
+          name: "常量",
+        },
+      ],
+      recordCount: 0,
+      pageTotal: 1,
+      dataList: [],
+    };
+  },
+  mounted() {
+    // this.onBtnSearch();
+  },
+  methods: {
+    handleDialogClosed() {
+      if (this.$refs.dataInfoRef) {
+        this.$refs.dataInfoRef.resetForm();
+      }
+      this.dataId = 0; // Reset dataId when dialog is closed
+    },
+
+    onClose(e) {
+      this.dialogVisible = false;
+      this.onBtnSearch();
+      this.handleDialogClosed();
+    },
+
+    btnOnEdit(id, row) {
+      this.title = row.type == 1 ? "修改变量" : "修改常量";
+      this.$nextTick(() => {
+        this.dataId = id;
+        this.dialogVisible = true;
+      });
+    },
+
+    btnOnDelete(e) {
+      let _this = this;
+      let par = {
+        id: e,
+      };
+      _this
+        .$confirm("您是否确认删除该记录?", "提示", {
+          confirmButtonText: "确认",
+          cancelButtonText: "取消",
+          type: "warning",
+        })
+        .then((res) => {
+          deleteParams(par).then((res) => {
+            _this.search();
+          });
+        })
+        .catch(() => {});
+    },
+
+    onBtnCreate(e) {
+      let _this = this;
+      _this.dataId = 0;
+      _this.title = "创建变量";
+      _this.dialogVisible = true;
+    },
+
+    onBtnSearch(e) {
+      let _this = this;
+      _this.queryForm.page = 1;
+      _this.search();
+    },
+    btnPicked(e) {
+      this.$emit("onPicked", e);
+    },
+    //搜索
+    search() {
+      let _this = this;
+      searchParams(_this.queryForm).then((res) => {
+        if (!res) return;
+        _this.dataList = res.data.dataList;
+        _this.recordCount = res.data.totalRecord;
+        _this.pageTotal = res.data.pageTotal;
+      });
+    },
+    //修改分页
+    ChangePage(e) {
+      let _this = this;
+      _this.queryForm.page = e;
+      _this.search();
+    },
+    /* 新增常量 */
+    onBtnConst() {
+      let _this = this;
+      _this.dataId = 0;
+      _this.type = 2;
+      _this.title = "创建常量";
+      _this.dialogVisible = true;
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./index.scss";
+</style>

+ 0 - 0
ui/src/views/system/document/com/components/sourceAi/index.scss


+ 147 - 0
ui/src/views/system/document/com/components/sourceAi/index.vue

@@ -0,0 +1,147 @@
+<template>
+  <div class="com-formual">
+    <el-form>
+      <el-form-item>
+        <el-select
+          v-model="tableCode"
+          placeholder="请选择模型"
+          @change="onChangeTab"
+          size="large"
+          style="width: 100%"
+        >
+          <el-option label="测试模型" :value="0"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-input v-model="dataIntro" v-if="showSearch" @keyup.enter="retrieval"
+          >
+          <el-button
+            slot="append"
+            icon="el-icon-search"
+            @click="retrieval"
+          ></el-button
+        ></el-input>
+      </el-form-item>
+      <el-form-item
+        style="min-height: 200px"
+        v-if="fileLoadStatus"
+        v-loading="loading"
+      >
+        <div>当前回答内容:{{ textInfo }}</div>
+        <div
+          class="dialog-footer"
+          style="display: flex; justify-content: flex-end"
+        >
+          <el-button type="primary" @click="onConfirm"> 确认选择 </el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+  
+  <script>
+import { searchSourceData, deleteSourceData } from "@/api/sourceData";
+import axios from "axios";
+export default {
+  name: "SourceAi",
+  emits: ["onPicked"],
+
+  components: {},
+  data() {
+    return {
+      tableCode: "",
+      tableList: [],
+      showConfirm: false,
+      fileLoadStatus: false,
+      showSearch: false,
+      position: {
+        tb: "",
+        c: 0,
+        r: 0,
+        sheet: "",
+        value: "",
+      },
+      dataIntro: "",
+      textInfo: "",
+      loading: false, // 增加 loading 状态
+      searchResult: {},
+    };
+  },
+  mounted() {
+    this.initTableList();
+  },
+  methods: {
+    /* ai检索 */
+    async retrieval() {
+      this.fileLoadStatus = true;
+      this.loading = true; // 开始加载动画
+      try {
+        const response = await axios.post(
+          "http://58.246.234.210:7860/api/v1/run/c7ea3a2d-f50c-4936-b96c-3fe7c34fa062?stream=false",
+          {
+            input_value: this.dataIntro,
+            output_type: "chat",
+            input_type: "chat",
+            tweaks: {
+              "ChatInput-MbQRf": {},
+              "ParseData-UiiZo": {},
+              "Prompt-L8PiG": {},
+              "ChatOutput-MdiWR": {},
+              "SplitText-JIbWW": {},
+              "File-1i88L": {},
+              "OllamaEmbeddings-4TSqH": {},
+              "Milvus-Wotjl": {},
+              "OllamaModel-j2Vj0": {},
+              "Milvus-fivEN": {},
+              "OllamaEmbeddings-e409J": {},
+            },
+          },
+          {
+            headers: {
+              "Content-Type": "application/json",
+            },
+          }
+        );
+
+        if (response.status === 200) {
+          // Assuming the response structure is the same
+          this.textInfo =
+            response.data.outputs[0].outputs[0].results.message.data.text;
+          this.searchResult = {
+            searchTerm: this.dataIntro,
+            result: this.textInfo,
+          };
+        }
+      } catch (error) {
+        console.error("Error during retrieval:", error);
+        this.$message.error("检索失败,请稍后重试");
+      } finally {
+        this.loading = false; // 结束加载动画
+      }
+    },
+
+    onConfirm(e) {
+      /*       this.showConfirm = false; */
+      this.$emit("onPicked", this.searchResult);
+    },
+
+    onChangeTab(e) {
+      let _this = this;
+      /*      let item = _this.tableList[e]; */
+      _this.showSearch = true;
+    },
+    initTableList() {
+      let _this = this;
+      searchSourceData({ page: 1, pageSize: 999, status: 5 }).then((res) => {
+        if (res.status != 200) return;
+        _this.tableList = res.data.dataList;
+      });
+    },
+  },
+};
+</script>
+  
+  <style lang="scss">
+@import "./index.scss";
+</style>
+  

+ 171 - 0
ui/src/views/system/document/com/components/sourceEs/index.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="com-formual">
+    <el-button @click="searchEx">检索</el-button>
+
+    <!-- 显示匹配到的结果 -->
+    <div v-if="matchResults.length" class="match-results">
+      <div
+        v-for="(item, index) in matchResults"
+        :key="index"
+        class="match-item"
+      >
+        <div class="match-content">
+          <span>原始值: {{ item }}</span>
+          <span>替换后: {{ item + "1" }}</span>
+        </div>
+        <div class="actions">
+          <el-button size="small" @click="replaceItem(item)">替换</el-button>
+        </div>
+      </div>
+      <el-button type="primary" @click="replaceAll">批量替换</el-button>
+    </div>
+  </div>
+</template>
+    
+    <script>
+import { searchSourceData, deleteSourceData } from "@/api/sourceData";
+/* import { search } from "core-js/fn/symbol"; */
+import LuckyExcel from "luckyexcel"; //引入LuckyExcel
+import axios from "axios";
+export default {
+  name: "SourceEs",
+  emits: ["onPicked"],
+  props: {
+    TemList: {
+      type: Array,
+      default: [],
+    },
+  },
+  components: {},
+  data() {
+    return {
+      tableCode: "",
+      tableList: [],
+      showConfirm: false,
+      fileLoadStatus: false,
+      showSearch: false,
+      position: {
+        tb: "",
+        c: 0,
+        r: 0,
+        sheet: "",
+        value: "",
+      },
+      dataIntro: "",
+      textInfo: "",
+      loading: false, // 增加 loading 状态
+      searchResult: {},
+      matchResults: [], // 存储匹配到的结果
+    };
+  },
+  mounted() {
+    this.initTableList();
+  },
+  methods: {
+    searchEx() {
+      let allMatches = [];
+
+      this.TemList.forEach((el) => {
+        const matches = el.content.match(
+          /\[[A-Z0-9]+-[A-Z0-9]+-[0-9]+-[0-9]+\]/g
+        );
+        if (matches) {
+          allMatches = [...allMatches, ...matches];
+        }
+      });
+
+      // 去重
+      this.matchResults = [...new Set(allMatches)];
+      console.log("匹配到的结果:", this.matchResults);
+    },
+
+    // 单个替换
+    replaceItem(item) {
+      this.$confirm(`确认将 ${item} 替换为 ${item}1?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+      })
+        .then(() => {
+          // 这里添加替换逻辑
+          this.$message.success("替换成功");
+        })
+        .catch(() => {
+          // 取消操作
+        });
+    },
+
+    // 批量替换
+    replaceAll() {
+      this.$confirm(
+        `确认批量替换 ${this.matchResults.length} 项内容?`,
+        "提示",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+        }
+      )
+        .then(() => {
+          // 这里添加批量替换逻辑
+          this.$message.success("批量替换成功");
+        })
+        .catch(() => {
+          // 取消操作
+        });
+    },
+    /* 检索当前模版的所有内容 */
+    /*  searchEx() {
+      console.log("TemList", this.TemList);
+      this.TemList.map((el) => {
+         const matches = el.content.match(/\[[A-Z0-9]+-[A-Z0-9]+-[0-9]+-[0-9]+\]/g);
+        console.log("matches",matches);
+        return matches || []; // 如果没有匹配到返回空数组
+        el.content = el.content.replace(
+          /\[[A-Z0-9]+-[A-Z0-9]+-[0-9]+-[0-9]+\]/g,
+          (match) => {
+            // match 是匹配到的原始文本,例如 [FT7-RP-00-012]
+            // 这里可以根据需求对 match 进行处理
+            return `${match}1`; // 例如:替换后的[FT7-RP-00-012]
+            // 或者返回其他处理结果
+          }
+        );
+        return el;
+      });
+    }, */
+    /* ai检索 */
+    initTableList() {
+      let _this = this;
+      searchSourceData({ page: 1, pageSize: 999, status: 5 }).then((res) => {
+        if (res.status != 200) return;
+        _this.tableList = res.data.dataList;
+      });
+    },
+  },
+};
+</script>
+    
+<style lang="scss" scoped>
+.match-results {
+  margin-top: 20px;
+  
+  .match-item {
+    padding: 10px;
+    border-bottom: 1px solid #eee;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    
+    .match-content {
+      flex: 1;
+      display: flex;
+      gap: 20px;
+    }
+    
+    .actions {
+      margin-left: 10px;
+    }
+  }
+}
+</style>
+    

+ 130 - 0
ui/src/views/system/document/com/editor.scss

@@ -0,0 +1,130 @@
+.editor{
+  width:990px;
+  min-height:100vh;
+  margin:auto;
+  display:flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: flex-start;
+  margin-left:0px;
+  .editor-empty{
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width:100%;
+  }
+
+  .layers{
+    margin-bottom: 10px;
+    width:92.5%;
+    // min-height:30px;
+    display:flex;
+    flex-direction: column;
+    justify-content: flex-start;
+    align-content: flex-start;
+    position: relative;
+    // min-height:50px;
+    .main{
+      width:100%;
+      height:100%;
+      display:flex;
+      flex-direction: column;
+      justify-content: flex-start;
+      align-content: flex-start;
+      border:1px dashed #CCC;
+
+      &:hover{
+          border:1px dashed red;
+      }
+
+      .empty-box{
+        border:1px dashed #CCC;
+        width:100%;
+        height:40px;
+      }
+    }
+    .label{
+       position: absolute;
+       top:50%;
+       right:-80px;
+       width:80px;
+       height:50px;
+       line-height: 50px;
+       transform: translateY(-50%);
+       color: #323233;
+       background-color: #fff;
+       text-align:center;
+       font-size:12px;
+       -webkit-box-shadow: 0 0 4px 0 rgba(10,42,97,.2);
+       box-shadow: 0 0 4px 0 rgba(10,42,97,.2);
+       overflow: hidden;
+       white-space: nowrap;
+       text-overflow: ellipsis;
+         .del{
+           width:100%;
+           position:absolute;
+           height:100%;
+           background:#000;
+           opacity: 0.8;
+           top:0px;
+           left:0px;
+           display:none;
+         }
+         &:hover{
+            .del{
+              display:block;
+            }
+         }
+
+
+
+    }
+    .controller{
+      position:absolute;
+      top:0px;
+      right:-50px;
+      width:50px;
+      height:100px;
+      font-size:12px;
+      border:1px dashed #CCC;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+      .attr{
+        height:50px;
+        width:100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        border-bottom:1px solid #FFF;
+        .attr-icon-size{
+          width:28px;
+          height:28px;
+        }
+      }
+      .del{
+        height:50px;
+        width:100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        .attr-icon-size{
+          width:28px;
+          height:28px;
+        }
+      }
+    }
+  }
+
+  .active-layer{
+    box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
+    border: 1px solid black;
+  }
+
+}
+.btn-save{
+  display: flex;
+  justify-content: flex-end;
+  margin-right: 10px;
+}

+ 747 - 0
ui/src/views/system/document/com/editor.vue

@@ -0,0 +1,747 @@
+<template>
+  <div class="editor">
+    <draggable
+      v-model="comList"
+      @add="onAdd"
+      @update="onDragEnd"
+      @end="onDragComplete"
+      group="itxst"
+      animation="300"
+      :style="draggableStyle"
+      handle=".drag-handle"
+      :scroll="true"
+      :scrollSensitivity="100"
+      :scrollSpeed="20"
+    >
+      <transition-group style="display: block; min-height: 100vh">
+        <template v-for="(it, index) in comList">
+          <div
+            class="layers"
+            :key="index"
+            :class="comIndex == index ? 'active-layer' : ''"
+          >
+            <div class="drag-handle" draggable>
+              <svg-icon icon-class="drag" />
+            </div>
+            <!-- :options="categoryList":props="props" -->
+            <div style="padding: 10px 0">
+              <el-form label-position="left" class="inline-form">
+                <el-row :gutter="24" style="margin-left: 0; margin-right: 0">
+                  <el-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                    <el-form-item label="模块分类:">
+                      <!-- 此处需要处理为只有admin账号可编辑 -->
+                      <!--  <el-cascader
+                        v-if="showCategorySelect(it)"
+                        v-model="it.category_id"
+                        clearable
+                        :options="categoryList"
+                        :props="props"
+                        :show-all-levels="false"
+                        @change="onChangeCategory(it)"
+                        placeholder="请选择模块分类"
+                      />
+                     {{ $store.state.user }} -->
+                      <!-- <el-select
+                        v-if="showCategorySelect(it)"
+                        v-model="it.category_id"
+                        placeholder="请选择模块分类"
+                        size="large"
+                        style="width: 80%"
+                      >
+                        <el-option
+                          v-for="item in categoryList"
+                          :key="item.id"
+                          :label="item.name"
+                          :value="item.id"
+                        />
+                      </el-select>  v-else-->
+
+                      <span @click="enableSelect(it)" style="cursor: pointer">{{
+                        getCategory(it.category_id)
+                      }}</span>
+                    </el-form-item>
+                  </el-col>
+                  <el-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                    <el-form-item label="模块名称:">
+                      <!--  <el-input
+                        v-if="showCategoryName(it)"
+                        class="input-item"
+                        v-model="it.name"
+                        placeholder="请填写模块名称"
+                      />v-else -->
+                      <span @click="enableName(it)" style="cursor: pointer">{{
+                        it.name
+                      }}</span>
+                    </el-form-item>
+                  </el-col>
+                  <!-- <el-col :span="6">
+                    <el-form-item label="模块描述:">
+                      <el-input
+                        v-if="$store.state.user.roleInfo.id == 1"
+                        class="input-item"
+                        v-model="it.intro"
+                        placeholder="请填写模块介绍"
+                      />
+                      <span v-else>{{ it.intro }}</span>
+                    </el-form-item>
+                  </el-col> -->
+                  <el-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                    <el-form-item v-if="$store.state.user.roleInfo.id == 1">
+                      <div class="btn-save">
+                        <el-tooltip
+                          v-if="type !== 'document'"
+                          class="item"
+                          effect="dark"
+                          content="保存模块"
+                          placement="top"
+                        >
+                          <el-button
+                            size="mini"
+                            @click="onSaveTemplate(it)"
+                            icon="el-icon-document-add"
+                            circle
+                          >
+                          </el-button>
+                        </el-tooltip>
+                        <!--  <el-tooltip
+                          class="item"
+                          effect="dark"
+                          content="另存为"
+                          placement="top"
+                        >
+                          <el-button
+                            circle
+                            size="mini"
+                            @click="onSaveAs(it)"
+                            v-if="it.id > 0"
+                            icon="el-icon-document-copy"
+                          >
+                          </el-button>
+                        </el-tooltip> -->
+                        <el-button
+                          circle
+                          size="mini"
+                          v-if="it.isEdit !== 1 && type !== 'document'"
+                          @click="onEdit(index, 1)"
+                        >
+                          <svg-icon icon-class="edit" />
+                        </el-button>
+                        <el-button
+                          circle
+                          size="mini"
+                          @click="onEdit(index, 2)"
+                          icon="el-icon-view"
+                          v-else
+                        >
+                        </el-button>
+                        <el-tooltip
+                          v-if="!templateId && type !== 'document'"
+                          class="item"
+                          effect="dark"
+                          content="删除"
+                          placement="top"
+                        >
+                          <el-button
+                            circle
+                            size="mini"
+                            @click="onRemove(index)"
+                          >
+                            <svg-icon icon-class="delete"
+                          /></el-button>
+                        </el-tooltip>
+                      </div>
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+              </el-form>
+            </div>
+            <div class="main" @click="onSetActive(index)">
+              <component
+                :is="it.type"
+                :com="it"
+                :coms="comList"
+                :currentIndex="index"
+                :isAdmin="isAdmin"
+                :insertCmd="insertCmd"
+                @onUpdate="onUpdate"
+                @onUpdateAttr="onUpdateAttr"
+                @onUpdata="onUpdata"
+                @onUpdateProdAttr="onUpdateProdAttr"
+                @updateComContent="handleComContentUpdate"
+                :isEdit="it.isEdit"
+              />
+            </div>
+            <!--  <div class="label">
+              {{ it.name }}
+              <div class="del">
+                <svg-icon
+                  @click="onEdit(index, 1)"
+                  v-if="it.isEdit !== 1"
+                  icon-class="edit"
+                  style="color: #fff; margin-right: 10px"
+                />
+                <svg-icon
+                  @click="onEdit(index, 2)"
+                  v-else
+                  icon-class="view"
+                  style="color: #fff; margin-right: 10px"
+                />
+                <svg-icon
+                  @click="onRemove(index)"
+                  icon-class="delete"
+                  style="color: #fff; margin-left: 10px"
+                />
+              </div>
+            </div> -->
+          </div>
+        </template>
+        <template v-if="comList.length <= 0">
+          <div class="editor-empty" :key="-1">
+            <el-empty description="请拖放组件至当前页面" />
+          </div>
+        </template>
+      </transition-group>
+    </draggable>
+  </div>
+</template>
+
+<script>
+import draggable from "vuedraggable";
+import Table from "./components/Table";
+import TextArea from "./components/TextArea";
+import Empty from "./components/Empty";
+import {
+  getAllList,
+  createTemplate,
+  updateTemplate,
+  treeCategory,
+} from "@/api/template";
+export default {
+  name: "editor",
+  emits: [
+    "onSetActiveIndex",
+    "onDelete",
+    "onInsert",
+    "onLoadArticle",
+    "onRebuild",
+    "onSetComs",
+  ],
+  components: {
+    Table,
+    TextArea,
+    draggable,
+    Empty,
+  },
+  props: {
+    coms: {
+      type: Array,
+      default: null,
+    },
+    comIndex: {
+      type: Number,
+      default: 0,
+    },
+    templateCate: {
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+    insertCmd: {
+      type: Object,
+      default: null,
+    },
+    isAdmin: {
+      type: Number,
+      default: 2,
+    },
+  },
+  watch: {
+    coms: {
+      handler(val) {
+        if (val == null) return;
+        /* const newComList = JSON.parse(JSON.stringify(val));
+
+        // 比较新旧值,只有在真正变化时才更新
+        if (JSON.stringify(this.comList) !== JSON.stringify(newComList)) {
+          this.comList = newComList;
+        } */
+         console.log("val", val);
+        this.comList = JSON.parse(JSON.stringify(val));
+        console.log("comList", this.comList);
+        /*   console.log("val", val);
+        this.comList = this.carefulCopy(val);
+        console.log("comList", this.comList); */
+      },
+      immediate: true, //立即执行
+      deep: true,
+    },
+    comIndex: {
+      handler(val) {
+        if (val <= 0) return;
+        this.comList = this.comList.map((it) => {
+          it.isEdit = 2;
+          it.selDisabled = true;
+          it.valDisabled = true;
+          if (
+            typeof it.category_id === "string" &&
+            it.category_id.includes(" ")
+          ) {
+            it.category_id = it.category_id.split(" ");
+          } else if (!Array.isArray(it.category_id)) {
+            // 如果不是数组,将其转换为包含单个元素的数组
+            it.category_id = [it.category_id].filter(Boolean);
+          }
+          return it;
+        });
+      },
+      immediate: true, //立即执行
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      showEditor: false,
+      operation: "",
+      id: 0,
+      comList: [],
+      categoryList: [],
+      props: {
+        value: "id",
+        label: "name",
+        children: "children",
+        checkStrictly: true,
+      },
+      saveAs: false,
+      draggableStyle: {
+        width: "100%",
+
+        overflowY: "auto",
+      },
+      type: "",
+    };
+  },
+  mounted() {
+    console.log(this.$store.state);
+    this.initCategoryList();
+    this.type = this.$route.query.type;
+    this.templateId = this.$route.query.templateId;
+    this.$nextTick(() => {
+      this.updateDirectoryNumbers(); // 初始化目录序号
+    });
+  },
+  methods: {
+    /* 拖拽 修改目录序号*/
+    onDragComplete() {
+      console.log("拖动结束");
+      this.$nextTick(() => {
+        this.updateDirectoryNumbers();
+        this.$emit("onRebuild", this.comList);
+      });
+    },
+
+    updateDirectoryNumbers() {
+      let levelCounters = {};
+      this.comList.forEach((module, moduleIndex) => {
+        if (module.attrs) {
+          module.attrs.forEach((attr) => {
+            if (attr.type === "Directory") {
+              const newNumber = this.generateNumberedContent(
+                moduleIndex,
+                attr.level,
+                levelCounters
+              );
+
+              // 只更新 number 属性,完全不触及 content
+              if (!attr.hasOwnProperty("number")) {
+                this.$set(attr, "number", newNumber);
+              } else {
+                attr.number = newNumber;
+              }
+            }
+          });
+        }
+      });
+    },
+
+    generateNumberedContent(moduleIndex, level, levelCounters) {
+      return this.generateLevelPrefix(moduleIndex, level, levelCounters);
+    },
+
+    generateLevelPrefix(moduleIndex, level, levelCounters) {
+      let prefix = [];
+
+      for (let i = 1; i <= level; i++) {
+        if (i === 1) {
+          prefix.push(moduleIndex + 1);
+        } else {
+          if (!levelCounters[i]) {
+            levelCounters[i] = 1;
+          } else {
+            levelCounters[i]++;
+          }
+          prefix.push(levelCounters[i]);
+        }
+      }
+
+      return prefix.join(".");
+    },
+
+    carefulCopy(items) {
+      return items.map((item) => {
+        const newItem = { ...item };
+
+        // 深拷贝 attrs 数组
+        if (Array.isArray(newItem.attrs)) {
+          newItem.attrs = newItem.attrs.map((attr) => ({ ...attr }));
+        }
+
+        // 深拷贝 content,如果它是一个对象或数组
+        if (typeof newItem.content === "object" && newItem.content !== null) {
+          newItem.content = JSON.parse(JSON.stringify(newItem.content));
+        }
+
+        // 处理特殊的 type,如 Table
+        if (newItem.type === "Table") {
+          newItem.tableHeader = [...newItem.tableHeader];
+          newItem.tableData = newItem.tableData.map((row) => ({ ...row }));
+        }
+
+        // 保留其他属性的引用
+        return newItem;
+      });
+    },
+    handleComContentUpdate(index, updatedCom) {
+      // 更新本地的 coms 数组
+      this.$set(this.coms, index, updatedCom);
+
+      // 触发父组件的更新
+      this.$emit("onRebuild", this.coms);
+    },
+    showCategoryName(item) {
+      return (
+        this.type == "module" &&
+        this.$store.state.user.roleInfo.id == 1 &&
+        !item.valDisabled
+      );
+    },
+    enableName(item) {
+      item.valDisabled = false;
+      this.$forceUpdate();
+    },
+    showCategorySelect(item) {
+      return (
+        this.type === "module" &&
+        this.$store.state.user.roleInfo.id === 1 &&
+        !item.selDisabled
+      );
+    },
+    /* 选择分类 */
+    enableSelect(item) {
+      item.selDisabled = true;
+      console.log(item);
+      this.$forceUpdate();
+    },
+    /* 获取分类名称 */
+    getCategory(item) {
+      const findCategory = (list, id) => {
+        for (const el of list) {
+          if (el.id === id) {
+            return el.name;
+          }
+          if (el.children) {
+            const name = findCategory(el.children, id);
+            if (name) {
+              return name;
+            }
+          }
+        }
+        return null;
+      };
+
+      return findCategory(this.categoryList, item) || "请选择分类";
+      /* return (
+        this.categoryList.find((el) => el.id === item)?.name || "请选择分类"
+      ); */
+    },
+    onSaveAs(e) {
+      this.saveAs = true;
+      this.onSaveTemplate(e);
+    },
+    /* 保存模块 */
+    onSaveTemplate(e) {
+      let _this = this;
+      /* e.category_id='1' */
+      let data = JSON.parse(JSON.stringify(e));
+      console.log(data);
+      if (data.category) {
+        delete data.category;
+      }
+      data.attrs = JSON.stringify(data.attrs);
+      data.code = data.name;
+      data.status = 5;
+      // 检查 category_id 是否为数组
+      if (Array.isArray(e.category_id)) {
+        // 如果是数组,取最后一个元素作为 category_id
+        data.category_id = e.category_id[e.category_id.length - 1];
+      } else {
+        // 如果不是数组,直接赋值
+        data.category_id = e.category_id;
+      }
+
+      if (data.id == undefined || this.saveAs) {
+        createTemplate(data).then((res) => {
+          if (res.status != 200) return;
+          data.id = res.data;
+          e.id = res.data;
+
+          _this.$alert("模板信息保存成功");
+          _this.$emit("onRefresh");
+          this.saveAs = false;
+          data.selDisabled = false;
+          data.valDisabled = false;
+          this.$forceUpdate();
+        });
+      } else {
+        updateTemplate(data).then((res) => {
+          if (res.status != 200) return;
+          _this.$alert("模板信息更新成功");
+          _this.$emit("onRefresh");
+          data.selDisabled = false;
+          data.valDisabled = false;
+          this.$forceUpdate();
+        });
+      }
+    },
+    /* 获取模块分类 */
+    //获取模板分类信息
+    async initCategoryList() {
+      let _this = this;
+      let res = await treeCategory(); //await getAllList();
+      console.log(res);
+      this.categoryList =
+        res.data; /* //this.processDataForCascader(res.data); */
+    },
+    processDataForCascader(data) {
+      console.log("Raw data:", data); // 调试原始数据
+      const idMap = new Map();
+      const rootItems = [];
+
+      // First pass: create a map of all items
+      data.forEach((item) => {
+        const processedItem = { ...item, children: [] };
+        idMap.set(item.id, processedItem);
+      });
+
+      // Second pass: build the tree structure
+      data.forEach((item) => {
+        if (item.parent_id === "0" || item.parent_id === 0) {
+          rootItems.push(idMap.get(item.id));
+        } else {
+          const parent = idMap.get(parseInt(item.parent_id));
+          if (parent) {
+            parent.children.push(idMap.get(item.id));
+          } else {
+            console.warn(
+              `Parent with id ${item.parent_id} not found for item:`,
+              item
+            );
+            rootItems.push(idMap.get(item.id)); // 如果找不到父项,作为根项添加
+          }
+        }
+      });
+
+      console.log("Processed data:", rootItems); // 调试处理后的数据
+      return rootItems;
+    },
+    onChangeCategory(item, value) {
+      const selectedId = value[value.length - 1];
+      item.category_id = selectedId;
+      // You can now use the selected ID as needed
+    },
+    //更新产品属性
+    onUpdateProdAttr(comIndex, attrIndex, data) {
+      this.comList[comIndex].attrs[attrIndex].content = data;
+
+      let attrName = this.comList[comIndex].attrs[attrIndex].attrs.name;
+      let attrType = this.comList[comIndex].attrs[attrIndex].attrs.type;
+
+      for (var i = 0; i < this.comList.length; i++) {
+        for (var j = 0; j < this.comList[i].attrs.length; j++) {
+          if (this.comList[i].attrs[j].type == "ProductAttr") {
+            if (
+              this.comList[i].attrs[j].attrs.name == attrName &&
+              this.comList[i].attrs[j].attrs.type == attrType
+            ) {
+              this.comList[i].attrs[j].content = data;
+            }
+          }
+        }
+      }
+      this.$emit("onRebuild", this.comList);
+    },
+
+    onUpdateAttr(comIndex, attrIndex, data) {
+      this.comList[comIndex].attrs[attrIndex].content = data;
+
+      let dataType = this.comList[comIndex].attrs[attrIndex].type;
+      let dataId = this.comList[comIndex].attrs[attrIndex].data;
+
+      for (var i = 0; i < this.comList.length; i++) {
+        for (var j = 0; j < this.comList[i].attrs.length; j++) {
+          if (
+            this.comList[i].attrs[j].type == dataType &&
+            this.comList[i].attrs[j].id == dataId
+          ) {
+            this.comList[i].attrs[j].content = data;
+          }
+        }
+      }
+      this.$emit("onRebuild", this.comList);
+    },
+    /* 修改单个值 */
+    onUpdata(comIndex, attrIndex, attrId, data, type) {
+      let _this = this;
+      let com = _this.coms[comIndex];
+
+      if (type === "variableNull") {
+        // 只更新特定的 variableNull 属性
+        if (
+          com &&
+          com.attrs &&
+          com.attrs[attrIndex] &&
+          com.attrs[attrIndex].id === attrId
+        ) {
+          this.$set(com.attrs[attrIndex], "content", data);
+        }
+      } else {
+        // 原有的逻辑,更新所有相同类型的属性
+        let dataType = com.attrs[attrIndex].type;
+        for (var i = 0; i < _this.coms.length; i++) {
+          for (var j = 0; j < _this.coms[i].attrs.length; j++) {
+            if (
+              _this.coms[i].attrs[j].type == dataType &&
+              _this.coms[i].attrs[j].id == attrId
+            ) {
+              _this.coms[i].attrs[j].content = data;
+            }
+          }
+        }
+      }
+
+      _this.$emit("onRebuild", _this.coms);
+    },
+    onRemove(index) {
+      this.$emit("onDelete", index);
+    },
+    onEdit(e, state) {
+      this.comList = this.comList.map((it, index) => {
+        it.isEdit = 2;
+
+        // 当切换到编辑状态时,对当前项进行 attrs 过滤
+        if (index === e && state === 1) {
+          this.filterAttrs(it);
+        }
+
+        return it;
+      });
+
+      if (state == 1) {
+        this.comList[e].isEdit = 1;
+        this.onSetActive(e);
+      }
+
+      this.$emit("onRebuild", this.comList);
+    },
+
+    // 新增 filterAttrs 方法
+    filterAttrs(item) {
+      // 从 content 中提取所有 {{}} 包裹的 ID
+      const contentIds = (item.content.match(/{{([^}]+)}}/g) || []).map(
+        (match) => match.slice(2, -2).trim()
+      );
+
+      // 过滤 attrs,只保留在 content 中出现的 ID
+      item.attrs = item.attrs.filter((attr) => contentIds.includes(attr.id));
+    },
+    onAdd(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      this.$emit("onRebuild", this.comList);
+    },
+    onDragEnd(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      this.$emit("onRebuild", this.comList);
+    },
+    onUpdate(index, content) {
+      let _this = this;
+      this.coms[index].content = content;
+      // 触发父组件的更新
+      this.$emit("onRebuild", this.coms);
+    },
+    //激活当前层
+    onSetActive(e) {
+      this.$emit("onSetActiveIndex", e);
+    },
+  },
+};
+</script>
+<style lang="scss">
+@import "./editor.scss";
+.layers {
+  margin-left: 26px;
+  .drag-handle {
+    position: absolute;
+    top: 0;
+    left: -27px;
+    cursor: move;
+    padding: 0 5px;
+    display: flex;
+    align-items: center;
+    /*  background-color: #f0f0f0; */
+
+    &:hover {
+      /*  background-color: #e0e0e0; */
+    }
+  }
+
+  .content-wrapper {
+    flex-grow: 1;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .main {
+    flex-grow: 1;
+  }
+}
+.inline-form {
+  display: block;
+  width: 100%;
+  /* flex-wrap: wrap;
+  gap: 10px; */
+}
+
+.inline-form .el-form-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 0;
+  flex: 1;
+  min-width: 300px;
+}
+
+.inline-form .el-form-item__label {
+  flex-shrink: 0;
+  width: auto;
+  padding-right: 12px;
+}
+
+.inline-form .el-form-item__content {
+  flex-grow: 1;
+  margin-left: 0 !important;
+}
+.input-item {
+  width: 200px;
+}
+</style>

+ 71 - 0
ui/src/views/system/document/com/menus.scss

@@ -0,0 +1,71 @@
+.com-menus{
+  width:100%;
+  height:68px;
+  overflow: hidden;
+  overflow-x: auto;
+  background:#f3f3f3;
+  padding-top:5px;
+ &::-webkit-scrollbar {
+     width:5px;
+     height:5px;
+     background-color: #CCC;
+ }
+ &::-webkit-scrollbar-thumb{
+   background-color:#b9deff;
+ }
+  .menus-out{
+    width:100%;
+    height:100%;
+  }
+  .group-item{
+    display:flex;
+    flex-direction: row;
+    font-size:12px;
+    margin-right: 10px;
+    float:left;
+/*     background:#e2e2e2; */
+    margin-left:5px;
+    border-radius: 5px;
+    padding-top:5px;
+    padding-bottom:5px;
+    .items-name{
+      font-size:14px;
+      display: flex;
+      width:25px;
+      justify-content: center;
+      align-items: center;
+      margin-left:10px;
+      border-right:1px solid #CCC;
+      font-weight: bold;
+    }
+    .menus{
+      display: flex;
+      justify-content:center;
+      align-items: center;
+
+      .menu{
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content:center;
+        width:60px;
+        cursor: pointer;
+        .icon{
+          width:32px;
+          height:32px;
+          display: flex;
+          align-items: center;
+          justify-content:center;
+          .svg-style{
+            width:24px;
+            height:24px;
+          }
+        }
+      }
+    }
+  }
+  .group-item0{
+    /*  width:500px; */
+     overflow: hidden;
+  }
+}

+ 394 - 0
ui/src/views/system/document/com/menus.vue

@@ -0,0 +1,394 @@
+<template>
+  <div class="com-menus">
+    <div class="menus-out">
+      <div
+        class="group-item"
+        v-for="(it, index) in filteredMenus"
+        :key="index"
+        :class="'group-item' + index"
+      >
+        <div class="items-name">{{ it.name }}</div>
+        <div class="menus">
+          <template v-for="(item, itemIndex) in it.subMenus">
+            <div
+              :key="itemIndex"
+              class="menu"
+              @click="onClickMenus(item)"
+              :class="{
+                disabled:
+                  (typeMenu === 'document' && item.key !== 'ai') ||
+                  (item.key === 'article' && (typeMenu === 'module' || typeMenu === undefined)),
+              }"
+            >
+              <div class="icon">
+                <svg-icon
+                  v-if="item.icon != ''"
+                  className="svg-style"
+                  size="120"
+                  :icon-class="item.icon"
+                />
+              </div>
+              <div class="menu-name">{{ item.name }}</div>
+            </div>
+          </template>
+        </div>
+      </div>
+    </div>
+
+    <el-dialog
+      :visible.sync="showFormula"
+      append-to-body
+      v-el-drag-dialog
+      width="60%"
+      :close-on-click-modal="false"
+      custom-class="prod-verify"
+      top="20px"
+      title="选择公式"
+    >
+      <Formula @onPicked="onPickedFormula" :comList="comArr"></Formula>
+    </el-dialog>
+
+    <el-dialog
+      :visible.sync="showVariable"
+      append-to-body
+      v-el-drag-dialog
+      width="800px"
+      :close-on-click-modal="false"
+      custom-class="prod-verify"
+      top="20px"
+      :title="insertTitle"
+    >
+      <Variable
+        :type="type"
+        @onPicked="onPickedVariable"
+        @onData="btnbianl"
+      ></Variable>
+    </el-dialog>
+
+    <el-dialog
+      :visible.sync="showSourceData"
+      append-to-body
+      v-el-drag-dialog
+      width="300"
+      :close-on-click-modal="false"
+      top="20px"
+      custom-class="prod-verify"
+      title="选择源数据"
+    >
+      <sourceData @onPicked="onPickedTab"></sourceData>
+    </el-dialog>
+    <!-- 插入ai -->
+    <el-dialog
+      :visible.sync="showSourceAi"
+      append-to-body
+      v-el-drag-dialog
+      width="80%"
+      :close-on-click-modal="false"
+      top="20px"
+      custom-class="prod-verify"
+      title="AI"
+    >
+      <!-- <sourceAi @onPicked="onPickedAi"></sourceAi> -->
+     <!--  <ChatBox /> -->
+    </el-dialog>
+
+    <!-- ai -->
+    <el-dialog
+      :visible.sync="showSourceEs"
+      append-to-body
+      v-el-drag-dialog
+      width="300"
+      :close-on-click-modal="false"
+      custom-class="prod-verify"
+      title=""
+    >
+      <sourceEs @onPicked="onPickedAi" :TemList="comArr"></sourceEs>
+    </el-dialog>
+    <!-- 插入目录 -->
+    <el-dialog
+      :visible.sync="showDirectoryLevelDialog"
+      append-to-body
+      v-el-drag-dialog
+      width="300px"
+      :close-on-click-modal="false"
+      custom-class="prod-verify"
+      title="选择目录等级"
+    >
+      <el-form :model="directoryForm" label-width="100px">
+        <el-form-item label="目录等级">
+          <el-select v-model="directoryForm.level" placeholder="请选择目录等级">
+            <el-option label="一级目录" :value="1"></el-option>
+            <el-option label="二级目录" :value="2"></el-option>
+            <el-option label="三级目录" :value="3"></el-option>
+            <el-option label="四级目录" :value="4"></el-option>
+            <el-option label="五级目录" :value="5"></el-option>
+            <el-option label="六级目录" :value="6"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="showDirectoryLevelDialog = false">取 消</el-button>
+        <el-button type="primary" @click="confirmDirectoryLevel"
+          >确 定</el-button
+        >
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import Formula from "./components/Formula";
+import sourceData from "./components/SourceData";
+import sourceAi from "./components/sourceAi/index.vue";
+import sourceEs from "./components/sourceEs/index.vue";
+import Variable from "./components/Variable";
+import elDragDialog from "@/directive/el-drag-dialog";
+/* import ChatBox from "@/components/ai.vue"; */
+import TemList from "../temList/temList.vue";
+export default {
+  name: "menus",
+  emits: ["onEvents", "onVariable"],
+  components: {
+    Formula,
+    Variable,
+    sourceData,
+    sourceAi,
+    sourceEs,
+   /*  ChatBox, */
+  },
+  directives: { elDragDialog },
+  props: {
+    comArr: {
+      type: Array,
+      default: [],
+    },
+  },
+  data() {
+    return {
+      showDirectoryLevelDialog: false,
+      directoryForm: {
+        level: 1
+      },
+      showFormula: false,
+      showVariable: false,
+      showSourceData: false,
+      showSourceAi: false,
+      showSourceEs: false,
+      type: null,
+      typeMenu:null,
+      insertTitle: "插入变量",
+      menus: [
+        {
+          name: "插入",
+          subMenus: [
+            // {
+            //   key:"template",
+            //   name:"空白模板",
+            //   icon:"insertTemplate",
+            // },
+            {
+              key: "article",
+              name: "图文",
+              icon: "图文",
+              disabled: () =>this.typeMenu === "module" || this.typeMenu === undefined,
+            },
+
+            {
+                key:"SourceData",
+                name:"源数据",
+                icon:"sourceData",
+              },
+            {
+              key: "formual",
+              name: "公式",
+              icon: "公式",
+            },
+
+            {
+              key: "variable",
+              name: "变量",
+              icon: "变量",
+            },
+            {
+              key: "constant",
+              name: "常量",
+              icon: "常量",
+            },
+            {
+              key: "attr",
+              name: "属性",
+              icon: "属性",
+            },
+            {
+              key: "Directory",
+              name: "目录",
+              icon: "目录",
+            },
+            {
+              key: "InsertNull",
+              name: "插入空值",
+              icon: "插入空值",
+            },
+            /* {
+                key:"pager",
+                name:"分页",
+                icon:"insertPage",
+              }, */
+            {
+              key: "ai",
+              name: "AI模块",
+              icon: "AI",
+            },
+             {
+                key:"searchEs",
+                name:"检索",
+                icon:"form",
+              },
+          ],
+        },
+        // {
+        //   name:'设置',
+        //   subMenus:[
+
+        //     {
+        //       key:"headerAndTop",
+        //       name:"页眉页脚",
+        //       icon:"insertPager",
+        //     },
+
+        // {
+        //   key:"auth",
+        //   name:"权限",
+        //   icon:"insertAuth",
+        // },
+        // {
+        //   key:"fav",
+        //   name:"收藏",
+        //   icon:"insertFav",
+        // },
+        // {
+        //   key:"share",
+        //   name:"分享",
+        //   icon:"insertShare",
+        // },
+        //   ]
+        // }
+      ],
+    };
+  },
+  computed: {
+    filteredMenus() {
+      /* if (this.type === "document") { */
+      this.menus
+        .map((group) => ({
+          ...group,
+          subMenus: group.subMenus.filter((item) => item.key === "ai"),
+        }))
+        .filter((group) => group.subMenus.length > 0);
+      /* } */
+      return this.menus;
+    },
+  },
+  mounted() {
+    this.typeMenu = this.$route.query.type;
+  },
+  methods: {
+    /* 变量变动 */
+    btnbianl(e) {
+      this.$emit("onVariable", e);
+    },
+
+    /* 插入ai */
+    onPickedAi(e) {
+      let _this = this;
+      let item = {
+        type: "insert",
+        key: "ai",
+        content: e,
+      };
+      this.$emit("onEvents", item);
+      this.showSourceAi = false;
+    },
+    //选取公式
+
+    onPickedTab(e) {
+      let _this = this;
+      let item = {
+        type: "insert",
+        key: "sourceData",
+        content: e,
+      };
+      this.$emit("onEvents", item);
+      this.showSourceData = false;
+    },
+    onPickedFormula(e) {
+      let _this = this;
+      let item = {
+        type: "insert",
+        key: "formual",
+        content: e,
+      };
+      this.$emit("onEvents", item);
+      console.log(item);
+      this.showFormula = false;
+    },
+    onPickedVariable(e) {
+      let _this = this;
+      let item = {
+        type: "insert",
+        key: e.type == 2 ? "constant" : "variable",
+        content: e,
+      };
+      this.$emit("onEvents", item);
+      this.showVariable = false;
+    },
+    onClickMenus(e) {
+      if (
+        (this.typeMenu === "document" && e.key !== "ai") ||
+        (e.key === "article" && (this.typeMenu === "module" || this.typeMenu === undefined))
+      ) {
+        return; // Exit the method if the item is disabled
+      }
+      /* e.type = "insert"; */
+      if (e.key == "formual") {
+        this.showFormula = true;
+      } else if (e.key == "SourceData") {
+        this.showSourceData = true;
+      } else if (e.key == "variable" || e.key == "constant") {
+        if (e.key == "variable") {
+          this.type = 1;
+          this.insertTitle = "插入变量";
+        } else {
+          this.type = 2;
+          this.insertTitle = "插入常量";
+        }
+        this.showVariable = true;
+      } else if (e.key == "ai") {
+        this.showSourceAi = true;
+      } else if (e.key == "searchEs") {//aiEs
+        this.showSourceEs = true;
+      } else  if (e.key === "Directory") {
+        this.showDirectoryLevelDialog = true;
+      } else {
+        this.$emit("onEvents", e);
+      }
+    },
+    confirmDirectoryLevel() {
+      this.showDirectoryLevelDialog = false;
+      this.$emit("onEvents", {
+        type: "insert",
+        key: "Directory",
+        content: { level: this.directoryForm.level }
+      });
+    }
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./menus.scss";
+.menu.disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+</style>

+ 63 - 0
ui/src/views/system/document/com/view.scss

@@ -0,0 +1,63 @@
+.com-viewer{
+
+  width:100%;
+  height:500px;
+  padding-bottom:50px;
+  .content{
+      width:800px;
+      margin:auto;
+      height:450px;
+      overflow-x: hidden;
+      overflow-y: auto;
+      border:2px solid #CCC;
+      border-radius: 10px;
+      padding:10px;
+      margin-bottom: 10px;
+      .template-textarea{
+        .rich-editor{
+          .directory-level-1 {
+            font-size: 24px;
+            font-weight: bold;
+            margin-left: 0;
+          }
+          
+          .directory-level-2 {
+            font-size: 20px;
+            font-weight: bold;
+            margin-left: 20px;
+          }
+          
+          .directory-level-3 {
+            font-size: 18px;
+            font-weight: bold;
+            margin-left: 40px;
+          }
+          
+          .directory-level-4 {
+            font-size: 16px;
+            font-weight: bold;
+            margin-left: 60px;
+          }
+          
+          .directory-level-5 {
+            font-size: 14px;
+            font-weight: bold;
+            margin-left: 80px;
+          }
+          
+          .directory-level-6 {
+            font-size: 12px;
+            font-weight: normal;
+            margin-left: 100px;
+          }
+        }
+      }
+
+     
+  }
+  .export{
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+  }
+}

+ 867 - 0
ui/src/views/system/document/com/view.vue

@@ -0,0 +1,867 @@
+<template>
+  <div class="com-viewer">
+    <!-- getComponentType() -->
+    <div class="content" ref="content">
+      <template v-for="(item, index) in coms">
+        <component
+          :key="index"
+          :is="item.type"
+          :com="item"
+          :coms="comList"
+          :currentIndex="index"
+          :isActive="false"
+          @onUpdateData="onUpdateData"
+        />
+      </template>
+    </div>
+    <div class="export">
+      <el-button
+        @click="onExport"
+        type="primary"
+        :loading="exporting"
+        :disabled="exporting"
+        >导出</el-button
+      >
+    </div>
+  </div>
+</template>
+
+<script>
+import { exportDocument } from "@/api/document";
+import TextArea from "./components/TextView";
+import htmlDocx from "html-docx-js/dist/html-docx";
+import jsPDF from "jspdf";
+import html2canvas from "html2canvas";
+import { findData } from "@/api/sourceData";
+import HTMLtoDOCX from "html-to-docx";
+import { saveAs } from "file-saver";
+
+export default {
+  name: "Viewer",
+  components: {
+    TextArea,
+  },
+  props: {
+    id: {
+      type: Number,
+      default: 0,
+    },
+    coms: {
+      type: Array,
+      default: null,
+    },
+    docAttr: {
+      type: Object,
+      default: () => ({
+        title: "",
+      }),
+    },
+  },
+  watch: {
+    coms: {
+      handler(val) {
+        if (val == null) return;
+        this.comList = JSON.parse(JSON.stringify(val));
+        this.replaceData(val);
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      comList: [],
+      content: "",
+      exporting: false, // 新增:用于跟踪导出状态
+      exportProgress: 0,
+    };
+  },
+  methods: {
+    async replaceData(data) {
+      for (let item of this.comList) {
+        for (let el of item.attrs) {
+          let attrId = el.id;
+          if (el.type == "formual") {
+            try {
+              let formual = await this.analysisFormual(el);
+
+              formual = await this.getRemote(formual);
+              el.content = eval(formual);
+            } catch (error) {}
+          } else if (el.type == "variableNull") {
+            console.log(el);
+          }
+        }
+      }
+      //this.$emit('update:coms', [...this.comList])
+      return data;
+    },
+    //分析公式
+    async analysisFormual(attrs) {
+      let _this = this;
+      // console.log("开始分析公式:", attrs.formula);
+      var pattern = /(\[.*?\]){3}/;
+      var formual = attrs.formula;
+      var reg = new RegExp(pattern);
+      while (true) {
+        var items = formual.match(reg);
+        if (items == null) break;
+        let itemName = items[0];
+        // console.log("处理项:", itemName);
+        try {
+          let data = await _this.getFormualData(itemName);
+          // console.log(`获取到的数据: ${itemName} = ${data}`);
+          if (data === null || data === undefined || isNaN(data)) {
+            console.warn(`获取到的数据无效: ${itemName}`);
+            formual = formual.replace(itemName, "(0)");
+          } else {
+            formual = formual.replace(itemName, `(${parseFloat(data)})`);
+          }
+        } catch (error) {
+          console.error(`处理 ${itemName} 时出错:`, error);
+          formual = formual.replace(itemName, "(0)");
+        }
+      }
+      return formual;
+    },
+    async getFormualData(formualItem) {
+      let _this = this;
+      // console.log("开始获取公式数据:", formualItem);
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      let items = formualItem.match(reg);
+      // console.log("解析结果:", items);
+      let calResult = 0;
+      if (items && items[1] == "T") {
+        calResult = await _this.getModuleData(items[2], items[3]);
+      } else {
+        console.warn("无法解析公式项:", formualItem);
+      }
+      // console.log("计算结果:", calResult);
+      return calResult;
+    },
+    //获取本地文档模块数据
+    async getModuleData(code, attrName) {
+      let _this = this;
+      // console.log(`尝试获取模块数据: code=${code}, attrName=${attrName}`);
+      // console.log("当前 coms:", _this.coms);
+
+      let item = _this.comList.filter((o) => o.name == code);
+      // console.log(`找到的模块:`, item);
+
+      if (item.length > 0) {
+        let attr = item[0].attrs.filter((o) => o.name == attrName);
+        // console.log(`找到的属性:`, attr);
+
+        if (attr.length > 0) {
+          // console.log(`返回的值:`, attr[0].content);
+          return parseFloat(attr[0].content);
+        }
+      }
+      // console.log("未找到匹配的模块或属性,返回 0");
+      return 0;
+    },
+
+    async getRemote(formual) {
+      let _this = this;
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      while (true) {
+        let items = formual.match(reg);
+        if (items == null) break;
+        if (items[1] == "R") {
+          //表示获取远程数据
+          var dataIndex = items[4];
+          dataIndex = items[4].split(",");
+          let calResult = await _this.getRemoteData(
+            items[2],
+            items[3],
+            dataIndex[0],
+            dataIndex[1]
+          );
+          let itemName = items[0];
+          // 检查calResult是否为数字
+          if (!isNaN(calResult)) {
+            formual = formual.replace(
+              itemName,
+              "(" + parseFloat(calResult) + ")"
+            );
+          } else {
+            formual = formual.replace(itemName, `"${calResult}"`);
+          }
+          formual = formual.replace(
+            itemName,
+            "(" + parseFloat(calResult) + ")"
+          );
+        }
+      }
+      return formual;
+    },
+    async getRemoteData(code, sheet, row, col) {
+      let _this = this;
+      let result = 0;
+      let par = {
+        code: code,
+        sheetName: sheet,
+        row: row,
+        col: col,
+      };
+
+      /*  if (!this.isEditing) {
+        this.loading = true;
+        this.progress = 0;
+
+        try {
+          const updateProgress = setInterval(() => {
+            if (this.progress < 90) {
+              this.progress += 10;
+            }
+          }, 100);
+
+          let res = await findData(par);
+          if (res.status == 200) {
+            result = res.data.result;
+          }
+
+          this.progress = 100;
+          clearInterval(updateProgress);
+        } catch (error) {
+          console.error("Error fetching remote data:", error);
+        } finally {
+          setTimeout(() => {
+            this.loading = false;
+            this.progress = 0;
+          }, 1000);
+        }
+      } else { */
+      // 如果正在编辑,直接获取数据而不显示进度条
+      try {
+        let res = await findData(par);
+        if (res.status == 200) {
+          result = res.data.result;
+          // 添加数据类型检查和转换
+          if (typeof result === "string" && !isNaN(result)) {
+            result = parseFloat(result);
+          }
+        }
+      } catch (error) {
+        console.error("获取远程数据错误:", error);
+      }
+      /*   } */
+
+      return result;
+    },
+    getComponentType(type) {
+      if (type === "textarea") {
+        return TextArea;
+      }
+      // Add other component types if needed
+      return type;
+    },
+    onUpdateData(index, data) {
+      this.comList[index].content = data;
+      this.rebuildContent();
+      //this.$emit('update:coms', [...this.comList])
+    },
+    rebuildContent() {
+      this.content = this.comList.map((item) => item.content).join("");
+    },
+    /* 后端赚word */
+    async onExport() {
+      if (this.exporting) return; // 如果正在导出,直接返回
+      this.exporting = true; // 设置导出状态为 true
+
+      try {
+        let contentClone = this.$refs.content.cloneNode(true);
+        /* const templateTextareas =
+          contentClone.querySelectorAll(".template-textarea");
+
+        if (templateTextareas.length > 0) {
+          // 创建目录页
+          const tocPage = document.createElement("div");
+          tocPage.className = "toc-page";
+          tocPage.innerHTML = `
+    <div style="page-break-before: always; page-break-after: always; height: 100vh; display: flex; flex-direction: column; justify-content: center;">
+      <h1 style="text-align: center; margin-bottom: 40px;">目录</h1>
+      <div id="toc" style="margin: 0 auto; min-width: 600px;"></div>
+    </div>
+  `;
+
+          // 在第一个 template-textarea 之后插入目录页
+          templateTextareas[0].parentNode.insertBefore(
+            tocPage,
+            templateTextareas[0].nextSibling
+          );
+
+          // 生成目录内容
+          let tocHtml = '<div style="column-count: 1; column-gap: 40px;">';
+          templateTextareas.forEach((textarea, index) => {
+            const title = textarea.querySelector("h1, h2, h3, h4, h5, h6");
+            if (title) {
+              tocHtml += `
+                <p style="margin: 0; padding: 5px 0;">
+                  <span style="float: left;">${title.textContent}</span>
+                  <span style="float: right;">${index + 1}</span>
+                  <span style="display: block; overflow: hidden; height: 1.2em; border-bottom: 1px dotted black;"></span>
+                </p>
+              `;
+              title.id = `section-${index + 1}`;
+            }
+          });
+          tocHtml += "</div>";
+
+          // 将目录内容插入到目录页中
+          contentClone.querySelector("#toc").innerHTML = tocHtml;
+        } */
+        const templateTextareas =
+          contentClone.querySelectorAll(".template-textarea");
+
+       /*  if (templateTextareas.length > 0) {
+          // 创建空白页和目录页
+          const blankPage = document.createElement("div");
+          blankPage.className = "blank-page";
+          blankPage.innerHTML = '<div style="page-break-after: always;"></div>';
+
+          const tocPage = document.createElement("div");
+          tocPage.className = "toc-page";
+          tocPage.innerHTML = `
+            <div class="toc-content">
+              <h1 style="text-align: center; margin-bottom: 40px;">目录</h1>
+              <div id="toc"></div>
+            </div>
+          `;
+
+          // 在第一个 template-textarea 之后插入空白页和目录页
+          templateTextareas[0].parentNode.insertBefore(
+            tocPage,
+            templateTextareas[0].nextSibling
+          );
+          templateTextareas[0].parentNode.insertBefore(blankPage, tocPage);
+
+          // 生成目录内容
+          let tocHtml = "";
+          let currentLevel = 0;
+          let pageNumber = 2; // 从第2页开始计数
+
+          templateTextareas.forEach((textarea) => {
+            const headings = textarea.querySelectorAll(
+              "h1, h2, h3, h4, h5, h6"
+            );
+            headings.forEach((heading) => {
+              const level = parseInt(heading.tagName.charAt(1));
+              const title = heading.textContent.trim();
+
+              // 计算缩进
+              let indent = "";
+              for (let i = 1; i < level; i++) {
+                indent += "&nbsp;&nbsp;&nbsp;&nbsp;";
+              }
+
+              // 添加目录项
+              tocHtml += `
+            <div class="toc-item" style="margin-left: ${(level - 1) * 20}px;">
+              <span class="toc-text">${indent}${title}</span>
+              <span class="toc-page-num">${pageNumber}</span>
+            </div>
+          `;
+
+              heading.id = `section-${pageNumber}`;
+              currentLevel = level;
+            });
+
+            pageNumber++; // 每个模板对应一页
+          });
+
+          // 将目录内容插入到目录页中
+          contentClone.querySelector("#toc").innerHTML = tocHtml;
+        } */
+
+        contentClone.querySelectorAll('input[type="text"]').forEach((input) => {
+          const span = document.createElement("span");
+          span.textContent = input.value;
+          input.parentNode.replaceChild(span, input);
+        });
+
+        let contentData = `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>
+          @page {
+            size: A4;
+            margin: 2cm;
+          }
+          body {
+            font-family: Arial, sans-serif;
+            font-size: 12pt;
+            line-height: 1.5;
+          }
+          table {
+            width: 100%;
+            border-collapse: collapse;
+          }
+          td, th {
+            border: 1px solid black;
+            padding: 5px;
+          }
+          .template-textarea {
+            page-break-after: always;
+          }
+           .toc-page {
+                page-break-before: always;
+                page-break-after: always;
+              }
+              .toc-content {
+                height: 100vh;
+                display: flex;
+                flex-direction: column;
+                justify-content: flex-start;
+                padding-top: 100px;
+              }
+              .toc-item {
+                display: flex;
+                justify-content: space-between;
+                margin-bottom: 10px;
+              }
+              .toc-text {
+                margin-right: 10px;
+              }
+              .toc-page-num {
+                flex-shrink: 0;
+              }
+              .toc-item::after {
+                content: "";
+                border-bottom: 1px dotted black;
+                flex-grow: 1;
+                order: 2;
+                margin: 0 5px;
+                position: relative;
+                top: -5px;
+              }
+              .toc-text {
+                order: 1;
+              }
+              .toc-page-num {
+                order: 3;
+              }
+              
+              .TOC {
+                display: none !important;
+              }
+        </style></head><body>${contentClone.innerHTML}</body></html>`; /*`<!DOCTYPE html><html><head><meta charset="UTF-8"><style>
+      @media print {
+         body {
+          width: 210mm;
+           height: 297mm;
+
+                margin: 0;
+                padding: 0;
+              }
+              .template-textarea {
+                page-break-after: always;
+              }
+              .toc-page {
+                page-break-before: always;
+                page-break-after: always;
+              }
+              .toc-content {
+                height: 100vh;
+                display: flex;
+                flex-direction: column;
+                justify-content: flex-start;
+                padding-top: 100px;
+              }
+              #toc {
+                margin: 0 auto;
+                max-width: 600px;
+              }
+              .toc-item {
+                display: flex;
+                justify-content: space-between;
+                margin-bottom: 10px;
+              }
+              .toc-text {
+                margin-right: 10px;
+              }
+              .toc-page-num {
+                flex-shrink: 0;
+              }
+              .toc-item::after {
+                content: "";
+                border-bottom: 1px dotted black;
+                flex-grow: 1;
+                order: 2;
+                margin: 0 5px;
+                position: relative;
+                top: -5px;
+              }
+              .toc-text {
+                order: 1;
+              }
+              .toc-page-num {
+                order: 3;
+              }
+              
+              .TOC {
+                display: none !important;
+              }
+      }
+    </style></head><body><p class="header1"></p>${contentClone.innerHTML}</body></html>` 移除 WPS 自动添加的目录引用框 */
+        contentData = contentData
+          .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
+          .replaceAll("<td>", `<td style="border: 1px solid black;">`);
+
+        const res = await exportDocument({
+          content: contentData,
+          title: this.docAttr.title,
+        });
+
+        if (res.status != 200) {
+          this.$alert(res.errMsg);
+          return;
+        }
+
+        let a = document.createElement("a");
+        a.href = res.data.file_path;
+        a.download = res.data.file_name;
+        document.body.appendChild(a);
+        a.click();
+        document.body.removeChild(a);
+      } catch (error) {
+        this.$alert("导出文档时发生错误,请稍后重试。");
+      } finally {
+        this.exporting = false; // 无论成功还是失败,都将导出状态设置回 false
+      }
+    },
+    /* 前端导出word */
+    async onExports() {
+      if (this.exporting) return;
+      this.exporting = true;
+
+      try {
+        let contentClone = this.$refs.content.cloneNode(true);
+        console.log("Original content:", contentClone.innerHTML);
+        /* const firstHeading = contentClone.querySelector("p");
+        if (firstHeading) {
+          const span = firstHeading.querySelector("span");
+          if (span) {
+            span.textContent = span.textContent
+              .replace(/&nbsp; &nbsp; &nbsp; &nbsp;/, " ")
+              .trim();
+          }
+        } */
+        // 处理被span包裹的图片
+        contentClone.querySelectorAll("span > img").forEach((img) => {
+          const span = img.parentElement;
+          const parent = span.parentElement;
+          parent.insertBefore(img, span);
+          if (span.childNodes.length === 0) {
+            parent.removeChild(span);
+          }
+        });
+        contentClone.querySelectorAll("table").forEach((table, tableIndex) => {
+          table.style.cssText = `
+        border-collapse: separate !important;
+        width: 100% !important;
+        border: none !important;
+      `;
+
+          table.querySelectorAll("tr").forEach((row, rowIndex) => {
+            row.querySelectorAll("td, th").forEach((cell, cellIndex) => {
+              let cellStyle = `
+            padding: 5px !important;
+            border: 0.1px solid #000000 !important;
+          `;
+
+              // 移除顶部边框(除了第一行)
+              if (rowIndex !== 0) {
+                cellStyle += "border-top: none !important;";
+              }
+
+              // 移除左侧边框(除了第一列)
+              if (cellIndex !== 0) {
+                cellStyle += "border-left: none !important;";
+              }
+
+              cell.style.cssText += cellStyle;
+            });
+          });
+        });
+        // 等待所有图片加载完成并转换为 Base64
+        const images = contentClone.querySelectorAll("img");
+        for (let img of images) {
+          await this.convertImageToBase64(img);
+          // 设置图片样式以确保它们显示正确
+          img.style.maxWidth = "100%";
+          img.style.height = "auto";
+          if (img.parentElement.tagName === "P") {
+            console.log(img);
+            img.parentElement.style.textAlign =
+              img === images[0] ? "left" : "right";
+            /* img.style.marginTop =img === images[0] ? '':"-80px"; */
+          }
+        }
+
+        console.log("Content after image processing:", contentClone.innerHTML);
+
+        // 处理输入框
+        contentClone.querySelectorAll('input[type="text"]').forEach((input) => {
+          const span = document.createElement("span");
+          span.textContent = input.value;
+          input.parentNode.replaceChild(span, input);
+        });
+
+        // 创建HTML内容
+        let contentData = `
+      <!DOCTYPE html>
+      <html>
+        <head>
+          <meta charset="UTF-8">
+          <style>
+            @page {
+              size: A4;
+              margin: 2cm;
+            }
+            body {
+              font-family: Arial, sans-serif;
+              font-size: 12pt;
+              line-height: 1.5;
+            }
+            .template-textarea {
+              page-break-after: always;
+            }
+            img {
+              max-width: 100%;
+              height: auto;
+            }
+          </style>
+        </head>
+        <body>${contentClone.innerHTML}</body>
+      </html>
+    `;
+
+        console.log("Final HTML content:", contentData);
+
+        // 创建文档选项
+        const options = {
+          margin: {
+            top: "2cm",
+            right: "2cm",
+            bottom: "2cm",
+            left: "2cm",
+          },
+          table: {
+            row: { cantSplit: true },
+          },
+          footer: {
+            default:
+              '<p style="color: #444; font-size: 10pt">页码: {page} / {pages}</p>',
+          },
+          pageNumber: true,
+        };
+
+        // 创建文档
+        const docxBlob = await HTMLtoDOCX(contentData, null, options);
+
+        // 保存文件
+        saveAs(docxBlob, `${this.docAttr.title}.docx`);
+
+        this.$message.success("文档导出成功");
+      } catch (error) {
+        console.error("导出文档时发生错误:", error);
+        this.$message.error("导出文档时发生错误,请稍后重试");
+      } finally {
+        this.exporting = false;
+      }
+    },
+
+    // 新增方法来处理所有表格
+    processAllTables(contentClone) {
+      contentClone.querySelectorAll("table").forEach((table) => {
+        table.style.borderCollapse = "separate";
+        table.style.width = "100%";
+        table.style.border = "none";
+        table.style.backgroundColor = "#000000";
+
+        table.querySelectorAll("tr").forEach((row) => {
+          row.querySelectorAll("td, th").forEach((cell) => {
+            cell.style.padding = "5px";
+            cell.style.borderRight = "0.1px solid #ffffff";
+            cell.style.borderBottom = "0.1px solid #ffffff";
+            cell.style.backgroundColor = "#ffffff";
+          });
+        });
+      });
+    },
+
+    // 修改 convertImageToBase64 方法
+    async convertImageToBase64(img) {
+      return new Promise((resolve, reject) => {
+        if (img.complete) {
+          this.toDataURL(img.src, (dataUrl) => {
+            img.src = dataUrl;
+            resolve();
+          });
+        } else {
+          img.onload = () => {
+            this.toDataURL(img.src, (dataUrl) => {
+              img.src = dataUrl;
+              resolve();
+            });
+          };
+          img.onerror = reject;
+        }
+      });
+    },
+
+    // 新增 toDataURL 方法
+    toDataURL(url, callback) {
+      var xhr = new XMLHttpRequest();
+      xhr.onload = function () {
+        var reader = new FileReader();
+        reader.onloadend = function () {
+          callback(reader.result);
+        };
+        reader.readAsDataURL(xhr.response);
+      };
+      xhr.open("GET", url);
+      xhr.responseType = "blob";
+      xhr.send();
+    },
+    /* 终 */
+    /* pdf导出 */
+    async onExportPDF() {
+      if (this.exporting) return;
+      this.exporting = true;
+      this.exportProgress = 0;
+
+      try {
+        const content = this.$refs.content;
+        console.log("Content element:", content); // 调试日志
+
+        // 保存原始样式
+        const originalStyle = {
+          height: content.style.height,
+          overflow: content.style.overflow,
+          position: content.style.position,
+        };
+
+        // 临时修改样式以确保所有内容可见
+        content.style.height = "auto";
+        content.style.overflow = "visible";
+        content.style.position = "absolute";
+
+        // 等待图片加载
+        await this.waitForImages(content);
+
+        const pdf = new jsPDF("p", "pt", "a4");
+        const pageHeight = pdf.internal.pageSize.getHeight();
+        const pageWidth = pdf.internal.pageSize.getWidth();
+        // 设置超时
+        const timeout = setTimeout(() => {
+          throw new Error("PDF export timed out");
+        }, 60000); // 60秒超时
+
+        await this.addContentToPDF(content, pdf, pageWidth, pageHeight);
+
+        clearTimeout(timeout);
+
+        pdf.save(`${this.docAttr.title || "document"}.pdf`);
+
+        // 恢复原始样式
+        Object.assign(content.style, originalStyle);
+
+        this.$message.success("PDF导出成功");
+      } catch (error) {
+        console.error("Export to PDF failed:", error);
+        this.$message.error(`PDF导出失败:${error.message}`);
+      } finally {
+        this.exporting = false;
+        this.exportProgress = 0;
+      }
+    },
+
+    async waitForImages(element) {
+      const images = element.getElementsByTagName("img");
+      const imagePromises = Array.from(images).map((img) => {
+        if (img.complete) return Promise.resolve();
+        return new Promise((resolve) => {
+          img.onload = img.onerror = resolve;
+        });
+      });
+      await Promise.all(imagePromises);
+    },
+
+    async addContentToPDF(element, pdf, pageWidth, pageHeight, topOffset = 0) {
+      const totalHeight = element.scrollHeight;
+      const canvas = await html2canvas(element, {
+        scale: 2,
+        useCORS: true,
+        logging: false,
+        windowWidth: pageWidth,
+        windowHeight: pageHeight,
+        y: topOffset,
+        ignoreElements: (el) => {
+          // 忽略使用已弃用的 -ms-high-contrast 的元素
+          const style = window.getComputedStyle(el);
+          return style.getPropertyValue("-ms-high-contrast") !== "none";
+        },
+        onclone: (clonedDoc) => {
+          // 在克隆的文档中进行任何必要的修改
+          const clonedElement = clonedDoc.body.querySelector(".content");
+          if (clonedElement) {
+            clonedElement.style.height = "auto";
+            clonedElement.style.overflow = "visible";
+            clonedElement.style.position = "static";
+          }
+        },
+      });
+
+      const imgData = canvas.toDataURL("image/jpeg", 1.0);
+      const imgWidth = pageWidth;
+      const imgHeight = (canvas.height * imgWidth) / canvas.width;
+
+      pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
+
+      this.exportProgress = Math.min(
+        100,
+        Math.round(((topOffset + pageHeight) / totalHeight) * 100)
+      );
+
+      if (canvas.height > pageHeight) {
+        pdf.addPage();
+        await this.addContentToPDF(
+          element,
+          pdf,
+          pageWidth,
+          pageHeight,
+          topOffset + pageHeight
+        );
+      }
+    },
+
+    /* 后端转pdf */
+    async onExportPdfApi() {},
+    /* onExport() {
+      let contentData = `<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><p class="header1"></p> ${this.$refs.content.innerHTML} </body></html>`;
+      contentData = contentData
+        .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
+        .replaceAll("<td>", `<td style="border: 1px solid black;">`);
+      exportDocument({ content: contentData, title: this.docAttr.title }).then(
+        (res) => {
+          if (res.status != 200) {
+            this.$alert(res.errMsg);
+            return;
+          }
+          let a = document.createElement("a");
+          a.href = res.data.file_path; // Use the file_path from the response
+          a.download = res.data.file_name; // Set the download attribute to the file_name
+          document.body.appendChild(a);
+          a.click();
+          document.body.removeChild(a);
+        }
+      );
+    }, */
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./view.scss";
+</style>

+ 55 - 0
ui/src/views/system/document/components/dataList.scss

@@ -0,0 +1,55 @@
+
+.data-list{
+
+
+  .btns{
+    display:flex;
+    flex-direction: row;
+  }
+
+
+
+
+  .page-info{
+    display:flex;
+    flex-direction: row;
+    margin-top:10px;
+    .opt{
+      width:50%;
+      display:flex;
+      flex-direction:row;
+      justify-content: flex-start;
+      align-items: center;
+
+      .pick{
+        width:100px;
+
+      }
+      .section{
+        margin-left:10px;
+      }
+    }
+
+    .page-data{
+
+      display:flex;
+      flex-direction: row;
+      justify-content: space-between;
+      align-items: center;
+      width:100%;
+      .page-item{
+          width:100%;
+          // border:1px solid red;
+      }
+      .page-opt{
+        width:100%;
+        // border:1px solid red;
+        display: flex;
+        justify-content: flex-end;
+      }
+    }
+
+
+  }
+
+}

+ 231 - 0
ui/src/views/system/document/components/dataList.vue

@@ -0,0 +1,231 @@
+<template>
+  <div class="data-list" v-loading="loading" element-loading-text="加载中...">
+    <el-table
+      :data="dataList"
+      style="width: 100%"
+      header-row-class-name="headerBg"
+      empty-text="没有文档信息"
+    >
+      <el-table-column prop="id" label="ID" align="center" width="80" />
+      <el-table-column
+        prop="title"
+        label="文档名称"
+        align="left"
+        min-width="180"
+      />
+      <el-table-column
+        prop="user_name"
+        label="创建人"
+        align="center"
+        width="150"
+      />
+      <el-table-column
+        prop="type_name"
+        label="所属分类"
+        align="center"
+        width="150"
+      />
+      <el-table-column
+        prop="create_time"
+        label="创建时间"
+        align="center"
+        width="150"
+      />
+      <el-table-column
+        prop="status"
+        label="文档状态"
+        align="center"
+        width="150"
+      >
+        <template #default="scope">
+          <div v-if="scope.row.status == 5">启用</div>
+          <div v-if="scope.row.status == 6">停用</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" :width="editWidth()">
+        <template #default="scope">
+          <div class="btns">
+            <el-button
+              type="primary"
+              size="small"
+              v-if="allowEdit"
+              @click="btnEdit(scope.row.id)"
+              ><svg-icon icon-class="edit" />编辑</el-button
+            >
+
+            <el-button
+              type="danger"
+              size="small"
+              v-if="allowDelete"
+              @click="btnDelete(scope.row.id)"
+              ><svg-icon icon-class="delete" />删除</el-button
+            >
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="page-info">
+      <el-pagination
+        :currentPage="queryForm.page"
+        :page-size="queryForm.pageSize"
+        :total="recordCount"
+        :page-count="pageTotal"
+        background
+        layout="prev, pager, next"
+        @current-change="ChangePage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+import { pageDocument, deleteDocument,searchlistDoc } from "@/api/document";
+export default {
+  props: {
+    queryForm: {
+      type: Object,
+      default: () => {
+        return {
+          page: 1,
+          pageSize: 10,
+          title: "",
+          category_id: "",
+          status: "",
+        };
+      },
+    },
+    allowEdit: {
+      type: Boolean,
+      default: false,
+    },
+    allowDelete: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  watch: {
+    queryForm: {
+      handler: function (val) {
+        this.search();
+      },
+      // immediate: true,//立即执行
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      loading: false,
+      dialogVisible: false, //控制产品审核
+      currentDataId: 0,
+      recordCount: 0,
+      pageTotal: 1,
+      dataList: [],
+      currentData: {},
+    };
+  },
+  mounted() {
+    this.search();
+  },
+  methods: {
+    editWidth() {
+      if (this.allowDelete && this.allowEdit) {
+        return 200;
+      }
+      if (this.allowDelete || this.allowEdit) {
+        return 120;
+      }
+      return 100;
+    },
+
+    onFocusVal(e) {
+      let _this = this;
+      _this.currentDataId = e.target.dataset.id;
+    },
+
+    onChangeVal(e) {
+      let _this = this;
+      let par = {
+        id: _this.currentDataId,
+        val: e,
+      };
+    },
+
+    btnDelete(e) {
+      let _this = this;
+      let par = {
+        id: e,
+      };
+      _this
+        .$confirm("您是否确认删除该记录?", "提示", {
+          confirmButtonText: "确认",
+          cancelButtonText: "取消",
+          type: "warning",
+        })
+        .then((res) => {
+          // console.log("AAA")
+          deleteDocument(par).then((res) => {
+            _this.search();
+          });
+        })
+        .catch(() => {});
+    },
+
+    searchData() {
+      let _this = this;
+      _this.dialogVisible = false;
+      _this.search();
+    },
+
+    //编辑
+    btnEdit(e) {
+      let _this = this;
+      let a = document.createElement("a");
+      a.href = "#/document/create?articleId=" + e+"&type=document"; // 使用 hash
+      a.target = "_blank";
+      a.click();
+
+       //this.$router.push({path:"/document/create",query:{articleId:e}})
+    },
+
+    handleClose() {
+      let _this = this;
+      _this.currentData = {};
+      _this.dialogVisible = false;
+    },
+
+    //搜索
+    search() {
+      let _this = this;
+      _this.loading = true; // Set loading to true before API call
+      searchlistDoc(_this.queryForm)
+        .then((res) => {
+          if (!res) {
+            _this.loading = false; // Set loading to false if no response
+            return;
+          }
+          _this.dataList = res.data.dataList.filter(
+            (el) => el.is_template == 0
+          );
+          _this.recordCount = res.data.totalRecord;
+          _this.pageTotal = res.data.pageTotal;
+          _this.loading = false; // Set loading to false after data is processed
+        })
+        .catch(() => {
+          _this.loading = false; // Set loading to false if there's an error
+        });
+    },
+    //修改分页
+    ChangePage(e) {
+      let _this = this;
+      _this.queryForm.page = e;
+      _this.search();
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./dataList.scss";
+</style>

+ 3 - 0
ui/src/views/system/document/components/dataSearch.scss

@@ -0,0 +1,3 @@
+.data-search{ 
+  padding:20px 10px 0px 10px;
+}

+ 141 - 0
ui/src/views/system/document/components/dataSearch.vue

@@ -0,0 +1,141 @@
+<template>
+  <div class="data-search">
+    <el-form :inline="true" :model="queryForm" class="demo-form-inline">
+      <el-form-item label="文档标题:">
+        <el-input
+          v-model="queryForm.title"
+          placeholder="请填写文档标题"
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="所属分类:">
+        <el-select
+          v-model="queryForm.category_id"
+          clearable
+          class="m-2"
+          placeholder="请选择分类"
+          size="large"
+        >
+          <el-option
+            v-for="item in categoryList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="文档状态:">
+        <el-select
+          v-model="queryForm.status"
+          class="m-2"
+          placeholder="请选择文档状态"
+          size="large"
+        >
+          <el-option
+            v-for="item in statusOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="primary" @click="onSubmit">
+          <svg-icon icon-class="search" /> 搜索</el-button
+        >
+      </el-form-item>
+      <el-form-item v-if="createNewDocument">
+        <el-button
+          type="primary"
+          @click="addDocument"
+          class="el-icon-circle-plus-outline"
+          >创建文档</el-button
+        >
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { searchDocumentCategory } from "@/api/document";
+export default {
+  props: {
+    queryForm: {
+      type: Object,
+      default: () => {
+        return {
+          page: 1,
+          pageSize: 10,
+          title: "",
+          category_id: "",
+          status: "",
+        };
+      },
+    },
+  },
+  data() {
+    return {
+      statusOptions: [
+        {
+          value: "",
+          label: "请选择状态",
+        },
+        {
+          value: 5,
+          label: "启用",
+        },
+        {
+          value: 6,
+          label: "停用",
+        },
+      ],
+      dialogVisible: false,
+      categoryList: [],
+      createNewDocument: true,
+    };
+  },
+  created() {
+    this.initCategoryList();
+    this.checkCreatePermission();
+  },
+  methods: {
+    checkCreatePermission() {
+      const createPermission = this.$store.state.user.authList.find(
+        (e) => e.code === "createNewDocument"
+      );
+      /* this.createNewDocument = !!createPermission; */
+    },
+
+    initCategoryList() {
+      let _this = this;
+      searchDocumentCategory({ page: 1, pageSize: 999, status: 5 }).then(
+        (res) => {
+          if (res.status != 200) return;
+          _this.categoryList = res.data.dataList;
+        }
+      );
+    },
+    onCreate(e) {
+      this.dialogVisible = true;
+    },
+    //搜索
+    onSubmit() {
+      this.$emit("bindSetQuery", this.$props.queryForm);
+    },
+    /* 创建文档 */
+    addDocument() {
+      this.$router.push('system/document/create');
+      /* let _this = this;
+      let a = document.createElement("a");
+      a.href = "#/document/create?type=document"; // 使用 hash
+      a.target = "_blank";
+      a.click(); */
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./dataSearch.scss";
+</style>

+ 3 - 0
ui/src/views/system/document/components/index.js

@@ -0,0 +1,3 @@
+export { default as dataSearch } from './dataSearch'
+export { default as dataList } from './dataList'
+ 

+ 226 - 0
ui/src/views/system/document/create.scss

@@ -0,0 +1,226 @@
+
+.doc-container{
+  width:90%;
+  min-height:90vh;
+  position:relative;
+  .editor-top{
+    display: flex;
+    width:88%;
+    height:60px;
+    min-height: 60px;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    position:fixed;
+    top:100px;
+    z-index: 999;
+    background: #fff;
+    border-bottom:1px solid #ebebeb;
+    .left{
+      width:50%;
+      height:100%;
+      display: flex;
+      flex-direction: row;
+      justify-content: flex-start;
+      align-items: center;
+      font-size:14px;
+      padding-left:10px;
+      .doc{
+          margin-right:20px;
+           font-size:14px;
+          >span{
+            font-weight:800;
+
+          }
+      }
+      .creator{
+        font-size:14px;
+          >span{
+            font-weight:800;
+          }
+      }
+
+    }
+    .right{
+      width:50%;
+      display: flex;
+      flex-direction: row;
+      justify-content: flex-end;
+      justify-items: center;
+      padding-right: 10px;
+    }
+
+  }
+  .editor-content{
+    width:100%;
+    min-height:80vh;
+    margin-top:60px;
+    border:1px solid #ebebeb;
+    .neary-doc{
+      position:fixed;
+      top:160px;
+      left:203px;
+      width:205px;
+      // min-height: 100%;
+      height: 99vh;
+      border:1px solid #ebebeb;
+      padding:5px;
+      border-radius: 10px;
+      border-top-left-radius: 0;
+      z-index:1000!important;
+      background:#FFF;
+      .left-content{
+         padding:10px;
+         height:80vh;
+         overflow:hidden;
+         overflow-y:auto;
+        
+          overflow-x: auto; /* Enable horizontal scrolling */
+        
+         .new-article{
+             background:#FFF;
+             margin-top:5px;
+             .sub-title{
+               height:30px;
+               line-height: 30px;
+               font-weight: bold;
+               margin-top:10px;
+               font-size:14px;
+               border-bottom: 1px solid #e5e5e5;
+             }
+             .el-collapse{
+               width:90%;
+               margin:auto;
+               .el-collapse-item {
+                .el-collapse-item__header{
+                  height: 30px;
+                }
+                   .el-collapse-item__content{
+                    padding-bottom: 5px;
+                     .sub-menus{
+                       margin-left:15px;
+                       font-size:12px;
+                       height:20px;
+                       line-height: 20px;
+                       /* cursor:move; */
+                     }
+                   }
+               }
+             }
+         }
+      }
+    }
+    .editor-main{
+      width:65%;
+      margin:auto;
+      overflow: hidden;
+      overflow-y: auto;
+      position:relative;
+      .menus-box{
+        position: fixed;
+        top:160px;
+        background:#FFF;
+        z-index:999;
+        width: 61.5%;
+      }
+
+      .editor-box{
+        margin-top:80px;
+      }
+      .editor{
+        width: 100%;
+        div{
+          span{
+            
+          }
+        }
+        
+        .template-textarea{
+          .rich-editor{
+               padding: 0 10px;     
+          }
+        } 
+      }
+
+    }
+
+
+    .editor-option{
+      width:255px;
+      height: 100vh;
+      position:fixed;
+      top:160px;
+      right:0;
+      border:1px solid #f0f0f0;
+      z-index: 1000 !important;
+      background:#FFF;
+      .editor-menu{
+        writing-mode: vertical-rl;
+        position: fixed;
+        width:60px;
+        right:254px;
+        top:165px;
+        font-size:12px;
+        .item{
+            background-color: #fff;
+            border: 1px solid rgba(0,0,0,.1);
+            /* -webkit-box-shadow: 0 2px 8px 0 rgba(0,0,0,.1);
+            box-shadow: 0 2px 8px 0 rgba(0,0,0,.1); */
+            border-radius: 2px;
+        /*     width: 94px; */
+        letter-spacing: 3px;
+            height: 75px;
+            display: inline-block;
+            text-align: center;
+            line-height: 32px;
+            margin-bottom: 12px;
+            -webkit-transition: all .8s;
+            transition: all .8s;
+            cursor: pointer;
+            .name{
+              font-size:12px;
+            }
+        }
+
+        .active-item{
+          background-color: #155bd4;
+          color: #fff;
+        }
+      }
+
+
+
+    }
+  }
+
+}
+/* .neary-doc {
+  display: flex;
+}
+
+.left-content {
+  flex: 1;
+}
+ */
+ .right-sidebarew{
+  position: absolute;
+  left: 203px;
+  top: 10px;
+  display: flex;
+  writing-mode: vertical-rl;
+  flex-direction: row;
+  font-size: 12px;
+ }
+.sidebar-item {
+  background-color: #fff;
+  border: 1px solid #ddd;
+  padding: 10px;
+  letter-spacing: 3px;
+  margin-bottom: 10px;
+  cursor: pointer;
+  text-align: center;
+}
+.active-item{
+  background-color: #155bd4;
+  color: #fff;
+}

+ 2297 - 0
ui/src/views/system/document/create.vue

@@ -0,0 +1,2297 @@
+<template>
+  <div class="doc-container">
+    <div class="editor-top">
+      <div class="left">
+        <div class="doc" v-if="type == 'document'">
+          <span>当前打开文档:</span
+          >{{ docAttr.title == "" ? "新建文档" : docAttr.title }}
+        </div>
+        <div class="doc" v-else-if="type == 'module'">
+          <span>当前打开模块:</span
+          >{{ coms[0].name == "" ? "新建模块" : coms[0].name }}
+        </div>
+        <div class="doc" v-else>
+          <span>当前打开模版:</span
+          >{{ docAttr.title == "" ? "新建模版" : docAttr.title }}
+        </div>
+        <div class="creator" v-if="userInfo != null">
+          <span>创建者:</span>{{ userInfo && userInfo.username }}
+        </div>
+      </div>
+      <div class="right">
+        <el-button
+          type="primary"
+          size="mini"
+          @click="onSaveAs"
+          v-if="
+            type !== 'module' && type == 'document' && articleId == undefined
+          "
+          >保存文档</el-button
+        ><!-- v-if="docAttr.id > 0" -->
+        <el-button
+          type="primary"
+          size="mini"
+          @click="renew"
+          v-if="
+            articleId !== undefined && type !== 'module' && type == 'document'
+          "
+          >更新文档</el-button
+        >
+        <!-- <el-popover
+          placement="bottom"
+          trigger="click"
+          v-if="type !== 'module' && type !== 'document'"
+        > -->
+        <el-button
+          type="primary"
+          v-if="
+            $store.state.user.name == 'admin' &&
+            type !== 'module' &&
+            type !== 'document' &&
+            !articleId
+          "
+          @click="onSaveUpload"
+          >保存</el-button
+        >
+        <el-button
+          type="primary"
+          v-if="
+            $store.state.user.name == 'admin' &&
+            type !== 'module' &&
+            type !== 'document' &&
+            articleId
+          "
+          @click="onUpload"
+          >更新</el-button
+        >
+        <!-- <el-button
+            slot="reference"
+            type="primary"
+            size="mini"
+            style="margin: 0 10px"
+            v-if="$store.state.user.name == 'admin'"
+            >保存并更新模版</el-button
+          >
+        </el-popover> -->
+        <!-- <el-button
+          type="primary"
+          size="mini"
+          v-if="$store.state.user.name == 'admin'"
+          @click="onSaveUpload"
+          >保存并更新模版</el-button
+        > -->
+        <el-button
+          type="primary"
+          size="mini"
+          v-if="type !== 'module' && type == 'document'"
+          @click="onOpenView"
+          >预览导出
+        </el-button>
+        <el-button type="primary" size="mini" @click="onClose">
+          关闭文档
+        </el-button>
+      </div>
+    </div>
+    <div class="editor-content">
+      <div class="neary-doc">
+        <div class="left-content">
+          <!--  <div class="search">
+            <el-input placeholder="搜索文档标题"></el-input>
+          </div> -->
+          <div class="new-article">
+            <div v-if="showView == 0 && type !== 'module'">
+              <el-collapse v-model="activeNames">
+                <el-collapse-item
+                  title="最新文档"
+                  name="-1"
+                  v-if="type == 'document'"
+                >
+                  <div
+                    v-for="(it, index) in articleList"
+                    draggable
+                    :key="it.id"
+                    :data-index="index"
+                    data-type="article"
+                    class="sub-menus"
+                  >
+                    <div @click="onLoadArticle(it.id)" style="cursor: pointer">
+                      {{ it.title }}
+                    </div>
+                  </div>
+                </el-collapse-item>
+                <el-collapse-item
+                  title="模版列表"
+                  name="-2"
+                  v-if="showView == 0"
+                >
+                  <div
+                    v-for="(it, index) in templateList"
+                    draggable
+                    :key="it.id"
+                    :data-index="index"
+                    data-type="article"
+                    class="sub-menus"
+                  >
+                    <div @click="onTemplateInfo(it.id)" style="cursor: pointer">
+                      {{ it.title }}
+                    </div>
+                  </div>
+                </el-collapse-item>
+              </el-collapse>
+            </div>
+            <div v-if="showView == 1" class="scrollable-container">
+              <!-- <el-collapse
+                v-model="activeModel"
+                :accordion="true"
+                @change="handleItemClick"
+                class="borderless-collapse"
+              >
+                <el-collapse-item
+                  v-for="(item, itemIndex) in categoryList"
+                  :key="itemIndex"
+                  :title="item.name"
+                  :name="item.id"
+                >
+                  <el-collapse
+                    v-if="item.children && item.children.length"
+                    v-model="item.activeChildren"
+                    @change="handleItemVal"
+                    v-show="item.children"
+                    class="borderless-collapse"
+                  >
+                    <el-collapse-item
+                      v-for="(child, childIndex) in item.children"
+                      :key="childIndex"
+                      :title="child.name"
+                      :name="child.id"
+                    >
+                      <draggable
+                        v-model="child.dataList"
+                        :options="{
+                          group: { name: 'itxst', pull: 'clone' },
+                          sort: true,
+                        }"
+                        :disabled="type == 'module'"
+                      >
+                        <transition-group>
+                          <div
+                            class="sub-menus"
+                            v-for="(its, index) in child.dataList"
+                            :key="index"
+                          >
+                            <div>{{ its.name }}</div>
+                          </div>
+                        </transition-group>
+                      </draggable>
+                    </el-collapse-item>
+                  </el-collapse>
+                  <draggable
+                    v-model="item.dataList"
+                    :options="{
+                      group: { name: 'itxst', pull: 'clone' },
+                      sort: true,
+                    }"
+                    :disabled="type == 'module'"
+                  >
+                    <transition-group>
+                      <div
+                        class="sub-menus"
+                        v-for="(it, index) in item.dataList"
+                        :key="index"
+                      >
+                        <div>{{ it.name }}</div>
+                      </div>
+                    </transition-group>
+                  </draggable>
+                </el-collapse-item>
+              </el-collapse> -->
+              <el-collapse
+                v-model="activeModel"
+                :accordion="true"
+                @change="handleItemClick"
+                class="borderless-collapse"
+              >
+                <el-collapse-item
+                  v-for="(item, itemIndex) in categoryList"
+                  :key="itemIndex"
+                  :title="item.name"
+                  :name="item.id"
+                  class="min-width-content"
+                >
+                  <el-collapse
+                    v-if="item.children && item.children.length"
+                    v-model="item.activeChildren"
+                    @change="handleItemVal"
+                    v-show="item.children"
+                    class="borderless-collapse"
+                  >
+                    <el-collapse-item
+                      v-for="(child, childIndex) in item.children"
+                      :key="childIndex"
+                      :title="child.name"
+                      :name="child.id"
+                      class="min-width-content"
+                    >
+                      <el-collapse
+                        v-if="child.children && child.children.length"
+                        v-model="child.activeChildren"
+                        @change="handleChildItemVal"
+                        v-show="child.children"
+                        class="borderless-collapse"
+                      >
+                        <el-collapse-item
+                          v-for="(
+                            grandChild, grandChildIndex
+                          ) in child.children"
+                          :key="grandChildIndex"
+                          :title="grandChild.name"
+                          :name="grandChild.id"
+                          class="min-width-content"
+                        >
+                          <el-collapse
+                            v-if="
+                              grandChild.children && grandChild.children.length
+                            "
+                            v-model="grandChild.activeChildren"
+                            @change="handleGrandChildItemVal"
+                            v-show="grandChild.children"
+                            class="borderless-collapse"
+                          >
+                            <el-collapse-item
+                              v-for="(
+                                greatGrandChild, greatGrandChildIndex
+                              ) in grandChild.children"
+                              :key="greatGrandChildIndex"
+                              :title="greatGrandChild.name"
+                              :name="greatGrandChild.id"
+                              class="min-width-content"
+                            >
+                              <el-collapse
+                                v-if="
+                                  greatGrandChild.children &&
+                                  greatGrandChild.children.length
+                                "
+                                v-model="greatGrandChild.activeChildren"
+                                @change="handlegreatGrandChildItemVal"
+                                v-show="greatGrandChild.children"
+                                class="borderless-collapse"
+                              >
+                                <el-collapse-item
+                                  v-for="(
+                                    fiveChild, fiveIndex
+                                  ) in greatGrandChild.children"
+                                  :key="fiveIndex"
+                                  :title="fiveChild.name"
+                                  :name="fiveChild.id"
+                                  class="min-width-content"
+                                >
+                                  <draggable
+                                    v-model="fiveChild.dataList"
+                                    :options="{
+                                      group: { name: 'itxst', pull: 'clone' },
+                                      sort: true,
+                                    }"
+                                    :disabled="type === 'module'"
+                                  >
+                                    <transition-group>
+                                      <div
+                                        class="sub-menus min-width-content"
+                                        v-for="(
+                                          fiveIts, fiveIndex
+                                        ) in fiveChild.dataList"
+                                        :key="fiveIndex"
+                                      >
+                                        <div style="cursor: move">
+                                          {{ fiveIts.name }}
+                                        </div>
+                                      </div>
+                                    </transition-group>
+                                  </draggable>
+                                </el-collapse-item>
+                              </el-collapse>
+                              <draggable
+                                v-model="greatGrandChild.dataList"
+                                :options="{
+                                  group: { name: 'itxst', pull: 'clone' },
+                                  sort: true,
+                                }"
+                                :disabled="type == 'module'"
+                              >
+                                <transition-group>
+                                  <div
+                                    class="sub-menus min-width-content"
+                                    v-for="(
+                                      its, index
+                                    ) in greatGrandChild.dataList"
+                                    :key="index"
+                                  >
+                                    <div style="cursor: move">
+                                      {{ its.name }}
+                                    </div>
+                                  </div>
+                                </transition-group>
+                              </draggable>
+                            </el-collapse-item>
+                          </el-collapse>
+                          <draggable
+                            v-model="grandChild.dataList"
+                            :options="{
+                              group: { name: 'itxst', pull: 'clone' },
+                              sort: true,
+                            }"
+                            :disabled="type == 'module'"
+                          >
+                            <transition-group>
+                              <div
+                                class="sub-menus min-width-content"
+                                v-for="(it, index) in grandChild.dataList"
+                                :key="index"
+                              >
+                                <div style="cursor: move">{{ it.name }}</div>
+                              </div>
+                            </transition-group>
+                          </draggable>
+                        </el-collapse-item>
+                      </el-collapse>
+                      <draggable
+                        v-model="child.dataList"
+                        :options="{
+                          group: { name: 'itxst', pull: 'clone' },
+                          sort: true,
+                        }"
+                        :disabled="type == 'module'"
+                      >
+                        <transition-group>
+                          <div
+                            class="sub-menus min-width-content"
+                            v-for="(it, index) in child.dataList"
+                            :key="index"
+                          >
+                            <div style="cursor: move">{{ it.name }}</div>
+                          </div>
+                        </transition-group>
+                      </draggable>
+                    </el-collapse-item>
+                  </el-collapse>
+                  <draggable
+                    v-model="item.dataList"
+                    :options="{
+                      group: { name: 'itxst', pull: 'clone' },
+                      sort: true,
+                    }"
+                    :disabled="type == 'module'"
+                  >
+                    <transition-group>
+                      <div
+                        class="sub-menus min-width-content"
+                        v-for="(it, index) in item.dataList"
+                        :key="index"
+                      >
+                        <div style="cursor: move">{{ it.name }}</div>
+                      </div>
+                    </transition-group>
+                  </draggable>
+                </el-collapse-item>
+              </el-collapse>
+            </div>
+          </div>
+        </div>
+        <div class="right-sidebarew">
+          <div
+            v-if="type !== 'module'"
+            class="sidebar-item"
+            :class="showView == 0 ? 'active-item' : ''"
+            @click="viewDocument"
+          >
+            <span v-if="type == 'document'">查看文档</span>
+            <span v-else>查看模版</span>
+          </div>
+          <div
+            v-if="type !== 'document'"
+            class="sidebar-item"
+            :class="showView == 1 ? 'active-item' : ''"
+            @click="viewModule"
+          >
+            模块列表
+          </div>
+        </div>
+      </div>
+
+      <div class="editor-main">
+        <div class="menus-box">
+          <menus
+            @onEvents="onInsert"
+            @onVariable="uptadeVariable"
+            :comArr="coms"
+          ></menus>
+        </div>
+        <div
+          class="editor-box"
+          ref="tabHtml"
+          v-loading="loading"
+          element-loading-text="加载中..."
+          element-loading-spinner="el-icon-loading"
+        >
+          <editor
+            :coms="coms"
+            :comIndex="comIndex"
+            :isAdmin="isAdmin"
+            :templateCate="categoryList"
+            :insertCmd="insertCmd"
+            @onSetActiveIndex="onSetActiveIndex"
+            @onInsert="onInsert"
+            @onLoadArticle="onLoadArticle"
+            @onRebuild="onRebuild"
+            @onDelete="onRemove"
+            @onSetComs="onSetComs"
+          ></editor>
+        </div>
+      </div>
+
+      <div class="editor-option">
+        <div class="editor-menu">
+          <div
+            v-if="type !== 'module' && type !== 'document'"
+            class="item"
+            @click="onSetActiveIndexs(3)"
+            :class="docuComIndex == 3 ? 'active-item' : ''"
+          >
+            <span class="name">模版属性</span>
+          </div>
+          <div
+            v-if="type !== 'module'"
+            class="item"
+            @click="onSetActiveIndex(9998)"
+            :class="comIndex == 9998 ? 'active-item' : ''"
+          >
+            <span class="name">模块管理</span>
+          </div>
+          <div
+            class="item"
+            @click="onCatalogIndex(9997)"
+            :class="catalogIndex == 9997 ? 'active-item' : ''"
+          >
+            <span class="name">目录信息</span>
+          </div>
+          <div
+            class="item"
+            @click="onAttributeIndex(1)"
+            :class="AttributeIndex == 1 ? 'active-item' : ''"
+          >
+            <span class="name">属性变量</span>
+          </div>
+        </div>
+
+        <template
+          v-if="docuComIndex == 3 && type !== 'module' && type !== 'document'"
+        >
+          <docAttr :attrs="docAttr"></docAttr>
+        </template>
+        <template v-if="comIndex == 9998">
+          <comList
+            :coms="coms"
+            @onRemove="onRemove"
+            @onSetActive="onSetActiveIndex"
+            @onRebuild="onRebuild"
+          ></comList>
+        </template>
+        <template v-if="catalogIndex == 9997">
+          <Directory :coms="coms" />
+        </template>
+        <!-- comIndex != 9999 && comIndex != 9998 && comIndex != 9997 AttributeIndex==1-->
+        <!-- <template
+          v-if="
+            comIndex != 9999 &&
+            comIndex != 9998 &&
+            catalogIndex != 9997 &&
+            AttributeIndex != 1
+          "
+        >
+          <Attribute
+            :com="coms[comIndex]"
+            @onRefresh="initCategoryList"
+          ></Attribute>
+        </template> -->
+        <template v-if="AttributeIndex == 1">
+          <attributeVar
+            :com="coms[comIndex]"
+            @onRefresh="initCategoryList"
+          ></attributeVar>
+        </template>
+        <!-- <Directory :coms="coms" /> -->
+      </div>
+    </div>
+
+    <el-dialog
+      :visible.sync="showViewForm"
+      :close-on-click-modal="false"
+      append-to-body
+      v-el-drag-dialog
+      width="850px"
+      custom-class="prod-verify"
+      title="文档预览"
+    >
+      <ViewForm :coms="coms" :docAttr="docAttr"></ViewForm>
+    </el-dialog>
+
+    <el-dialog
+      :visible.sync="showProductAttr"
+      append-to-body
+      v-el-drag-dialog
+      :close-on-click-modal="false"
+      width="850px"
+      custom-class="prod-verify"
+      title="插入产品属性"
+    >
+      <ProductAttr
+        :docAttr="docAttr"
+        @onInsertProductAttr="onInsertProductAttr"
+      ></ProductAttr>
+    </el-dialog>
+    <el-dialog
+      :visible.sync="docVisible"
+      append-to-body
+      v-el-drag-dialog
+      :close-on-click-modal="false"
+      width="500px"
+      custom-class="prod-verify"
+      title="文档信息"
+    >
+      <el-form
+        :model="docForm"
+        :rules="docRules"
+        ref="docRef"
+        label-position="left"
+        label-width="100"
+      >
+        <el-form-item label="文档名称:" prop="title">
+          <el-input
+            v-model="docForm.title"
+            style="width: 60%"
+            class="input-item"
+            placeholder="请填写文档名称"
+            :disabled="docAttr.id > 0"
+          />
+        </el-form-item>
+
+        <el-form-item label="文档分类:" prop="category_id">
+          <el-select
+            v-model="docForm.category_id"
+            style="width: 60%"
+            placeholder="请选择所属分类"
+            :disabled="docAttr.id > 0"
+          >
+            <el-option
+              v-for="(item, index) in articleCategoryList"
+              :key="index"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="创建人:">
+          {{ userInfo && userInfo.username }}
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="closeDoc">取 消</el-button>
+        <el-button type="primary" @click="submitDoc">确 定</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { findData } from "@/api/sourceData";
+/* import utils from "@/utils/fun"; // 方法类 */
+import { searchDocument, pageDocument } from "@/api/document";
+import menus from "./com/menus";
+import editor from "./com/editor";
+import Attribute from "./com/components/Attribute";
+import attributeVar from "./com/components/Attribute/attributeVar/index.vue";
+import ProductAttr from "./com/components/ProductAttr";
+import {
+  searchTemplateCategory,
+  searchTemplate,
+  getTemplateInfo,
+  getAllCategory,
+} from "@/api/template";
+import docAttr from "./com/components/Attribute/docAttr";
+import comList from "./com/components/Attribute/comList";
+import Directory from "./com/components/Attribute/Directory";
+import { getCurrentUserInfo } from "@/api/api";
+import {
+  createDocument,
+  updateDocument,
+  getDocumentInfo,
+  exportDocument,
+  searchDocumentCategory,
+} from "@/api/document";
+import ViewForm from "./com/view";
+import elDragDialog from "@/directive/el-drag-dialog";
+import draggable from "vuedraggable";
+import htmlDocx from "html-docx-js/dist/html-docx";
+import { searchFormula } from "@/api/formula";
+import axios from "axios";
+import settings from "@/settings";
+// import htmlDocx from "htmlDocx";
+export default {
+  name: "create",
+  components: {
+    menus,
+    editor,
+    Attribute,
+    comList,
+    docAttr,
+    ViewForm,
+    ProductAttr,
+    Directory,
+    draggable,
+    attributeVar,
+  },
+  directives: {
+    elDragDialog,
+  },
+  data() {
+    return {
+      activeNames: "-1",
+      nestedActiveNames: [],
+      articleList: [],
+      activeModel: "",
+      templateList: [],
+      categoryList: [],
+      //组件相关
+      coms: [],
+      comIndex: 9999, //当前组件索引
+      showProductAttr: false,
+      comList: [],
+      //文档属性
+      docAttr: {
+        id: 0,
+        category_id: "",
+        title: "",
+        content: "",
+        status: 5,
+        links: "",
+        linkProduct: [],
+        linkProject: [],
+        projects: "",
+      },
+      insertCmd: null,
+      showViewForm: false,
+      isAdmin: 2, //1是管理员,2非管理员
+      //当前选中表格
+      tabExt: {
+        row: 0,
+        col: 0,
+      },
+      userInfo: null,
+      articleId: 0,
+      loading: false, // 增加 loading 状态
+      showView: 0, //查看 文档/模块
+      childrenList: "",
+      value: [],
+      AttributeIndex: 0,
+      catalogIndex: 0, //目录信息
+      docuComIndex: 3,
+      type: "",
+      docVisible: false,
+      docForm: {
+        title: "",
+        category_id: "",
+      },
+      docRules: {
+        title: [{ required: true, message: "请输文档名称", trigger: "blur" }],
+        category_id: [
+          { required: true, message: "请选择文档分类", trigger: "change" },
+        ],
+      },
+      articleCategoryList: [],
+    };
+  },
+  watch: {
+    coms: {
+      handler(newComs, oldComs) {
+        this.updateAttrs(newComs, oldComs);
+      },
+      immediate: true, //立即执行
+      deep: true,
+    },
+    articleId: {
+      handler(val) {
+        if (val == null || val == undefined || val <= 0) return;
+        if (this.$route.query.type == "document") {
+          this.loadArticle(val);
+        } else {
+          this.onTemplateInfo(val);
+        }
+        //
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+
+  async mounted() {
+    this.articleId = this.$route.query.articleId;
+    this.type = this.$route.query.type;
+    if (this.type == "module") {
+      this.showView = 1;
+    }
+
+    this.searchArticle();
+    await this.initCategoryList();
+    await this.initCurrentUser();
+    let templateId = this.$route.query.templateId;
+    if (templateId > 0) {
+      this.loadTemplateInfo(templateId);
+    }
+    this.isAdmin = this.userInfo ? this.userInfo.roleInfo.is_admin : 2;
+    this.initArticleCategoryList();
+    if (this.type !== "module" && this.type !== "document") {
+      document.title = "新建模版-" + settings.title;
+    }
+    if (
+      this.type !== "module" &&
+      this.type !== "document" &&
+      this.articleId !== undefined
+    ) {
+      document.title = this.docAttr.title + settings.title;
+    }
+    this.$nextTick(() => {
+      const arrows = document.querySelectorAll(".el-collapse-item__arrow");
+      arrows.forEach((arrow) => {
+        arrow.style.margin = "0";
+      });
+    });
+  },
+
+  methods: {
+    handleItemVal(value) {
+      this.$forceUpdate();
+      this.loadChildren(value);
+    },
+    handleChildItemClick(child) {
+      if (!child.children) {
+        this.loadChildren(child);
+      }
+    },
+    handleChildItemVal(value) {
+      this.$forceUpdate();
+      this.loadChildren(value);
+    },
+    handleGrandChildItemClick(grandChild) {
+      if (!grandChild.children) {
+        this.loadChildren(grandChild);
+      }
+    },
+    handleGrandChildItemVal(value) {
+      this.loadChildren(value);
+      this.$forceUpdate();
+    },
+    handlegreatGrandChildItemVal(greatGrandChild) {
+      this.loadChildren(greatGrandChild);
+      this.$forceUpdate();
+    },
+    async loadChildren(category) {
+      const category_id = category[category.length - 1];
+
+      try {
+        const res = await searchTemplateCategory({ parent_id: category_id });
+        if (res.status != 200 || res.data.length == 0) return;
+        const newChildren = res.data;
+        for (let i = 0; i < newChildren.length; i++) {
+          newChildren[i].dataList = await this.getTemplateList(
+            newChildren[i].id
+          );
+        }
+        this.insertChildrenById(this.categoryList, category_id, newChildren);
+        console.log(this.categoryList);
+        this.$forceUpdate(); // Force Vue to re-render the component
+      } catch (error) {
+        console.error("Error fetching template categories:", error);
+      }
+    },
+
+    insertChildrenById(list, id, newChildren) {
+      for (let item of list) {
+        if (item.id === id) {
+          this.$set(item, "children", newChildren); // Use Vue.set to ensure reactivity
+          return true; // Stop searching once the item is found
+        }
+        if (item.children && item.children.length > 0) {
+          const found = this.insertChildrenById(item.children, id, newChildren);
+          if (found) return true; // Stop searching if the item is found in deeper levels
+        }
+      }
+      return false; // Continue searching
+    },
+    /* 取消新增 */
+    closeDoc() {
+      this.docVisible = false;
+      this.docForm = {
+        title: "",
+        category_id: "",
+      };
+    },
+    /* 确定新增模块*/
+    submitDoc() {
+      this.$refs.docRef.validate((valid) => {
+        if (valid) {
+          this.docAttr.title = this.docForm.title;
+          this.docAttr.category_id = this.docForm.category_id;
+          this.onSave();
+        }
+      });
+    },
+    //加载文档分类
+    initArticleCategoryList() {
+      let _this = this;
+      searchDocumentCategory({ page: 1, pageSize: 99, status: 5 }).then(
+        (res) => {
+          if (res.status != 200) return;
+          _this.articleCategoryList = res.data.dataList;
+        }
+      );
+    },
+    updateAttrs(newComs, oldComs) {
+      console.log(newComs);
+      newComs.forEach((newCom, comIndex) => {
+        const oldCom = oldComs[comIndex];
+        if (oldCom) {
+          newCom.attrs.forEach((newAttr, attrIndex) => {
+            const oldAttr = oldCom.attrs[attrIndex];
+            if (oldAttr && newAttr.content !== oldAttr.content) {
+              // 内容发生变化,进行全局更新
+              this.updateGlobalAttr(newAttr.name, newAttr.content);
+            }
+          });
+        }
+      });
+    },
+
+    updateGlobalAttr(attrId, newContent) {
+      // 遍历所有组件,更新匹配的属性
+      this.coms.forEach((com) => {
+        com.attrs.forEach((attr) => {
+          // 只更新非 variableNull 类型的属性
+          if (attr.name === attrId && attr.type !== "variableNull") {
+            attr.content = newContent;
+          }
+        });
+
+        // 如果是文本区域,更新内容中的占位符
+        if (com.type === "TextArea") {
+          com.content = com.content.replace(
+            new RegExp(`{{${attrId}}}`, "g"),
+            (match) => {
+              const attr = com.attrs.find((a) => a.id === attrId);
+              if (attr && attr.type !== "variableNull") {
+                return newContent;
+              }
+              return match; // 保留 variableNull 类型的原始占位符
+            }
+          );
+        }
+      });
+
+      // 触发视图更新
+      this.replaceData(this.coms);
+      this.$forceUpdate();
+    },
+
+    async replaceData(data) {
+      for (let item of this.coms) {
+        for (let el of item.attrs) {
+          let attrId = el.id;
+          if (el.type === "formual") {
+            try {
+              let formual = await this.analysisFormual(el);
+              formual = await this.getRemote(formual);
+              el.content = eval(formual);
+            } catch (error) {
+              console.error("处理公式时出错:", error);
+            }
+          } else if (el.type === "variableNull") {
+            // 保留 variableNull 类型的原始内容
+            el.content = el.content || "请输入";
+          }
+        }
+      }
+      return data;
+    },
+    //分析公式
+    async analysisFormual(attrs) {
+      let _this = this;
+      // console.log("开始分析公式:", attrs.formula);
+      var pattern = /(\[.*?\]){3}/;
+      var formual = attrs.formula;
+      var reg = new RegExp(pattern);
+      while (true) {
+        var items = formual.match(reg);
+        if (items == null) break;
+        let itemName = items[0];
+        // console.log("处理项:", itemName);
+        try {
+          let data = await _this.getFormualData(itemName);
+          // console.log(`获取到的数据: ${itemName} = ${data}`);
+          if (data === null || data === undefined || isNaN(data)) {
+            console.warn(`获取到的数据无效: ${itemName}`);
+            formual = formual.replace(itemName, "(0)");
+          } else {
+            formual = formual.replace(itemName, `(${parseFloat(data)})`);
+          }
+        } catch (error) {
+          console.error(`处理 ${itemName} 时出错:`, error);
+          formual = formual.replace(itemName, "(0)");
+        }
+      }
+      // console.log("分析后的公式:", formual);
+      return formual;
+    },
+    async getFormualData(formualItem) {
+      let _this = this;
+      // console.log("开始获取公式数据:", formualItem);
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      let items = formualItem.match(reg);
+      // console.log("解析结果:", items);
+      let calResult = 0;
+      if (items && items[1] == "T") {
+        calResult = await _this.getModuleData(items[2], items[3]);
+      } else {
+        console.warn("无法解析公式项:", formualItem);
+      }
+      // console.log("计算结果:", calResult);
+      return calResult;
+    },
+    //获取本地文档模块数据
+    async getModuleData(code, attrName) {
+      let _this = this;
+      // console.log(`尝试获取模块数据: code=${code}, attrName=${attrName}`);
+      // console.log("当前 coms:", _this.coms);
+
+      let item = _this.coms.filter((o) => o.name == code);
+      // console.log(`找到的模块:`, item);
+
+      if (item.length > 0) {
+        let attr = item[0].attrs.filter((o) => o.name == attrName);
+        // console.log(`找到的属性:`, attr);
+
+        if (attr.length > 0) {
+          // console.log(`返回的值:`, attr[0].content);
+          return parseFloat(attr[0].content);
+        }
+      }
+      // console.log("未找到匹配的模块或属性,返回 0");
+      return 0;
+    },
+
+    async getRemote(formual) {
+      let _this = this;
+      var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
+      var reg = new RegExp(pattern);
+      while (true) {
+        let items = formual.match(reg);
+        if (items == null) break;
+        if (items[1] == "R") {
+          //表示获取远程数据
+          var dataIndex = items[4];
+          dataIndex = items[4].split(",");
+          let calResult = await _this.getRemoteData(
+            items[2],
+            items[3],
+            dataIndex[0],
+            dataIndex[1]
+          );
+          let itemName = items[0];
+          // 检查calResult是否为数字
+          if (!isNaN(calResult)) {
+            formual = formual.replace(
+              itemName,
+              "(" + parseFloat(calResult) + ")"
+            );
+          } else {
+            formual = formual.replace(itemName, `"${calResult}"`);
+          }
+          formual = formual.replace(
+            itemName,
+            "(" + parseFloat(calResult) + ")"
+          );
+        }
+      }
+      return formual;
+    },
+    async getRemoteData(code, sheet, row, col) {
+      let _this = this;
+      let result = 0;
+      let par = {
+        code: code,
+        sheetName: sheet,
+        row: row,
+        col: col,
+      };
+
+      /*  if (!this.isEditing) {
+        this.loading = true;
+        this.progress = 0;
+
+        try {
+          const updateProgress = setInterval(() => {
+            if (this.progress < 90) {
+              this.progress += 10;
+            }
+          }, 100);
+
+          let res = await findData(par);
+          if (res.status == 200) {
+            result = res.data.result;
+          }
+
+          this.progress = 100;
+          clearInterval(updateProgress);
+        } catch (error) {
+          console.error("Error fetching remote data:", error);
+        } finally {
+          setTimeout(() => {
+            this.loading = false;
+            this.progress = 0;
+          }, 1000);
+        }
+      } else { */
+      // 如果正在编辑,直接获取数据而不显示进度条
+      try {
+        let res = await findData(par);
+        if (res.status == 200) {
+          result = res.data.result;
+          // 添加数据类型检查和转换
+          if (typeof result === "string" && !isNaN(result)) {
+            result = parseFloat(result);
+          }
+        }
+      } catch (error) {
+        console.error("获取远程数据错误:", error);
+      }
+      /*   } */
+
+      return result;
+    },
+    /* 
+    handleItemVal(value) {
+      console.log(value);
+      this.$forceUpdate();
+    }, */
+    async handleItemClick(item) {
+      const category = this.categoryList.find((cat) => cat.id === item);
+      if (!category) return;
+
+      if (!category.children) {
+        try {
+          const res = await searchTemplateCategory({ parent_id: item });
+          if (res.status != 200 || res.data.length == 0) return;
+          category.children = res.data;
+          for (let i = 0; i < category.children.length; i++) {
+            category.children[i].dataList = await this.getTemplateList(
+              category.children[i].id
+            );
+          }
+        } catch (error) {
+          console.error("Error fetching template categories:", error);
+        }
+      }
+
+      // 触发视图更新
+      this.$set(category, "activeChildren", category.activeChildren || []);
+    },
+    /* 切换标签 */
+    viewDocument() {
+      this.showView = 0;
+    },
+    viewModule() {
+      this.showView = 1;
+    },
+    /* 更新变量 */
+    /*  uptadeVariable(value) {
+      for (const item of this.coms) {
+        for (const el of item.attrs) {
+          if (el.type === "variable") {
+            if (el.data.id == value.id) {
+              el.content = value.value;
+              el.data = value;
+            }
+          } else if (el.type === "constant") {
+            if (el.data.id == value.id) {
+              el.content = value.value;
+              el.data = value;
+            }
+          }
+        }
+      }
+    } */
+    uptadeVariable(value) {
+      const { id, value: newValue } = value;
+      this.coms.forEach((item) => {
+        item.attrs.forEach((el) => {
+          if (
+            (el.type === "variable" || el.type === "constant") &&
+            el.data.id === id
+          ) {
+            el.content = newValue;
+            el.data = value;
+          }
+        });
+      });
+    },
+    /* 更新检索内容 */
+    async uptadeSearch() {
+      for (const item of this.coms) {
+        for (const el of item.attrs) {
+          if (el.type === "ai") {
+            // await this.retrieval(el);
+          }
+        }
+      }
+    },
+    /* 初始化检索 */
+    async retrieval(el) {
+      this.loading = true; // 开始加载动画
+      try {
+        const response = await axios.post(
+          "http://58.246.234.210:7860/api/v1/run/3f84a841-cefd-44b3-9555-568cc3b6c2d2?stream=false",
+          {
+            input_value: el.search,
+            output_type: "chat",
+            input_type: "chat",
+            tweaks: {
+              "ChatInput-em6qC": {},
+              "ParseData-yO3YQ": {},
+              "Prompt-Wj75b": {},
+              "ChatOutput-zy9na": {},
+              "SplitText-O1knk": {},
+              "File-4j6Zd": {},
+              "OllamaEmbeddings-xbYXX": {},
+              "Chroma-OIejP": {},
+              "OllamaModel-04mEO": {},
+              "OllamaEmbeddings-UGvLP": {},
+              "Chroma-HzukO": {},
+            },
+          },
+          {
+            headers: {
+              "Content-Type": "application/json",
+            },
+          }
+        );
+
+        if (response.status === 200) {
+          // 假设返回的数据在 response.data.outputs[0].outputs[0].results.message.data.text 中
+          const resultText =
+            response.data.outputs[0].outputs[0].results.message.data.text;
+          // 将搜索词和生成的内容存储在 searchResult 对象中
+          el.content = resultText; // 重新赋值给原数组的对应 content
+        }
+      } catch (error) {
+        console.error("Error during retrieval:", error);
+        this.$message.error("检索失败,请稍后重试");
+      } finally {
+        this.loading = false; // 结束加载动画
+      }
+    },
+    async loadTemplateInfo(id) {
+      try {
+        // 获取模板信息
+        const { data } = await getTemplateInfo({ id });
+
+        // 设置默认值
+        if (!data.attrs || !data.type) {
+          Object.assign(data, {
+            attrs: "[]",
+            content: "请填写内容",
+            lay_id: "textArea",
+            type: "TextArea",
+          });
+        }
+
+        // 设置文档标题
+        document.title = data.name
+          ? `${data.name}-${settings.title}`
+          : "新建模块-" + settings.title;
+
+        // 解析属性
+        data.attrs = JSON.parse(data.attrs);
+
+        // 获取公式并更新属性
+        const {
+          data: { dataList },
+        } = await searchFormula({ page: 1, pageSize: 999 });
+        console.log(data);
+        data.attrs = data.attrs.map((item) => {
+          const formula = dataList.find((el) => el.id === item.id);
+          return formula
+            ? { ...item, formula: formula.formula, data: formula }
+            : item;
+        });
+        // 从 content 中提取 {{}} 包裹的 ID
+        const contentIds = (data.content.match(/{{([^}]+)}}/g) || []).map(
+          (match) => match.slice(2, -2).trim()
+        );
+
+        // 过滤 attrs,只保留在 content 中出现的 ID
+        data.attrs = data.attrs.filter((item) => contentIds.includes(item.id));
+
+        this.coms = [data];
+      } catch (error) {
+        console.error("加载模板信息失败:", error);
+      }
+    },
+    onExport(e) {
+      let _this = this;
+      _this.comIndex = -1;
+      let content = _this.$refs.tabHtml.innerHTML;
+    },
+
+    onOpenView(e) {
+      console.log("this.coms", this.coms);
+      this.showViewForm = true;
+    },
+    onClose() {
+      /* this.$router.back(); */
+      window.close();
+    },
+
+    loadArticle(e) {
+      let _this = this;
+      _this.onLoadArticle(e);
+      /* getDocumentInfo({
+        id: e,
+      }).then((res) => {
+        if (res.status != 200) return;
+        _this.onLoadArticle(res.data);
+      }); */
+    },
+
+    handleDragStart(e) {
+      let data = {};
+      if (e.target.dataset.type == "article") {
+        let index = e.target.dataset.index;
+        data = {
+          optType: e.target.dataset.type,
+          item: this.articleList[index],
+        };
+      } else {
+        let index = e.target.dataset.index;
+        let subIndex = e.target.dataset.subIndex;
+        let item = this.categoryList[index].dataList[subIndex];
+        delete item.category;
+        data = {
+          ...item,
+          optType: e.target.dataset.type,
+          key: "addNew",
+          attrs: JSON.parse(item.attrs),
+        };
+      }
+      e.dataTransfer.setData("item", JSON.stringify(data));
+      this.operation = "drag";
+    },
+
+    onRebuild(updatedComList) {
+      this.coms = [...updatedComList];
+      this.$nextTick(() => {
+        this.$forceUpdate();
+      });
+    },
+    //初始化当前用户信息
+    async initCurrentUser() {
+      let _this = this;
+      let res = await getCurrentUserInfo();
+      if (res.status != 200) return;
+      _this.userInfo = res.data;
+    },
+    //加载文档信息
+    async onLoadArticle(id) {
+      try {
+        this.loading = true;
+        const res = await getDocumentInfo({ id });
+
+        if (res.status !== 200) {
+          throw new Error("Invalid response status");
+        }
+
+        this.docAttr = {
+          id: res.data.id,
+          category_id: Number(res.data.category_id),
+          title: res.data.title,
+          content: "",
+          status: res.data.status,
+          is_template: res.data.is_template,
+          linkProduct: res.data.linkProduct,
+          links: res.data.links,
+          linkProject: res.data.linkProject,
+          projects: res.data.projects,
+        };
+
+        const templateData =
+          typeof res.data.data === "string"
+            ? JSON.parse(res.data.data)
+            : res.data.data;
+
+        const updatedComs = await Promise.all(
+          templateData.map(async (el) => {
+            /* let templateInfo = { data: {} };
+            if (this.type !== "document") {
+              templateInfo = await getTemplateInfo({ id: el.id });
+            } */
+            const formulaData = await searchFormula({ page: 1, pageSize: 999 });
+
+            const attrs = el.attrs;
+            /* typeof templateInfo.data.attrs === "string"
+                ? JSON.parse(templateInfo.data.attrs)
+                : templateInfo.data.attrs || []; // Add a fallback empty array */
+
+            attrs.forEach((item) => {
+              if (item && item.data && item.data.id) {
+                const matchedFormula = formulaData.data.dataList.find(
+                  (ite) => ite.id === item.data.id
+                );
+                if (matchedFormula) {
+                  item.data = matchedFormula;
+                  item.formula = matchedFormula.formula;
+                }
+              }
+            });
+
+            return { ...el /* , ...templateInfo.data, attrs */ };
+          })
+        );
+
+        this.coms = updatedComs;
+        console.log("coms", updatedComs);
+
+        if (this.type === "document" && this.articleId !== undefined) {
+          document.title = `${this.docAttr.title}-${settings.title}`;
+        }
+
+        this.uptadeSearch();
+      } catch (error) {
+        console.error("Error in onLoadArticle:", error);
+      } finally {
+        this.loading = false;
+      }
+    },
+    /* 加载模版信息 */
+    async onTemplateInfo(id) {
+      try {
+        this.loading = true;
+        // 获取文档信息
+        const res = await getDocumentInfo({ id });
+
+        if (res.status !== 200 || !res.data) {
+          throw new Error("无效的响应或未收到数据");
+        }
+
+        // 设置文档属性
+        this.docAttr = {
+          id: this.docAttr.id,
+          category_id: Number(this.docAttr.category_id),
+          title: res.data.title,
+          content: "",
+          status: res.data.status,
+          is_template: res.data.is_template,
+          linkProduct: res.data.linkProduct || [],
+          links: res.data.links || "",
+          linkProject: res.data.linkProject || [],
+          projects: res.data.projects || "",
+        };
+
+        // 解析文档数据
+        const documentData = res.data.data ? JSON.parse(res.data.data) : [];
+
+        const updatedComs = await Promise.all(
+          documentData.map(async (docEl) => {
+            if (!docEl || !docEl.id) {
+              console.warn("无效的文档项:", docEl);
+              return null;
+            }
+
+            // 获取模板信息
+            const templateInfo = await getTemplateInfo({ id: docEl.id });
+            // 获取公式数据
+            const formulaData = await searchFormula({ page: 1, pageSize: 999 });
+
+            // 使用模板的 content 作为基础
+            let content = templateInfo.data.content;
+            // 使用模板的 attrs 作为基础,但排除 Directory 类型
+            let attrs = templateInfo.data.attrs
+              ? JSON.parse(templateInfo.data.attrs).filter(
+                  (attr) => attr.type !== "Directory"
+                )
+              : [];
+
+            // 从文档数据中提取所有 Directory 类型的属性
+            const docDirectories = docEl.attrs.filter(
+              (attr) => attr.type === "Directory"
+            );
+
+            // 将文档中的 Directory 属性添加到 attrs 中
+            attrs = [...attrs, ...docDirectories];
+
+            // 解析 content 中的所有项
+            const contentItems = (content.match(/{{([^}]+)}}/g) || []).map(
+              (match) => match.slice(2, -2)
+            );
+            console.log("contentItems", contentItems);
+            // 确保 content 中引用的所有 Directory 项都存在于 attrs 中
+            contentItems.forEach((itemId) => {
+              if (
+                itemId.startsWith("Directory_") &&
+                !attrs.some((attr) => attr.id === itemId)
+              ) {
+                const [, moduleIndex, level] = itemId.split("_");
+                attrs.push({
+                  type: "Directory",
+                  id: itemId,
+                  name: "目录信息",
+                  intro: "目录信息",
+                  number: "",
+                  content: "",
+                  level: parseInt(level),
+                  moduleIndex: parseInt(moduleIndex),
+                });
+              }
+            });
+
+            // 更新公式数据
+            attrs = attrs.map((item) => {
+              if (item.type === "formual" && item.data && item.data.id) {
+                const matchedFormula = formulaData.data.dataList.find(
+                  (ite) => ite.id === item.data.id
+                );
+                if (matchedFormula) {
+                  return {
+                    ...item,
+                    data: matchedFormula,
+                    formula: matchedFormula.formula,
+                  };
+                }
+              }
+              return item;
+            });
+
+            // 返回更新后的数据,以模板信息为主,但 Directory 项完全使用文档信息
+            return { ...templateInfo.data, attrs, content };
+          })
+        );
+
+        // 过滤掉无效的组件
+        this.coms = updatedComs.filter(Boolean);
+        console.log("组件", this.coms);
+        console.log("模版", this.docAttr);
+        if (this.articleId !== undefined) {
+          document.title = `${this.docAttr.title}-${settings.title}`;
+        }
+        this.uptadeSearch();
+      } catch (error) {
+        console.error("加载模板信息时出错:", error);
+        this.$message.error("加载模板信息时出错,请稍后重试");
+      } finally {
+        this.loading = false;
+      }
+    },
+    onRemove(index) {
+      this.coms.splice(index, 1);
+    },
+    /* 目录信息 */
+    onCatalogIndex(e) {
+      this.catalogIndex = e;
+      if (
+        (this.docuComIndex =
+          3 || this.comIndex == 9998 || this.AttributeIndex == 1)
+      ) {
+        this.comIndex = 0;
+        this.AttributeIndex = 0;
+        this.docuComIndex = 0;
+      }
+      this.$nextTick(() => {
+        this.$forceUpdate();
+      });
+    },
+    onSetActiveIndexs(e) {
+      this.docuComIndex = e;
+      if (
+        this.AttributeIndex == 1 ||
+        this.comIndex == 9998 ||
+        this.catalogIndex == 9997
+      ) {
+        this.catalogIndex = 0;
+        this.AttributeIndex = 0;
+        this.comIndex = 0;
+      }
+    },
+    //设置当前索引
+    onSetActiveIndex(e) {
+      this.comIndex = e;
+      if (
+        this.AttributeIndex == 1 ||
+        this.docuComIndex == 3 ||
+        this.catalogIndex == 9997
+      ) {
+        this.catalogIndex = 0;
+
+        this.docuComIndex = 0;
+      }
+    },
+    /* 属性变量 */
+    onAttributeIndex(e) {
+      this.AttributeIndex = e;
+      if (
+        (this.docuComIndex =
+          3 || this.comIndex == 9998 || this.catalogIndex == 9997)
+      ) {
+        this.catalogIndex = 0;
+        this.comIndex = 0;
+        this.docuComIndex = 0;
+      }
+    },
+    /* 更新 */
+    renew() {
+      let _this = this;
+      _this.docVisible = true;
+      this.docForm.title = _this.docAttr.title;
+      this.docForm.category_id = _this.docAttr.category_id;
+    },
+    /* 保存 */
+    onSaveAs(e) {
+      let _this = this;
+      if (_this.coms.length <= 0) {
+        _this.$alert("请填写内容");
+        return;
+      }
+      _this.docAttr.id = "";
+      _this.docVisible = true;
+      this.docForm.title = _this.docAttr.title;
+      this.docForm.category_id = _this.docAttr.category_id;
+    },
+    //保存文档
+    onSave(e) {
+      let _this = this;
+      if (_this.coms.length <= 0) {
+        _this.$alert("增加组件");
+        return;
+      }
+      if (_this.docAttr.category_id == "" || _this.docAttr.category_id <= 0) {
+        _this.$alert("请选择文档分类");
+        return;
+      }
+      if (_this.docAttr.title == "") {
+        _this.$alert("请填写文档标题");
+        return;
+      }
+
+      _this.docAttr.links = JSON.stringify(_this.docAttr.linkProduct);
+      _this.docAttr.projects = JSON.stringify(_this.docAttr.linkProject);
+      _this.docAttr.data = JSON.stringify(_this.coms);
+      _this.docAttr.is_template = 0;
+      _this.docAttr.user_name = _this.$store.state.user.name;
+      _this.docAttr.user_id = _this.$store.state.user.id;
+      if (_this.docAttr.id > 0) {
+        updateDocument(_this.docAttr).then((res) => {
+          if (res.status != 200) return; //更新文档
+          _this.docAttr.id = res.data;
+          _this.$alert("文档更新成功");
+          _this.docVisible = false;
+          _this.docForm = {
+            title: "",
+            category_id: "",
+          };
+          _this.searchArticle();
+        });
+      } else {
+        createDocument(_this.docAttr).then((res) => {
+          if (res.status != 200) return; //保存文档
+          _this.docAttr.id = res.data;
+          _this.articleId = res.data;
+          _this.$alert("文档保存成功");
+          _this.docVisible = false;
+          _this.docForm = {
+            title: "",
+            category_id: "",
+          };
+          _this.searchArticle();
+        });
+      }
+    },
+    /*更新  */
+    onUpload() {
+      let _this = this;
+      if (_this.coms.length <= 0) {
+        _this.$alert("增加组件");
+        return;
+      }
+      /* if (_this.docAttr.category_id == "" || _this.docAttr.category_id <= 0) {
+        _this.$alert("请选择分类");
+        return;
+      } */
+      if (_this.docAttr.title == "") {
+        _this.$alert("请填写模版标题");
+        return;
+      }
+      _this.docAttr.links = JSON.stringify(_this.docAttr.linkProduct);
+      _this.docAttr.projects = JSON.stringify(_this.docAttr.linkProject);
+      _this.docAttr.data = JSON.stringify(_this.coms);
+      _this.docAttr.is_template = 1;
+      _this.docAttr.id = this.$route.query.articleId;
+      _this.docAttr.user_name = _this.$store.state.user.name;
+      _this.docAttr.user_id = _this.$store.state.user.id;
+      updateDocument(_this.docAttr).then((res) => {
+        if (res.status != 200) return; //更新文档
+        _this.docAttr.id = res.data;
+        _this.$alert("模版更新成功");
+        _this.searchArticle();
+      });
+    },
+    /* 保存并更新模版 */
+    onSaveUpload(e) {
+      let _this = this;
+      if (_this.coms.length <= 0) {
+        _this.$alert("增加组件");
+        return;
+      }
+      /* if (_this.docAttr.category_id == "" || _this.docAttr.category_id <= 0) {
+        _this.$alert("请选择分类");
+        return;
+      } */
+      if (_this.docAttr.title == "") {
+        _this.$alert("请填写模版标题");
+        return;
+      }
+
+      _this.docAttr.links = JSON.stringify(_this.docAttr.linkProduct);
+      _this.docAttr.projects = JSON.stringify(_this.docAttr.linkProject);
+      _this.docAttr.data = JSON.stringify(_this.coms);
+      _this.docAttr.is_template = 1;
+      _this.docAttr.user_name = _this.$store.state.user.name;
+      _this.docAttr.user_id = _this.$store.state.user.id;
+      /* if (_this.docAttr.id > 0) {
+        updateDocument(_this.docAttr).then((res) => {
+          if (res.status != 200) return; //更新文档
+          _this.docAttr.id = res.data;
+          _this.$alert("模版更新成功");
+          _this.searchArticle();
+        });
+      } else { */
+      createDocument(_this.docAttr).then((res) => {
+        if (res.status != 200) return; //保存文档
+        _this.docAttr.id = res.data;
+        _this.articleId = res.data;
+        _this.$alert("模版保存成功");
+        _this.searchArticle();
+      });
+      /*   } */
+    },
+
+    //设置组件值
+    onSetComs(e) {
+      let _this = this;
+      _this.coms = [...e];
+    },
+    onAddLayer(cateIndex, dataIndex) {
+      let _this = this;
+      let item = _this.categoryList[cateIndex].dataList[dataIndex];
+      let data = {
+        ...item,
+        attrs: JSON.parse(item.attrs),
+      };
+      delete data.category;
+      _this.coms.push(data);
+      _this.comIndex = _this.coms.length - 1;
+    },
+    //处理插入事件
+    onInsert(e) {
+      let _this = this;
+      switch (e.key) {
+        case "article": //插入图文
+          _this.insertArticle();
+          break;
+        case "table": //插入图表
+          _this.insertTable();
+          break;
+        case "sourceData": //插入原数据
+          _this.insertSourceData(e);
+          break;
+        case "formual": //插入公式
+          _this.insertFormual(e);
+          break;
+        case "constant": //插入常量
+          _this.insertConstant(e);
+          break;
+        case "variable": //插入变量
+          _this.insertVariable(e);
+          break;
+        case "pager": //插入分页符
+          _this.insertPager();
+          break;
+        case "attr":
+          this.insertProductAttr();
+          break;
+        case "Directory": //插入主题
+          _this.insertDirectory(e);
+          break;
+        case "addNew": //插入新记录
+          _this.insertNew(e);
+          break;
+        case "InsertNull":
+          _this.insertVal();
+          break;
+        case "ai": //插入ai
+          _this.insertAI(e);
+          break;
+      }
+    },
+    /* 插入空值 */
+    insertVal() {
+      let _this = this;
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+      let com = _this.coms[_this.comIndex];
+      let uniqueId = "val" + Date.now(); // 使用时间戳创建唯一ID
+      let data = {
+        type: "variableNull",
+        id: uniqueId,
+        dataId: "",
+        name: "非变量",
+        intro: "",
+        content: "请输入",
+        data: "",
+      };
+      com.attrs.push(data);
+      if (com.type == "TextArea") {
+        //如果是图文,更新内容
+        _this.insertCmd = {
+          content: "{{" + data.id + "}}",
+        };
+      } else {
+        //仅对图表中插入有效
+        if (_this.comIndex >= 0) {
+          _this.setTableData(data.id);
+        }
+      }
+    },
+    /* 插入ai */
+    insertAI(e) {
+      let _this = this;
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+      let com = _this.coms[_this.comIndex];
+
+      let data = {
+        type: "ai",
+        id: "ai" + (com.attrs.length + 1),
+        dataId: "",
+        name: "AI",
+        intro: "插入AI",
+        content: e.content.result,
+        search: e.content.searchTerm,
+      };
+      com.attrs.push(data);
+      if (com.type == "TextArea") {
+        //如果是图文,更新内容
+        _this.insertCmd = {
+          content: "{{" + data.id + "}}",
+        };
+      }
+    },
+    insertProductAttr() {
+      if (this.docAttr.linkProduct.length <= 0) {
+        this.$alert("请选择文档关联商品信息");
+        return false;
+      }
+      this.showProductAttr = true;
+    },
+    //插入目录
+    insertDirectory(e) {
+      let _this = this;
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+
+      let com = _this.coms[_this.comIndex];
+      let uniqueId = `Directory_${_this.comIndex}_${com.attrs.length + 1}`;
+
+      // 获取当前层级
+      let currentLevel = e.content.level || 1;
+
+      // 生成带有模块索引和层级的内容
+      let { number, content } = this.generateNumberedContent(
+        _this.comIndex,
+        currentLevel
+      );
+
+      let data = {
+        type: "Directory",
+        id: uniqueId,
+        name: "目录信息",
+        intro: "目录信息",
+        number: number,
+        content: content,
+        level: currentLevel,
+        moduleIndex: _this.comIndex,
+      };
+
+      // 使用 $set 来确保响应式更新
+      this.$set(com.attrs, com.attrs.length, data);
+
+      // 强制更新视图
+      this.$forceUpdate();
+
+      // 使用 nextTick 确保 DOM 更新后再检查
+      this.$nextTick(() => {
+        if (!com.attrs[com.attrs.length - 1].content) {
+          this.$set(com.attrs[com.attrs.length - 1], "content", content);
+          this.$forceUpdate();
+        }
+      });
+
+      // 使用新的唯一 ID 在插入命令中
+      _this.insertCmd = {
+        content: `{{${uniqueId}}}`,
+      };
+    },
+
+    generateNumberedContent(moduleIndex, level) {
+      let number = this.generateLevelPrefix(moduleIndex, level);
+      let content = this.coms[moduleIndex].type === "TextArea" ? "" : "";
+
+      return { number, content };
+    },
+
+    generateLevelPrefix(moduleIndex, level) {
+      let prefix = [];
+      let currentModule = this.coms[moduleIndex];
+
+      // 获取当前模块中已存在的目录项
+      let existingDirectories = currentModule.attrs.filter(
+        (attr) => attr.type === "Directory"
+      );
+
+      // 计算当前层级的序号
+      let currentLevelCount =
+        existingDirectories.filter((dir) => dir.level === level).length + 1;
+
+      // 构建多级序号
+      for (let i = 1; i <= level; i++) {
+        if (i === 1) {
+          prefix.push(moduleIndex + 1);
+        } else if (i === level) {
+          prefix.push(currentLevelCount);
+        } else {
+          // 对于中间层级,找到最近的上级目录项
+          let parentLevel = existingDirectories
+            .filter((dir) => dir.level === i - 1)
+            .pop();
+          prefix.push(parentLevel ? parentLevel.number.split(".")[i - 1] : 1);
+        }
+      }
+
+      return prefix.join(".");
+    },
+
+    onInsertProductAttr(e) {
+      let _this = this;
+      this.showProductAttr = false;
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+
+      let com = _this.coms[_this.comIndex];
+
+      let data = {
+        type: "ProductAttr",
+        id: "ProductAttr" + (com.attrs.length + 1),
+        dataId: e.id,
+        name: "商品属性",
+        intor: "商品属性",
+        content: "",
+        attrs: e,
+      };
+      com.attrs.push(data);
+      _this.insertCmd = {
+        content: "{{" + data.id + "}}",
+      };
+    },
+
+    //插入新记录
+    insertNew(e) {
+      let _this = this;
+      _this.coms.push(e);
+    },
+
+    //插入分页
+    insertPager() {
+      let _this = this;
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+      let com = _this.coms[_this.comIndex];
+      let data = {
+        type: "pager",
+        id: "pager" + (com.attrs.length + 1),
+        name: "分页符",
+        intor: "强制分页",
+        content: "",
+      };
+      com.attrs.push(data);
+      if (com.type == "TextArea") {
+        //如果是图文,更新内容
+        _this.insertCmd = {
+          content: "<div>{{" + data.id + "}}</div>",
+        };
+      }
+    },
+
+    //插入图文
+    insertArticle() {
+      /*   let _this = this;
+      _this.coms.push({
+        type: "TextArea",
+        lay_id: "textArea",
+        code: "模块名",
+        name: "图文",
+        intro: "图文介绍",
+        isEdit: 2,
+        content: "请填写内容",
+        attrs: [],
+      });
+      _this.comIndex = _this.coms.length - 1; */
+      let _this = this;
+      let newArticle = {
+        type: "TextArea",
+        lay_id: "textArea",
+        code: "模块名",
+        name: "图文",
+        intro: "图文介绍",
+        isEdit: 2,
+        content: "请填写内容",
+        attrs: [],
+      };
+      _this.coms.push(newArticle);
+      _this.comIndex = _this.coms.length - 1;
+
+      // 添加一些日志来帮助调试
+      console.log("New article added:", newArticle);
+      console.log("Current coms:", _this.coms);
+      console.log("New comIndex:", _this.comIndex);
+    },
+
+    //插入表格
+    insertTable() {
+      let _this = this;
+      _this.coms.push({
+        type: "Table",
+        lay_id: "Table",
+        code: "模块名",
+        name: "图表名称",
+        intro: "图表介绍",
+        isEdit: 2,
+        tableHeader: this.initTableHeader(),
+        tableData: [{}],
+        htmlData: "",
+        attrs: [],
+      });
+      _this.comIndex = _this.coms.length - 1;
+    },
+    initTableHeader() {
+      var start = 65;
+      var data = [];
+      for (var i = start; i <= start + 25; i++) {
+        data.push(String.fromCharCode(i));
+      }
+      return data;
+    },
+    //插入变量
+    insertConstant(e) {
+      let _this = this;
+      // console.log("insertconstant");
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+      let com = _this.coms[_this.comIndex];
+      let data = {
+        type: "constant",
+        id: e.content.code + (com.attrs.length + 1),
+        dataId: e.id,
+        name: e.content.code,
+        intro: e.content.intro,
+        content: e.content.value,
+        data: e.content,
+      };
+
+      com.attrs.push(data);
+      if (com.type == "TextArea") {
+        //如果是图文,更新内容
+        _this.insertCmd = {
+          content: "{{" + data.id + "}}",
+        };
+      } else {
+        //仅对图表中插入有效
+        if (_this.comIndex >= 0) {
+          _this.setTableData(data.id);
+        }
+      }
+    },
+
+    //插入变量
+    insertVariable(e) {
+      let _this = this;
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+      let com = _this.coms[_this.comIndex];
+      let data = {
+        type: "variable",
+        id: e.content.code + (com.attrs.length + 1),
+        dataId: e.id,
+        name: e.content.code,
+        intro: e.content.intro,
+        content: e.content.value,
+        data: e.content,
+      };
+      com.attrs.push(data);
+      if (com.type == "TextArea") {
+        //如果是图文,更新内容
+        _this.insertCmd = {
+          content: "{{" + data.id + "}}",
+        };
+      } else {
+        //仅对图表中插入有效
+        if (_this.comIndex >= 0) {
+          _this.setTableData(data.id);
+        }
+      }
+    },
+    //插入原数据
+    insertSourceData(e) {
+      let _this = this;
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+      let com = _this.coms[_this.comIndex];
+      let data = {
+        type: "sourceData",
+        id: "sourceData" + (com.attrs.length + 1),
+        dataId: "",
+        name: "源数据",
+        intro: "插入源数据",
+        formula:
+          "[R][" +
+          e.content.tb +
+          "][" +
+          e.content.sheet +
+          "][" +
+          e.content.r +
+          "," +
+          e.content.c +
+          "]",
+        content: e.content.value,
+        data: e.content,
+      };
+      com.attrs.push(data);
+      if (com.type == "TextArea") {
+        //如果是图文,更新内容
+        _this.insertCmd = {
+          content: "{{" + data.id + "}}",
+        };
+      }
+    },
+    //插入公式
+    insertFormual(e) {
+      let _this = this;
+      if (_this.comIndex < 0) {
+        _this.$alert("请选择插入图层");
+        return false;
+      }
+      let com = _this.coms[_this.comIndex];
+      let data = {
+        type: "formual",
+        id: "formual" + (com.attrs.length + 1),
+        dataId: e.id,
+        name: e.content.name,
+        intro: e.content.intro,
+        formula: e.content.formula,
+        content: 0,
+        data: e.content,
+      };
+      com.attrs.push(data);
+      if (com.type == "TextArea") {
+        //如果是图文,更新内容
+        _this.insertCmd = {
+          content: "{{" + data.id + "}}",
+        };
+      } else {
+        //仅对图表中插入有效
+        if (_this.comIndex >= 0) {
+          _this.setTableData(data.id);
+        }
+      }
+    },
+
+    //获取模版分类信息
+    async initCategoryList() {
+      let _this = this;
+      /*  let res = await getAllCategory({list:1 });
+        if(res.status!=200)return;
+        let dataList=res.data;
+
+        for(var i=0;i<dataList.length;i++){
+           for (var j=0;j<dataList[i].children.length;j++){
+             let dataId=dataList[i].children[j].id;
+
+             for(var k=0;k<dataList[i].children[j].dataList.length;k++){
+               dataList[i].children[j].dataList[k].attrs=JSON.parse(dataList[i].children[j].dataList[k].attrs)
+             }
+             // dataList[i].children[j].dataList=await _this.getTemplateList(dataId)
+           }
+        }
+
+        // console.log("dataList",dataList)
+       _this.categoryList=dataList; */
+      let res = await searchTemplateCategory({
+        page: 1,
+        pageSize: 99,
+        parent_id: 0,
+        status: 5,
+      });
+      if (res.status != 200) return;
+      _this.categoryList = res.data.dataList;
+      for (var i = 0; i < _this.categoryList.length; i++) {
+        _this.categoryList[i].dataList = await _this.getTemplateList(
+          _this.categoryList[i].id
+        );
+      }
+    },
+    //获取模版列表
+
+    async getTemplateList(categoryId) {
+      let _this = this;
+      let res = await searchTemplate({
+        page: 1,
+        pageSize: 999,
+        category_id: categoryId,
+        status: 5,
+      });
+      if (res.status != 200) return [];
+
+      let dataList = res.data.dataList.map((item) => {
+        // 添加空值检查
+        if (!item.attrs) {
+          item.attrs = "[]";
+        }
+        if (!item.content) {
+          item.content = "";
+        }
+
+        try {
+          // 解析 attrs,添加错误处理
+          item.attrs =
+            typeof item.attrs === "string"
+              ? JSON.parse(item.attrs)
+              : item.attrs;
+
+          // 从 content 中提取所有 {{}} 包裹的 ID,添加空值检查
+          const contentIds = (
+            (item.content || "").match(/{{([^}]+)}}/g) || []
+          ).map((match) => match.slice(2, -2).trim());
+
+          // 确保 attrs 是数组
+          item.attrs = Array.isArray(item.attrs) ? item.attrs : [];
+
+          // 过滤 attrs,只保留在 content 中出现的 ID
+          item.attrs = item.attrs.filter(
+            (attr) => attr && attr.id && contentIds.includes(attr.id)
+          );
+        } catch (error) {
+          console.error("Error processing template item:", error);
+          item.attrs = [];
+        }
+
+        return item;
+      });
+
+      return dataList;
+    },
+
+    //获取文档列表
+    searchArticle() {
+      let _this = this;
+      pageDocument({
+        page: 1,
+        pageSize: 99,
+      }).then((res) => {
+        if (res.status != 200) return;
+        _this.articleList = res.data.dataList.filter(
+          (el) => el.is_template == 0
+        );
+        _this.templateList = res.data.dataList.filter(
+          (el) => el.is_template == 1
+        );
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./create.scss";
+
+.el-collapse,
+.el-collapse-item__wrap {
+  border: none;
+}
+.el-collapse,
+.el-collapse-item__header {
+  border: none;
+  .el-collapse-item__arrow {
+    margin: 0;
+  }
+}
+.borderless-collapse {
+  border: none;
+  ::v-deep .el-collapse-item__header,
+  ::v-deep .el-collapse-item__wrap,
+  ::v-deep .el-collapse-item__content,
+  ::v-deep .el-collapse-item {
+    border: none;
+  }
+
+  ::v-deep .el-collapse-item__wrap {
+    border-bottom: none !important;
+  }
+}
+.scrollable-container {
+  overflow-x: auto;
+  white-space: nowrap;
+  width: 350px;
+  padding-bottom: 10px;
+}
+
+.min-width-content {
+  /*   display: inline-block;
+  vertical-align: top; */
+  min-width: 150px;
+  white-space: normal;
+}
+
+/*.el-collapse-item {
+  display: inline-block;
+  vertical-align: top;
+} */
+</style>

+ 4 - 0
ui/src/views/system/document/search.scss

@@ -0,0 +1,4 @@
+.project-search{
+  padding:30px;
+  font-size:12px;
+}

+ 58 - 0
ui/src/views/system/document/search.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="project-search">
+    <dataSearch @bindSetQuery="setQuery"></dataSearch>
+    <dataList :queryForm="queryForm" :allowEdit="checkAuth('/document/update')" :allowDelete="checkAuth('/document/delete')"></dataList>
+  </div>
+
+</template>
+
+<script>
+  import { mapGetters } from 'vuex'
+import {dataSearch,dataList} from "./components"
+export default{
+  components:{
+    dataSearch,
+    dataList,
+  },
+  computed: {
+    ...mapGetters([
+  	'roleInfo',
+  	'authList'
+    ])
+  },
+
+  data(){
+    return{
+        queryForm:{
+          page:1,
+          pageSize:10,
+          title:'',
+          category_id:'',
+          status:'',
+        },
+    }
+  },
+  methods:{
+
+    checkAuth(path){
+    /* 	if(this.roleInfo.is_admin==1){
+    	  return true;
+    	} */
+    	/* let auth=this.authList.filter(o=>o.type==999 && o.path==path);
+    	if(auth.length>0){
+    	  return true;
+    	} */
+    	return true;
+    },
+
+    setQuery(e){
+      this.queryForm=e;
+    }
+
+  }
+}
+</script>
+
+<style lang="scss">
+@import './search.scss'
+</style>

+ 55 - 0
ui/src/views/system/document/temList/components/dataList.scss

@@ -0,0 +1,55 @@
+
+.data-list{
+
+
+  .btns{
+    display:flex;
+    flex-direction: row;
+  }
+
+
+
+
+  .page-info{
+    display:flex;
+    flex-direction: row;
+    margin-top:10px;
+    .opt{
+      width:50%;
+      display:flex;
+      flex-direction:row;
+      justify-content: flex-start;
+      align-items: center;
+
+      .pick{
+        width:100px;
+
+      }
+      .section{
+        margin-left:10px;
+      }
+    }
+
+    .page-data{
+
+      display:flex;
+      flex-direction: row;
+      justify-content: space-between;
+      align-items: center;
+      width:100%;
+      .page-item{
+          width:100%;
+          // border:1px solid red;
+      }
+      .page-opt{
+        width:100%;
+        // border:1px solid red;
+        display: flex;
+        justify-content: flex-end;
+      }
+    }
+
+
+  }
+
+}

+ 222 - 0
ui/src/views/system/document/temList/components/dataList.vue

@@ -0,0 +1,222 @@
+<template>
+  <div class="data-list" v-loading="loading" element-loading-text="加载中...">
+    <el-table
+      :data="dataList"
+      style="width: 100%"
+      header-row-class-name="headerBg"
+      empty-text="没有模板信息"
+    >
+      <el-table-column prop="id" label="ID" align="center" width="80" />
+      <el-table-column
+        prop="title"
+        label="模板名称"
+        align="left"
+   
+      />
+     <!--  <el-table-column
+        prop="type_name"
+        label="所属分类"
+        align="center"
+        width="150"
+      /> -->
+      <el-table-column
+        prop="create_time"
+        label="创建时间"
+        align="center"
+    
+      />
+      <el-table-column
+        prop="status"
+        label="模板状态"
+        align="center"
+      
+      >
+        <template #default="scope">
+          <div v-if="scope.row.status == 5">启用</div>
+          <div v-if="scope.row.status == 6">停用</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" :width="editWidth()"><!-- :width="editWidth()" -->
+        <template #default="scope">
+          <div class="btns">
+            <el-button
+              type="primary"
+              size="small"
+              v-if="allowEdit"
+              @click="btnEdit(scope.row.id)"
+              ><svg-icon icon-class="edit" />编辑</el-button
+            >
+
+            <el-button
+              type="danger"
+              size="small"
+              v-if="allowDelete"
+              @click="btnDelete(scope.row.id)"
+              ><svg-icon icon-class="delete" />删除</el-button
+            >
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="page-info">
+      <el-pagination
+       :currentPage="queryForm.page"
+        :page-size="queryForm.pageSize"
+        :total="recordCount"
+        :page-count="pageTotal"
+        background
+        layout="prev, pager, next"
+        @current-change="ChangePage"
+      >
+      </el-pagination>
+    </div>
+  </div>
+</template>
+
+<script>
+import { pageDocument, deleteDocument,searchList } from "@/api/document";
+export default {
+  props: {
+    queryForm: {
+      type: Object,
+      default: () => {
+        return {
+          page: 1,
+          pageSize: 10,
+          title: "",
+          category_id: "",
+          status: "",
+        };
+      },
+    },
+    allowEdit: {
+      type: Boolean,
+      default: false,
+    },
+    allowDelete: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  watch: {
+    queryForm: {
+      handler: function (val) {
+        this.search();
+      },
+      // immediate: true,//立即执行
+      deep: true,
+    },
+  },
+  data() {
+    return {
+      loading: false,
+      dialogVisible: false, //控制产品审核
+      currentDataId: 0,
+      recordCount: 0,
+      pageTotal: 1,
+      dataList: [],
+      currentData: {},
+    };
+  },
+  mounted() {
+    this.search();
+  },
+  methods: {
+    editWidth() {
+      if (this.allowDelete && this.allowEdit) {
+        return 200;
+      }
+      if (this.allowDelete || this.allowEdit) {
+        return 120;
+      }
+      return 100;
+    },
+
+    onFocusVal(e) {
+      let _this = this;
+      _this.currentDataId = e.target.dataset.id;
+    },
+
+    onChangeVal(e) {
+      let _this = this;
+      let par = {
+        id: _this.currentDataId,
+        val: e,
+      };
+    },
+
+    btnDelete(e) {
+      let _this = this;
+      let par = {
+        id: e,
+      };
+      _this
+        .$confirm("您是否确认删除该记录?", "提示", {
+          confirmButtonText: "确认",
+          cancelButtonText: "取消",
+          type: "warning",
+        })
+        .then((res) => {
+          // console.log("AAA")
+          deleteDocument(par).then((res) => {
+            _this.search();
+          });
+        })
+        .catch(() => {});
+    },
+
+    searchData() {
+      let _this = this;
+      _this.dialogVisible = false;
+      _this.search();
+    },
+
+    //编辑
+    btnEdit(e) {
+      let _this = this;
+      let a = document.createElement("a");
+      a.href = "#/document/create?articleId=" + e;
+      a.target = "_blank";
+      a.click();
+
+       //_this.$router.push({path:"/document/create",query:{articleId:e}})
+    },
+
+    handleClose() {
+      let _this = this;
+      _this.currentData = {};
+      _this.dialogVisible = false;
+    },
+
+    //搜索
+    search() {
+      let _this = this;
+      _this.loading = true; // Set loading to true before API call
+      searchList(_this.queryForm)
+        .then((res) => {
+          if (!res) return;
+          _this.dataList = res.data.dataList/* .filter(
+            (el) => el.is_template == 1
+          ); */
+          _this.recordCount = res.data.totalRecord;
+          _this.pageTotal = res.data.pageTotal;
+          _this.loading = false; // Set loading to false after data is processed
+        })
+        .catch(() => {
+          _this.loading = false; // Set loading to false if there's an error
+        });
+    },
+    //修改分页
+    ChangePage(e) {
+      let _this = this;
+      _this.queryForm.page = e;
+      _this.search();
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+@import "./dataList.scss";
+</style>

+ 3 - 0
ui/src/views/system/document/temList/components/dataSearch.scss

@@ -0,0 +1,3 @@
+.data-search{ 
+  padding:20px 10px 0px 10px;
+}

+ 115 - 0
ui/src/views/system/document/temList/components/dataSearch.vue

@@ -0,0 +1,115 @@
+<template>
+  <div class="data-search">
+    <el-form :inline="true" :model="queryForm" class="demo-form-inline">
+
+
+        <el-form-item label="文档标题:">
+          <el-input v-model="queryForm.title" placeholder="请填写文档标题"></el-input>
+        </el-form-item>
+
+        <el-form-item label="所属分类:">
+          <el-select v-model="queryForm.category_id" clearable class="m-2" placeholder="请选择分类" size="large">
+              <el-option
+                v-for="item in categoryList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id"
+              />
+            </el-select>
+        </el-form-item>
+        <el-form-item label="文档状态:">
+          <el-select v-model="queryForm.status" class="m-2" placeholder="请选择文档状态" size="large">
+              <el-option
+                v-for="item in statusOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" @click="onSubmit">  <svg-icon icon-class="search"/> 搜索</el-button>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="addDocument" class="el-icon-circle-plus-outline"> 创建模版</el-button>
+        </el-form-item>
+      </el-form>
+
+
+  </div>
+</template>
+
+<script>
+import {searchDocumentCategory} from "@/api/document";
+export default{
+  props:{
+    queryForm:{
+        type:Object,
+        default:()=>{
+          return {
+             page:1,
+             pageSize:10,
+             title:'',
+             category_id:'',
+             status:'',
+          }
+        }
+    },
+  },
+  data(){
+    return{
+        statusOptions:[
+          {
+            value:'',
+            label:"请选择状态"
+          },
+          {
+            value:5,
+            label:'启用'
+          },
+          {
+            value:6,
+            label:"停用"
+          },
+        ],
+        dialogVisible:false,
+        categoryList:[],
+    }
+  },
+  created() {
+    this.initCategoryList();
+  },
+  methods:{
+
+
+    initCategoryList(){
+      let _this=this;
+      searchDocumentCategory({page:1,pageSize:999,status:5}).then(res=>{
+        if(res.status!=200)return;
+        _this.categoryList=res.data.dataList;
+      })
+    },
+    onCreate(e){
+      this.dialogVisible=true;
+    },
+    //搜索
+    onSubmit(){
+      this.$emit("bindSetQuery",this.$props.queryForm);
+    },
+    /* 创建模版 */
+    addDocument(){
+      let _this = this;
+      let a = document.createElement("a");
+      a.href = "#/document/create"; // 使用 hash
+      a.target = "_blank";
+      a.click();
+      //this.$router.push('/document/create');
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@import "./dataSearch.scss"
+</style>

+ 3 - 0
ui/src/views/system/document/temList/components/index.js

@@ -0,0 +1,3 @@
+export { default as dataSearch } from './dataSearch'
+export { default as dataList } from './dataList'
+ 

+ 60 - 0
ui/src/views/system/document/temList/temList.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="project-search">
+    <dataSearch @bindSetQuery="setQuery"></dataSearch>
+    <dataList
+      :queryForm="queryForm"
+      :allowEdit="checkAuth('/document/update')"
+      :allowDelete="checkAuth('/document/delete')"
+    ></dataList>
+  </div>
+</template>
+  
+  <script>
+import { mapGetters } from "vuex";
+import { dataSearch, dataList } from "./components";
+export default {
+  components: {
+    dataSearch,
+    dataList,
+  },
+  computed: {
+    ...mapGetters(["roleInfo", "authList"]),
+  },
+
+  data() {
+    return {
+      queryForm: {
+        page: 1,
+        pageSize: 10,
+        title: "",
+        category_id: "",
+        status: "",
+      },
+    };
+  },
+  methods: {
+    checkAuth(path) {
+      if (this.roleInfo.is_admin == 1) {
+        return true;
+      }
+      /* let auth=this.authList.filter(o=>o.type==999 && o.path==path);
+          if(auth.length>0){
+            return true;
+          } */
+      return true;
+    },
+
+    setQuery(e) {
+      this.queryForm = e;
+    },
+  },
+};
+</script>
+  
+  <style lang="scss">
+.project-search {
+  padding: 30px;
+  font-size: 12px;
+}
+</style>
+  

+ 68 - 0
ui/src/views/system/document/test.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="content-wrap" style="padding-top:20px;">
+        <button @click="exportToDocx">导出</button>
+  </div>
+</template>
+
+<script>
+import { createDocument } from 'html-to-docx';
+import {exportDocument} from "@/api/document";
+import fs from 'fs';
+  export default {
+    data() {
+      return {};
+    },
+    methods: {
+
+
+          exportToDocx() {
+              exportDocument({id:1}).then(res=>{
+                  console.log("res",res);
+              });
+
+         },
+
+    }
+  };
+</script>
+
+<style>
+  .content-wrap {
+    display: flex;
+    flex-direction: row;
+  }
+  .component-list {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    padding: 10px;
+    height: 200px;
+    width: 100px;
+    border: 1px solid lightblue;
+  }
+  .content {
+    height: 200px;
+    flex: 1;
+    border: 1px solid lightblue;
+    padding: 10px;
+    margin-left: 10px;
+    position: relative;
+  }
+  .list {
+    height: fit-content;
+    width: fit-content;
+    border: 1px solid grey;
+    cursor: grab;
+    margin-bottom: 10px;
+    text-align: center;
+    color: #333;
+    padding: 2px 5px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .content .list {
+    position: absolute;
+  }
+</style>

+ 1 - 1
ui/vue.config.js

@@ -36,7 +36,7 @@ module.exports = {
     proxy: {
       // detail: https://cli.vuejs.org/config/#devserver-proxy
       [process.env.VUE_APP_BASE_API]: {
-        target: `http://localhost:8080`,
+        target: `http://120.46.190.49:8080`,
         changeOrigin: true,
         pathRewrite: {
           ['^' + process.env.VUE_APP_BASE_API]: ''