Browse Source

page config

zhoucg 1 year ago
parent
commit
4fa23c409e

+ 11 - 0
components.d.ts

@@ -11,6 +11,8 @@ declare module 'vue' {
     Area: typeof import('./src/components/yvan-editor/Area.vue')['default']
     CanvasRuler: typeof import('./src/components/sketch-ruler/canvasRuler/canvasRuler.vue')['default']
     ComponentAdjuster: typeof import('./src/components/yvan-editor/ComponentAdjuster.vue')['default']
+    ComponentBand: typeof import('./src/components/yvan-editor/ComponentBand.vue')['default']
+    ComponentLayout: typeof import('./src/components/yvan-editor/ComponentLayout.vue')['default']
     Context: typeof import('./src/components/yvan-context/components/Context.vue')['default']
     ContextGroup: typeof import('./src/components/yvan-context/components/ContextGroup.vue')['default']
     ContextItem: typeof import('./src/components/yvan-context/components/ContextItem.vue')['default']
@@ -21,6 +23,7 @@ declare module 'vue' {
     Editor: typeof import('./src/components/yvan-editor/Editor.vue')['default']
     EditorCoordinate: typeof import('./src/components/yvan-editor/EditorCoordinate.vue')['default']
     EditorLine: typeof import('./src/components/yvan-editor/EditorLine.vue')['default']
+    EditorMenu: typeof import('./src/components/yvan-editor/EditorMenu.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
@@ -50,6 +53,8 @@ declare module 'vue' {
     Line: typeof import('./src/components/sketch-ruler/line.vue')['default']
     PagePalette: typeof import('./src/components/PagePalette.vue')['default']
     PageToc: typeof import('./src/components/PageToc.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
     RoyCircle: typeof import('./src/components/elements/RoyCircle.vue')['default']
     RoyComplexTable: typeof import('./src/components/elements/YvanTable/RoyComplexTable.vue')['default']
     RoyGroup: typeof import('./src/components/elements/RoyGroup.vue')['default']
@@ -79,6 +84,8 @@ declare module 'vue' {
     YvanCircleProps: typeof import('./src/components/elements/YvanCircleProps.vue')['default']
     YvanComplexTable: typeof import('./src/components/elements/yvan-table/YvanComplexTable.vue')['default']
     YvanComplexTableProps: typeof import('./src/components/elements/yvan-table/YvanComplexTableProps.vue')['default']
+    YvanFieldText: typeof import('./src/components/elements/YvanFieldText.vue')['default']
+    YvanFieldTextProps: typeof import('./src/components/elements/YvanFieldTextProps.vue')['default']
     YvanGroup: typeof import('./src/components/elements/YvanGroup.vue')['default']
     YvanImage: typeof import('./src/components/elements/YvanImage.vue')['default']
     YvanImageProps: typeof import('./src/components/elements/YvanImageProps.vue')['default']
@@ -94,6 +101,8 @@ declare module 'vue' {
     YvanPrintDesignerLeft: typeof import('./src/components/YvanPrintDesignerLeft.vue')['default']
     YvanPrintDesignerLeft_o: typeof import('./src/components/YvanPrintDesignerLeft_o.vue')['default']
     YvanPrintDesignerLeftElement: typeof import('./src/components/YvanPrintDesignerLeftElement.vue')['default']
+    YvanPrintDesignerLeftLayout: typeof import('./src/components/YvanPrintDesignerLeftLayout.vue')['default']
+    YvanPrintDesignerLeftLoyout: typeof import('./src/components/YvanPrintDesignerLeftLoyout.vue')['default']
     YvanPrintDesignerLeftStructure: typeof import('./src/components/YvanPrintDesignerLeftStructure.vue')['default']
     YvanPrintDesignerMain: typeof import('./src/components/YvanPrintDesignerMain.vue')['default']
     YvanPrintDesignerProps: typeof import('./src/components/YvanPrintDesignerProps.vue')['default']
@@ -117,6 +126,8 @@ declare module 'vue' {
     YvanSimpleTextInTable: typeof import('./src/components/elements/yvan-table/YvanSimpleTextInTable.vue')['default']
     YvanSimpleTextProps: typeof import('./src/components/elements/YvanSimpleTextProps.vue')['default']
     YvanTextEditor: typeof import('./src/components/yvan-ui/YvanTextEditor.vue')['default']
+    YvanTextField: typeof import('./src/components/elements/YvanTextField.vue')['default']
+    YvanTextFieldProps: typeof import('./src/components/elements/YvanTextFieldProps.vue')['default']
     YvanTextInTable: typeof import('./src/components/elements/yvan-table/YvanTextInTable.vue')['default']
   }
 }

+ 0 - 6
package-lock.json

@@ -19,7 +19,6 @@
         "nanoid": "^4.0.2",
         "pinia": "^2.1.4",
         "qrcode.vue": "^3.4.1",
-        "qrcodejs2": "^0.0.2",
         "vue": "^3.2.47",
         "vue3-styled-components": "^1.2.1",
         "vuedraggable": "^2.24.3",
@@ -2844,11 +2843,6 @@
         "vue": "^3.0.0"
       }
     },
-    "node_modules/qrcodejs2": {
-      "version": "0.0.2",
-      "resolved": "https://registry.npmmirror.com/qrcodejs2/-/qrcodejs2-0.0.2.tgz",
-      "integrity": "sha512-+Y4HA+cb6qUzdgvI3KML8GYpMFwB24dFwzMkS/yXq6hwtUGNUnZQdUnksrV1XGMc2mid5ROw5SAuY9XhI3ValA=="
-    },
     "node_modules/queue-microtask": {
       "version": "1.2.3",
       "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",

+ 5 - 0
package.json

@@ -14,6 +14,9 @@
     "@element-plus/icons-vue": "^2.1.0",
     "@wangeditor/editor": "^5.1.22",
     "@wangeditor/editor-for-vue": "^5.1.12",
+    "axios": "^0.27.2",
+    "dayjs": "^1.11.5",
+    "lodash": "^4.17.21",
     "big.js": "^6.2.1",
     "element-plus": "^2.3.7",
     "jsbarcode": "^3.11.5",
@@ -22,6 +25,7 @@
     "pinia": "^2.1.4",
     "qrcode.vue": "^3.4.1",
     "vue": "^3.2.47",
+    "vue-router": "^4.1.5",
     "vue3-styled-components": "^1.2.1",
     "vuedraggable": "^2.24.3",
     "yarn": "^1.22.19"
@@ -30,6 +34,7 @@
     "@types/node": "^20.3.1",
     "@vicons/fa": "^0.12.0",
     "@vitejs/plugin-vue": "^4.1.0",
+    "lodash": "^4.17.21",
     "less": "^4.1.3",
     "less-loader": "^11.1.3",
     "typescript": "^5.0.2",

+ 174 - 0
src/App_o.vue

@@ -0,0 +1,174 @@
+<template>
+  <div class="wrapper">
+    <SketchRule
+      :cornerActive="true"
+      :height="482"
+      :horLineArr="lines.h"
+      :lang="lang"
+      :scale="scale"
+      :shadow="shadow"
+      :startX="startX"
+      :startY="startY"
+      :thick="thick"
+      :verLineArr="lines.v"
+      :width="582"
+      @handleLine="handleLine"
+      @onCornerClick="handleCornerClick"
+    >
+    </SketchRule>
+    <div id="screens" ref="screensRef" @scroll="handleScroll" @wheel="handleWheel">
+      <div ref="containerRef" class="screen-container">
+        <div id="canvas" :style="canvasStyle" />
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import SketchRule from '@/components/sketch-ruler/sketchRuler.vue'
+
+const rectWidth = 160
+const rectHeight = 200
+export default {
+  data() {
+    return {
+      scale: 1, //658813476562495, //1,
+      startX: 0,
+      startY: 0,
+      lines: {
+        h: [100, 200],
+        v: [100, 200]
+      },
+      thick: 20,
+      lang: 'zh-CN', // 中英文
+      isShowRuler: true, // 显示标尺
+      isShowReferLine: true // 显示参考线
+    }
+  },
+  components: {
+    SketchRule
+  },
+  computed: {
+    shadow() {
+      return {
+        x: 0,
+        y: 0,
+        width: rectWidth,
+        height: rectHeight
+      }
+    },
+    canvasStyle() {
+      return {
+        width: rectWidth,
+        height: rectHeight,
+        transform: `scale(${this.scale})`
+      }
+    }
+  },
+  methods: {
+    handleLine(lines) {
+      this.lines = lines
+    },
+    handleCornerClick() {
+      return
+    },
+    handleScroll() {
+      const screensRect = document.querySelector('#screens').getBoundingClientRect()
+      const canvasRect = document.querySelector('#canvas').getBoundingClientRect()
+
+      // 标尺开始的刻度
+      const startX = (screensRect.left + this.thick - canvasRect.left) / this.scale
+      const startY = (screensRect.top + this.thick - canvasRect.top) / this.scale
+
+      this.startX = startX
+      this.startY = startY
+    },
+    // 控制缩放值
+    handleWheel(e) {
+      if (e.ctrlKey || e.metaKey) {
+        e.preventDefault()
+        const nextScale = parseFloat(Math.max(0.2, this.scale - e.deltaY / 500).toFixed(2))
+        this.scale = nextScale
+      }
+      this.$nextTick(() => {
+        this.handleScroll()
+      })
+    }
+  },
+  mounted() {
+    // 滚动居中
+    this.$refs.screensRef.scrollLeft =
+      this.$refs.containerRef.getBoundingClientRect().width / 2 - 300 // 300 = #screens.width / 2
+  }
+}
+</script>
+<style lang="less">
+body {
+  margin: 0;
+  padding: 0;
+  font-family: sans-serif;
+  overflow: hidden;
+}
+
+body * {
+  box-sizing: border-box;
+  user-select: none;
+}
+
+.wrapper {
+  background-color: #f5f5f5;
+  position: absolute;
+  top: 100px;
+  left: 100px;
+  width: 600px;
+  height: 500px;
+  border: 1px solid #dadadc;
+}
+
+#screens {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+}
+
+.screen-container {
+  position: absolute;
+  width: 5000px;
+  height: 3000px;
+}
+
+.scale-value {
+  position: absolute;
+  left: 0;
+  bottom: 100%;
+}
+
+.button {
+  position: absolute;
+  left: 100px;
+  bottom: 100%;
+}
+
+.button-ch {
+  position: absolute;
+  left: 200px;
+  bottom: 100%;
+}
+
+.button-en {
+  position: absolute;
+  left: 230px;
+  bottom: 100%;
+}
+
+#canvas {
+  position: absolute;
+  top: 80px;
+  left: 50%;
+  margin-left: -80px;
+  width: 160px;
+  height: 200px;
+  background: lightblue;
+  transform-origin: 50% 0;
+}
+</style>

+ 33 - 8
src/components/YvanPrintDesigner.vue

@@ -38,9 +38,7 @@
       </yvan-print-aside>
       <yvan-print-main class="yvan-print-designer-main">
         <yvan-print-designer-main :show-right="defaultExpendAside">
-          <template v-slot:yvan-print-designer-toolbar-slot>-->
-            <slot name="yvan-print-designer-toolbar-slot"></slot>
-          </template>
+          <slot name="yvan-print-designer-toolbar-slot"></slot>
         </yvan-print-designer-main>
       </yvan-print-main>
       <yvan-print-aside class="yvan-print-designer-props" width="auto">
@@ -51,8 +49,8 @@
 </template>
 
 <script>
-import {mapActions} from "pinia";
-import {modeStore} from "@/store";
+import {mapState, mapActions} from "pinia";
+import {globalStore, modeStore} from "@/store";
 import YvanPrintContainer from "@/components/yvan-ui/YvanPrintContainer.vue";
 import YvanPrintHeader from "@/components/yvan-ui/YvanPrintHeader.vue";
 import YvanPrintAside from "@/components/yvan-ui/YvanPrintAside.vue";
@@ -117,15 +115,15 @@ export default {
         },
         {
           code: 'exportTemplate',
-          name: '保存模板为文件',
+          name: '下载模板',
           icon: 'fa fa-cloud-download',
           event: this.exportJSON
         },
         {
           code: 'importTemplate',
-          name: '从模板文件导入',
+          name: '上传云端',
           icon: 'fa fa-cloud-upload',
-          event: this.importFile
+          event: this.uploadTemplate
         },
         {
           code: 'logout',
@@ -140,6 +138,15 @@ export default {
     isNightMode() {
       return false;
     },
+    ...mapState(globalStore, {
+      pageConfig: (state) => state.pageConfig,
+      componentData: (state) => state.componentData,
+      elementBands: (state) => state.elementBands,
+      dataSource: (state) => state.dataSource
+    }),
+    ...mapState(modeStore, {
+      isNightMode: (state) => state.isNightMode,
+    }),
     headIconConfig() {
       return this.headIcons.filter((item) => {
         return (
@@ -170,6 +177,24 @@ export default {
 
     },
     exportJSON() {
+      let eleA = document.createElement('a')
+      eleA.download = `${this.pageConfig.title + "_" + Date.now()}.json`
+      eleA.style.display = 'none'
+      const saveData = {
+        pageConfig: this.pageConfig,
+        dataSource: this.dataSource
+      }
+      this.elementBands.forEach((value, key) => {
+        saveData[key] = value
+      })
+      const blob = new Blob([JSON.stringify(saveData)])
+      eleA.href = URL.createObjectURL(blob)
+      document.body.appendChild(eleA)
+      eleA.click()
+      document.body.removeChild(eleA)
+      // window['system'].toast('导出成功!', 'success')
+    },
+    uploadTemplate() {
 
     }
   },

+ 3 - 3
src/components/YvanPrintDesignerLeft.vue

@@ -25,14 +25,14 @@
           <span>页面结构</span>
         </span>
       </template>
-      <YvanPrintDesignerLeftStructure/>
+      <YvanPrintDesignerLeftLayout/>
     </el-tab-pane>
   </el-tabs>
 </template>
 
 <script>
 import YvanPrintDesignerLeftElement from "@/components/YvanPrintDesignerLeftElement.vue";
-import YvanPrintDesignerLeftStructure from "@/components/YvanPrintDesignerLeftStructure.vue";
+import YvanPrintDesignerLeftLayout from "@/components/YvanPrintDesignerLeftLayout.vue";
 import YvanPrintDatasource from "@/components/YvanPrintDataSource.vue";
 
 export default {
@@ -40,7 +40,7 @@ export default {
   components: {
     YvanPrintDatasource,
     YvanPrintDesignerLeftElement,
-    YvanPrintDesignerLeftStructure
+    YvanPrintDesignerLeftLayout
   },
   data() {
     return {

+ 88 - 21
src/components/YvanPrintDesignerLeftStructure.vue

@@ -1,32 +1,31 @@
 <template>
   <section :style="asideStyle" class="yvan-print-designer-left-structure__main">
-<!--    <div class="yvan-print-designer-left-structure__title">-->
-<!--      <i class="fa fa-sitemap vsm&#45;&#45;icon"></i>-->
-<!--      <span>页面结构</span>-->
-<!--    </div>-->
     <div class="yvan-print-designer-left-structure__content">
       <yvan-print-main class="yvan-page-toc">
-        <div v-if="componentData.length">
+        <div class="yvan-page-element" v-for="(elementBand, idx) in elementBands" :key="idx">
+          <div class="yvan-element-band__list" @click="handleElementBandClick">
+            <i class="fa fa-list-ul" style="padding-right: 5px"></i>
+            <span class="yvan-element-band_label">{{ elementBand.name }}</span>
+            <div class="yvan-element-band__buttons">
+              <span class="fa fa-plus-square" @click.stop="addElementBand"></span>
+              <span class="fa fa-trash" @click.stop="deleteElementBand"></span>
+            </div>
+          </div>
           <div
-              v-for="(item, index) in componentData"
-              :key="index"
-              :class="{ activated: transformIndex(index) === curComponentIndex }"
+              v-for="(item, idx) in componentData"
+              :key="idx"
+              :class="{ activated: transformIndex(idx) === curComponentIndex }"
               class="yvan-page-toc__list"
-              @click="onClick(transformIndex(index))"
-          >
-            <i :class="getComponent(index).icon" style="padding-right: 5px"></i>
-            <span class="yvan-page-toc__label">{{ getComponent(index).label }}</span>
+              @click="onClick(transformIndex(idx))">
+            <i :class="getComponent(idx).icon" style="padding-right: 5px"></i>
+            <span class="yvan-page-toc__label">{{ getComponent(idx).label }}</span>
             <div class="yvan-page-toc__buttons">
-              <span class="fa fa-arrow-up" @click="upComponent(transformIndex(index))"></span>
-              <span class="fa fa-arrow-down" @click="downComponent(transformIndex(index))"></span>
-              <span class="fa fa-trash" @click="deleteComponent(transformIndex(index))"></span>
+              <span class="fa fa-arrow-up" @click.stop="upComponent(transformIndex(idx))"></span>
+              <span class="fa fa-arrow-down" @click.stop="downComponent(transformIndex(idx))"></span>
+              <span class="fa fa-trash" @click.stop="deleteComponent(transformIndex(idx))"></span>
             </div>
           </div>
         </div>
-        <div v-else class="yvan-page-structure__empty animate__animated animate__headShake">
-          <component is="exclamation-circle-outlined" class="yvan-page-structure__empty_icon"/>
-          <div>当前没有组件,请通过拖拽添加组件</div>
-        </div>
       </yvan-print-main>
     </div>
   </section>
@@ -36,6 +35,7 @@
 import {mapState} from "pinia";
 import {globalStore} from "@/store";
 import commonMixin from '@/mixin/commonMixin'
+import {elementBands} from "@/components/config/globalConfig"
 import YvanPrintMain from "@/components/yvan-ui/YvanPrintMain.vue";
 
 export default {
@@ -47,7 +47,7 @@ export default {
   data() {
     return {
       isNightMode: false,
-
+      elementBands,
     }
   },
   props: {
@@ -103,6 +103,15 @@ export default {
         component: this.componentData[index],
         index
       })
+    },
+    handleElementBandClick() {
+      console.log('>>> click')
+    },
+    addElementBand() {
+      console.log(">>> add")
+    },
+    deleteElementBand() {
+      console.log(">>>> delete")
     }
   },
   mounted() {
@@ -156,6 +165,64 @@ export default {
         }
       }
 
+      .yvan-element-band__list {
+        height: 30px;
+        cursor: pointer;
+        text-align: center;
+        color: var(--yvan-text-color-primary);
+        display: flex;
+        align-items: center;
+        font-size: 14px;
+        padding: 0 5px;
+        position: relative;
+        user-select: none;
+
+        .yvan-element-band__label {
+          text-align: left;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          width: 50%;
+          overflow: hidden;
+        }
+
+        i {
+          font-size: 12px;
+        }
+
+        &.activated {
+          color: var(--yvan-color-primary);
+          background: var(--yvan-color-primary-light-7);
+        }
+
+        &:active {
+          cursor: pointer;
+        }
+
+        &:hover {
+          background-color: rgba(200, 200, 200, 0.2);
+          color: var(--yvan-text-color-primary);
+
+          .yvan-element-band__buttons {
+            display: block;
+          }
+        }
+
+        .yvan-element-band__buttons {
+          position: absolute;
+          right: 10px;
+          display: none;
+
+          span {
+            cursor: pointer;
+            padding: 5px;
+
+            &:hover {
+              background: var(--yvan-menu-item-hover-fill);
+            }
+          }
+        }
+      }
+
       .yvan-page-toc__list {
         height: 30px;
         cursor: pointer;
@@ -164,7 +231,7 @@ export default {
         display: flex;
         align-items: center;
         font-size: 12px;
-        padding: 0 10px;
+        padding: 0 25px;
         position: relative;
         user-select: none;
 

+ 7 - 54
src/components/YvanPrintDesignerMain.vue

@@ -1,26 +1,16 @@
 <template>
   <section class="height-all">
     <yvan-print-toolbar>
-      <template v-slot:yvan-designer-toolbar-slot>-->
-        <slot name="yvan-designer-toolbar-slot"></slot>
-      </template>
+      <slot name="yvan-designer-toolbar-slot"></slot>
     </yvan-print-toolbar>
-    <div
-      @dragover="handleDragOver"
-      @drop="handleDrop"
-      @mousedown="handleMouseDown"
-      @mouseup="deselectCurComponent"
-    >
-      <yvan-print-designer-editor :show-right="showRight" />
-    </div>
+    <yvan-print-designer-editor :show-right="showRight"/>
   </section>
 </template>
 
-<script>
+<script lang="js">
 import {mapState} from "pinia";
 import {globalStore} from "@/store";
-import system from '@/utils/system'
-import commonMixin from '@/mixin/commonMixin'
+import commonMixin from '@/mixin/commonMixin';
 import YvanPrintToolbar from "@/components/yvan-ui/YvanPrintToolbar.vue";
 import YvanPrintDesignerEditor from "@/components/yvan-editor/Editor.vue";
 
@@ -46,48 +36,11 @@ export default {
     })
   },
   methods: {
-    initMounted() {},
-    handleDrop(e) {
-      e.preventDefault()
-      e.stopPropagation()
-
-      const componentCode = e.dataTransfer.getData('componentCode')
-      const rectInfo = document.querySelector('#designer-page').getBoundingClientRect()
-      if (componentCode) {
-        const component = this.deepCopy(this.findElementByCode(componentCode))
-        component.style = component.style ?? {}
-        component.style.top = e.clientY - rectInfo.y
-        component.style.left = e.clientX - rectInfo.x
-        component.id = this.getUuid()
-        component.label = `${component.name}-${component.id}`
-        globalStore().addComponent({ component })
-        globalStore().recordSnapshot()
-      } else {
-        system.toast('拖拽元素非页面组件,此次拖拽无效', 'info')
-      }
-    },
-
-    handleDragOver(e) {
-      e.preventDefault()
-      e.dataTransfer.dropEffect = 'copy'
-    },
-
-    handleMouseDown(e) {
-      e.stopPropagation()
-      globalStore().setClickComponentStatus(false)
-      globalStore().setInEditorStatus(false)
-    },
-
-    deselectCurComponent() {
-      if (!this.isClickComponent) {
-        globalStore().setCurComponent({
-          component: null,
-          index: null
-        })
-      }
+    initMounted() {
     }
   },
-  created() {},
+  created() {
+  },
   mounted() {
     this.initMounted()
   },

+ 18 - 20
src/components/YvanPrintPageFormat.vue

@@ -7,15 +7,15 @@
       <el-form-item label="模板编码">
         <el-input v-model="pageConfig.code" placeholder="请输入模板编码"/>
       </el-form-item>
-      <el-form-item label="纸张大小">
-        <el-select v-model="pageConfig.pageSize" class="m-2" placeholder="请选择纸张大小" clearable>
-          <el-option
-              v-for="pageSize in pageSizeList"
-              :key="pageSize.name"
-              :label="pageSize.name"
-              :value="pageSize.name"/>
-        </el-select>
-      </el-form-item>
+      <!--      <el-form-item label="纸张大小">-->
+      <!--        <el-select v-model="pageConfig.pageSize" class="m-2" placeholder="请选择纸张大小" clearable>-->
+      <!--          <el-option-->
+      <!--              v-for="pageSize in pageSizeList"-->
+      <!--              :key="pageSize.name"-->
+      <!--              :label="pageSize.name"-->
+      <!--              :value="pageSize.name"/>-->
+      <!--        </el-select>-->
+      <!--      </el-form-item>-->
       <el-form-item label="纸张方向">
         <el-radio-group v-model="pageConfig.pageDirection" @change="changePageDirection">
           <el-radio-button
@@ -34,10 +34,10 @@
         (mm)
       </el-form-item>
       <el-form-item label="外边距" class="yvan-print-page-format__margins">
-        <el-input-number v-model="pageConfig.margins.top" placeholder="上" controls-position="right"/>
-        <el-input-number v-model="pageConfig.margins.right" placeholder="右" controls-position="right"/>
-        <el-input-number v-model="pageConfig.margins.bottom" placeholder="下" controls-position="right"/>
-        <el-input-number v-model="pageConfig.margins.left" placeholder="左" controls-position="right"/>
+        <el-input-number v-model="pageConfig.topMargin" placeholder="上" controls-position="right"/>
+        <el-input-number v-model="pageConfig.rightMargin" placeholder="右" controls-position="right"/>
+        <el-input-number v-model="pageConfig.bottomMargin" placeholder="下" controls-position="right"/>
+        <el-input-number v-model="pageConfig.leftMargin" placeholder="左" controls-position="right"/>
       </el-form-item>
     </el-form>
   </div>
@@ -46,7 +46,7 @@
 <script setup lang="ts">
 import {storeToRefs} from "pinia";
 import {globalStore} from "@/store";
-import {pageSizeList, pageDirectionList} from '@/components/config/globalConfig'
+import {pageDirectionList} from '@/components/config/globalConfig'
 
 const {pageConfig} = storeToRefs(globalStore())
 
@@ -56,17 +56,15 @@ const changePageDirection = (newPageDirection) => {
 
 const changePageWidth = (newPageWidth) => {
   globalStore().setPageSize({
-    pageSize: pageConfig.value.pageSize,
-    w: newPageWidth,
-    h: pageConfig.value.pageHeight
+    pageWidth: newPageWidth,
+    pageHeight: pageConfig.value.pageHeight
   })
 }
 
 const changePageHeight = (newPageHeight) => {
   globalStore().setPageSize({
-    pageSize: pageConfig.value.pageSize,
-    w: pageConfig.value.pageWidth,
-    h: newPageHeight
+    pageWidth: pageConfig.value.pageWidth,
+    pageHeight: newPageHeight
   })
 }
 </script>

+ 115 - 65
src/components/config/globalConfig.ts

@@ -1,12 +1,10 @@
-/** 纸张大小 */
-export const pageSizeList = [
-    {name: "A4", width: 210, height: 297}
-]
+import {globalStore} from "@/store";
+
 /** 纸张方向 */
 export const pageDirectionList = [
     // vertical or horizontal
-    {label: "垂直方向", value: "v"},
-    {label: "水平方向", value: "h"},
+    {label: "垂直方向", value: "Vertical"},
+    {label: "水平方向", value: "Horizontal"},
 ]
 /** 字体 */
 export const fontList = [
@@ -46,14 +44,27 @@ export const commonAttr = {
     isLock: false // 是否锁定组件
 }
 
+export const elementBands = [
+    {name: 'Title', code: 'title', height: 30, isDefaultShow: true},
+    {name: 'Page Header', code: 'pageHeader', height: 30, isDefaultShow: true},
+    {name: 'Column Header', code: 'columnHeader', height: 30, isDefaultShow: true},
+    {name: 'Detail', code: 'detail', height: 30, isDefaultShow: true},
+    {name: 'Column Footer', code: 'columnFooter', height: 30, isDefaultShow: true},
+    {name: 'Page Footer', code: 'pageFooter', height: 30, isDefaultShow: true},
+    {name: 'Last Page Footer', code: 'lastPageFooter', height: 30, isDefaultShow: false},
+    {name: 'Summary', code: 'summary', height: 30, isDefaultShow: false},
+]
+
 // 元素组件
 export const elementList = [
     {
         icon: 'fa fa-file-text-o',
         code: 'text',
-        name: '文本',
+        name: '静态文本',
         component: 'YvanSimpleText',
-        propValue: '单击编辑文本',
+        expression: '双击编辑文本',
+        width: 200,
+        height: 50,
         style: {
             color: '#000000',
             padding: '0',
@@ -78,25 +89,56 @@ export const elementList = [
         groupStyle: {}
     },
     {
-        icon: 'fa fa-text-height',
-        code: 'long-text',
-        name: '长文本',
-        component: 'YvanRichText',
-        propValue:
-            '<p><span style="font-size: 16pt;">双击</span><span style="color: rgb(255, 255, 255); background-color: #009688; font-size: 16pt; font-family: 仿宋;">编辑</span><span style="font-size: 16pt;">文本</span></p>',
+        icon: 'fa fa-file-text',
+        code: 'text',
+        name: '字段文本',
+        component: 'YvanTextField',
+        expression: '双击编辑文本',
+        width: 200,
+        height: 50,
         style: {
-            width: 500,
-            height: 200,
-            fontSize: 12,
-            background: '#FFFFFF',
-            rotate: 0,
+            color: '#000000',
             padding: '0',
+            margin: '0',
+            fontFamily: 'Microsoft_YaHei',
+            lineHeight: 1,
+            letterSpacing: '0',
             borderWidth: 0,
+            borderRadius: 0,
+            borderColor: '#212121',
+            borderType: 'none',
             borderPosition: [],
-            borderColor: '#FFFFFF',
+            width: 200,
+            height: 50,
+            fontSize: 10,
+            background: '#FFFFFF',
+            rotate: 0,
+            justifyContent: 'flex-start',
+            alignItems: 'flex-start',
+            textStyle: [],
         },
         groupStyle: {}
     },
+    // {
+    //     icon: 'fa fa-text-height',
+    //     code: 'long-text',
+    //     name: '长文本',
+    //     component: 'YvanRichText',
+    //     propValue:
+    //         '<p><span style="font-size: 16pt;">双击</span><span style="color: rgb(255, 255, 255); background-color: #009688; font-size: 16pt; font-family: 仿宋;">编辑</span><span style="font-size: 16pt;">文本</span></p>',
+    //     style: {
+    //         width: 500,
+    //         height: 200,
+    //         fontSize: 12,
+    //         background: '#FFFFFF',
+    //         rotate: 0,
+    //         padding: '0',
+    //         borderWidth: 0,
+    //         borderPosition: [],
+    //         borderColor: '#FFFFFF',
+    //     },
+    //     groupStyle: {}
+    // },
     {
         icon: 'fa fa-th',
         code: 'table',
@@ -114,35 +156,36 @@ export const elementList = [
         },
         groupStyle: {}
     },
-    {
-        icon: 'fa fa-table',
-        code: 'complex-table',
-        name: '复杂表格',
-        component: 'YvanComplexTable',
-        propValue: {},
-        showPrefix: true,
-        showHead: true,
-        showFoot: true,
-        showSuffix: true,
-        style: {
-            width: 'auto',
-            height: 'auto',
-            fontSize: 12,
-            fontFamily: 'default',
-            color: '#000000',
-            background: '#FFFFFF',
-            borderWidth: 2,
-            borderColor: '#212121',
-            rotate: 0
-        },
-        groupStyle: {}
-    },
+    // {
+    //     icon: 'fa fa-table',
+    //     code: 'complex-table',
+    //     name: '复杂表格',
+    //     component: 'YvanComplexTable',
+    //     propValue: {},
+    //     showPrefix: true,
+    //     showHead: true,
+    //     showFoot: true,
+    //     showSuffix: true,
+    //     style: {
+    //         width: 'auto',
+    //         height: 'auto',
+    //         fontSize: 12,
+    //         fontFamily: 'default',
+    //         color: '#000000',
+    //         background: '#FFFFFF',
+    //         borderWidth: 2,
+    //         borderColor: '#212121',
+    //         rotate: 0
+    //     },
+    //     groupStyle: {}
+    // },
     {
         icon: 'fa fa-minus',
         code: 'line',
         name: '直线',
         component: 'YvanLine',
-        propValue: '',
+        width: 200,
+        height: 1,
         style: {
             width: 200,
             height: 1,
@@ -156,7 +199,8 @@ export const elementList = [
         code: 'rectangle',
         name: '矩形',
         component: 'YvanRect',
-        propValue: '',
+        width: 200,
+        height: 200,
         style: {
             borderRadius: 0,
             borderWidth: 1,
@@ -169,30 +213,33 @@ export const elementList = [
         },
         groupStyle: {}
     },
-    {
-        icon: 'fa fa-circle-thin',
-        code: 'circle',
-        name: '圆形',
-        component: 'YvanCircle',
-        propValue: '',
-        style: {
-            borderWidth: 1,
-            borderColor: '#212121',
-            borderType: 'solid',
-            width: 200,
-            height: 200,
-            background: '#ffffff',
-            rotate: 0
-        },
-        groupStyle: {}
-    },
+    // {
+    //     icon: 'fa fa-circle-thin',
+    //     code: 'circle',
+    //     name: '圆形',
+    //     component: 'YvanCircle',
+    //     propValue: '',
+    //     style: {
+    //         borderWidth: 1,
+    //         borderColor: '#212121',
+    //         borderType: 'solid',
+    //         width: 200,
+    //         height: 200,
+    //         background: '#ffffff',
+    //         rotate: 0
+    //     },
+    //     groupStyle: {}
+    // },
     {
         icon: 'fa fa-image',
         code: 'image',
         name: '图片',
         component: 'YvanImage',
         src: '/default_image.png',
+        expression: '',
         title: '默认图片',
+        width: 200,
+        height: 137,
         style: {
             borderRadius: 0,
             borderWidth: 0,
@@ -209,6 +256,9 @@ export const elementList = [
         code: 'barcode',
         name: '条形码',
         component: 'YvanBarcode',
+        width: 200,
+        height: 145,
+        expression: '',
         config: {
             textValue: '1234567890',
             format: 'CODE128',
@@ -233,6 +283,9 @@ export const elementList = [
         code: 'qrcode',
         name: '二维码',
         component: 'YvanQrcode',
+        width: 100,
+        height: 100,
+        expression: '',
         config: {
             textValue: '1234567890',
         },
@@ -241,8 +294,6 @@ export const elementList = [
             borderWidth: 0,
             borderColor: '#212121',
             borderRadius: 0,
-            width: 100,
-            height: 100,
             background: '#fff',
             rotate: 0
         },
@@ -344,7 +395,6 @@ export const functionList = [
     },
 ]
 
-
 export const componentList = [
     {code: 'element', title: '元素组件', type: 'element', elementList: elementList},
     {code: 'function', title: '系统函数组件', type: 'function', elementList: functionList},

+ 7 - 9
src/components/elements/YvanSimpleText.vue

@@ -1,12 +1,11 @@
 <template>
   <div
       class="yvan-simple-text"
-      style="width: 100%; height: 100%"
       @click="setEdit"
       @contextmenu="setEdit"
       @dragenter="handleDragEnter"
       @dragleave="handleDragLeave"
-      @drop="handleDrop"
+      @drop.stop.prevent="handleDrop"
   >
     <StyledSimpleText
         ref="editArea"
@@ -28,7 +27,7 @@
   </div>
 </template>
 
-<script>
+<script lang="js">
 import {mapState} from 'pinia';
 import {globalStore} from "@/store";
 import system from '@/utils/system';
@@ -37,7 +36,7 @@ import {StyledSimpleText} from '@/components/elements/style';
 import YvanSimpleTextProps from "@/components/elements/YvanSimpleTextProps.vue";
 
 export default {
-  name: 'YvanSimpleText',
+  name: 'YvanTextField',
   mixins: [commonMixin],
   components: {
     StyledSimpleText,
@@ -103,11 +102,7 @@ export default {
       selection.addRange(range)
     },
     handleDrop(e) {
-      e.preventDefault()
-      e.stopPropagation()
-
       this.dragOver = false
-
       const index = e.dataTransfer.getData('datasource-index')
       if (index) {
         console.log(' >>> ', index, this.dataSource)
@@ -186,8 +181,11 @@ export default {
 }
 </script>
 
-<style lang="less">
+<style lang="less" scoped>
 .yvan-simple-text {
+  width: 100%;
+  height: 100%;
+
   .edit-area {
     width: 100%;
     height: 100%;

+ 206 - 0
src/components/elements/YvanTextField.vue

@@ -0,0 +1,206 @@
+<template>
+  <div
+      class="yvan-simple-text"
+      @click="setEdit"
+      @contextmenu="setEdit"
+      @dragenter="handleDragEnter"
+      @dragleave="handleDragLeave"
+      @drop.stop.prevent="handleDrop"
+  >
+    <StyledSimpleText
+        ref="editArea"
+        :class="{
+          'can-edit': canEdit,
+          'is-drag-over': dragOver
+        }"
+        :contenteditable="canEdit"
+        class="edit-area"
+        tabindex="0"
+        v-bind="style"
+        @blur="handleBlur"
+        @keydown="handleKeyDown"
+        @mousedown="handleMouseDown"
+        @paste="clearStyle"
+    >
+      <div class="yvan-simple-text-inner" v-html="propValue"></div>
+    </StyledSimpleText>
+  </div>
+</template>
+
+<script lang="js">
+import {mapState} from 'pinia';
+import {globalStore} from "@/store";
+import system from '@/utils/system';
+import commonMixin from '@/mixin/commonMixin';
+import {StyledSimpleText} from '@/components/elements/style';
+import YvanTextFieldProps from "@/components/elements/YvanTextFieldProps.vue";
+
+export default {
+  name: 'YvanSimpleText',
+  mixins: [commonMixin],
+  components: {
+    StyledSimpleText,
+    YvanTextFieldProps
+  },
+  props: {
+    element: {
+      type: Object,
+      default: () => {
+      }
+    },
+    propValue: {
+      type: String,
+      default: ''
+    },
+    bindValue: {
+      type: Object,
+      default: null
+    }
+  },
+  computed: {
+    ...mapState(globalStore, {
+      curComponent: (state) => state.curComponent,
+      dataSource: (state) => state.dataSource
+    }),
+    style() {
+      return this.element.style || {}
+    },
+    mayEdit() {
+      return this.curComponent?.id === this.element?.id
+    }
+  },
+  data() {
+    return {
+      canEdit: false,
+      dragOver: false
+    }
+  },
+  methods: {
+    initMounted() {
+    },
+    setEdit() {
+      if (this.canEdit) {
+        return
+      }
+      if (this.bindValue) {
+        return
+      }
+      if (this.element.isLock) {
+        return
+      }
+      this.canEdit = true
+      // 全选
+      this.selectText(this.$refs.editArea.$el)
+      // 聚焦
+      this.$refs.editArea.$el.focus()
+    },
+    selectText(element) {
+      const selection = window.getSelection()
+      const range = document.createRange()
+      range.selectNodeContents(element)
+      selection.removeAllRanges()
+      selection.addRange(range)
+    },
+    handleDrop(e) {
+      this.dragOver = false
+      const index = e.dataTransfer.getData('datasource-index')
+      if (index) {
+        console.log(' >>> ', index, this.dataSource)
+        // let bindingDataSource = this.dataSource[index]
+        let bindingDataSource = {
+          title: '当前页',
+          code: 'pageNumber',
+          fieldName: 'page_number',
+          typeName: 'String'
+        }
+        if (bindingDataSource) {
+          globalStore().setBindValue({
+            id: this.element.id,
+            bindValue: bindingDataSource
+          })
+          globalStore().setPropValue({
+            id: this.element.id,
+            propValue: `<span class="yvan-binding-value">[绑定:${bindingDataSource.title}]</span>`
+          })
+          this.canEdit = false
+        }
+      } else {
+        system.toast('拖拽元素非数据源元素,此次拖拽无效', 'info')
+      }
+    },
+    handleBlur() {
+      this.canEdit = false
+    },
+    handleMouseDown(e) {
+      if (this.canEdit) {
+        e.stopPropagation()
+      }
+    },
+    handleKeyDown(e) {
+      if (this.canEdit && [13].includes(e.keyCode)) {
+        e.preventDefault()
+        document.execCommand('insertLineBreak')
+        return false
+      }
+    },
+    clearStyle(e) {
+      this.$emit('input', this.element, e.target.innerHTML)
+    },
+    handleDragEnter() {
+      this.dragOver = true
+    },
+    handleDragLeave() {
+      this.dragOver = false
+    },
+  },
+  install(Vue) {
+    Vue.component(this.name, this)
+    Vue.component(YvanTextFieldProps.name, YvanTextFieldProps)
+  },
+  created() {
+
+  },
+  mounted() {
+    this.initMounted()
+  },
+  watch: {
+    mayEdit(newVal) {
+      if (!newVal) {
+        this.canEdit = false
+      }
+    },
+    canEdit(newVal) {
+      if (!newVal) {
+        globalStore().setPropValue({
+          id: this.element.id,
+          propValue: this.$refs.editArea.$el.innerHTML
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.yvan-simple-text {
+  width: 100%;
+  height: 100%;
+
+  .edit-area {
+    width: 100%;
+    height: 100%;
+    outline: none;
+    word-break: break-all;
+  }
+
+  .is-drag-over {
+    border: 2px solid var(--roy-color-warning);
+    background: #cccccc;
+  }
+
+  .can-edit {
+    height: 100%;
+    cursor: text;
+  }
+}
+</style>

+ 142 - 0
src/components/elements/YvanTextFieldProps.vue

@@ -0,0 +1,142 @@
+<template>
+  <section class="yvan-simple-text-props">
+    <el-form ref="form" label-position="top">
+      <el-form-item label="宽度">
+        <el-input-number v-model="activeComponent.style.width" :min="0" controls-position="right"/>
+      </el-form-item>
+      <el-form-item label="高度">
+        <el-input-number v-model="activeComponent.style.height" :min="0" controls-position="right"/>
+      </el-form-item>
+      <el-form-item label="字体">
+        <el-select v-model="activeComponent.style.fontFamily" placeholder="请选择字体" filterable>
+          <el-option v-for="font in fontList" :label="font.name" :value="font.code"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="字体颜色">
+        <el-color-picker v-model="activeComponent.style.color"/>
+      </el-form-item>
+      <el-form-item label="字体大小(pt)">
+        <el-input-number v-model="activeComponent.style.fontSize" :min="0" controls-position="right"/>
+      </el-form-item>
+      <el-form-item label="行高">
+        <el-input-number v-model="activeComponent.style.lineHeight" :min="0" controls-position="right"/>
+      </el-form-item>
+      <el-form-item label="水平布局">
+        <el-radio-group v-model="activeComponent.style.justifyContent" size="small">
+          <el-radio-button label="flex-start">
+            <component is="align-left-outlined"/>
+          </el-radio-button>
+          <el-radio-button label="center">
+            <component is="align-center-outlined"/>
+          </el-radio-button>
+          <el-radio-button label="flex-end">
+            <component is="align-right-outlined"/>
+          </el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="垂直布局">
+        <el-radio-group v-model="activeComponent.style.alignItems" size="small">
+          <el-radio-button label="flex-start">
+            <component is="vertical-align-top-outlined"/>
+          </el-radio-button>
+          <el-radio-button label="center">
+            <component is="vertical-align-middle-outlined"/>
+          </el-radio-button>
+          <el-radio-button label="flex-end">
+            <component is="vertical-align-bottom-outlined"/>
+          </el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="文字样式">
+        <el-checkbox-group v-model="activeComponent.style.textStyle" size="small">
+          <el-checkbox-button label="bold">
+            <i class="fa fa-bold"/>
+          </el-checkbox-button>
+          <el-checkbox-button label="italic">
+            <i class="fa fa-italic"/>
+          </el-checkbox-button>
+          <el-checkbox-button label="underline">
+            <i class="fa fa-underline"/>
+          </el-checkbox-button>
+          <el-checkbox-button label="strikethrough">
+            <i class="fa fa-strikethrough"/>
+          </el-checkbox-button>
+        </el-checkbox-group>
+      </el-form-item>
+      <el-form-item label="背景颜色">
+        <el-color-picker v-model="activeComponent.style.background"/>
+      </el-form-item>
+      <el-form-item label="边框样式">
+        <el-checkbox-group v-model="activeComponent.style.borderPosition" size="small">
+          <el-checkbox-button label="top">
+            <component is="border-top-outlined"/>
+          </el-checkbox-button>
+          <el-checkbox-button label="right">
+            <component is="border-right-outlined"/>
+          </el-checkbox-button>
+          <el-checkbox-button label="bottom">
+            <component is="border-bottom-outlined"/>
+          </el-checkbox-button>
+          <el-checkbox-button label="left">
+            <component is="border-left-outlined"/>
+          </el-checkbox-button>
+        </el-checkbox-group>
+      </el-form-item>
+      <el-form-item label="边框类型">
+        <el-select v-model="activeComponent.style.borderType" placeholder="请选择边框类型" filterable>
+          <el-option v-for="borderType in borderTypeList" :label="borderType.label" :value="borderType.code"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="边框颜色">
+        <el-color-picker v-model="activeComponent.style.borderColor"/>
+      </el-form-item>
+      <el-form-item label="边框宽度">
+        <el-input-number v-model="activeComponent.style.borderWidth" :min="0" controls-position="right"/>
+      </el-form-item>
+      <el-form-item label="边框圆角">
+        <el-input-number v-model="activeComponent.style.borderRadius" :min="0" controls-position="right"/>
+      </el-form-item>
+      <el-form-item label="旋转角度(°)">
+        <el-input-number v-model="activeComponent.style.rotate" controls-position="right"/>
+      </el-form-item>
+    </el-form>
+  </section>
+</template>
+
+<script lang="ts">
+import {mapState} from "pinia";
+import {globalStore} from "@/store";
+import {fontList, borderTypeList} from "@/components/config/globalConfig";
+
+export default {
+  name: 'YvanSimpleTextProps',
+  data() {
+    return {
+      fontList,
+      borderTypeList
+    }
+  },
+  computed: {
+    ...mapState(globalStore, {
+      activeComponent: (state) => {
+        console.log("activeComponent => ", state.curComponent)
+        return state.curComponent
+      }
+    }),
+  },
+}
+
+</script>
+
+<style lang="less">
+.yvan-simple-text-props {
+
+  .el-input-number {
+    width: 100%;
+  }
+
+  .el-select {
+    width: 100%;
+  }
+}
+</style>

+ 2 - 0
src/components/elements/index.js

@@ -5,6 +5,7 @@ import YvanCircle from "@/components/elements/YvanCircle.vue";
 import YvanQrcode from "@/components/elements/YvanQrcode.vue";
 import YvanBarcode from "@/components/elements/YvanBarcode.vue";
 import YvanRichText from "@/components/elements/YvanRichText.vue";
+import YvanTextField from "@/components/elements/YvanTextField.vue";
 import YvanSimpleText from "@/components/elements/YvanSimpleText.vue";
 import YvanSimpleTable from "@/components/elements/yvan-table/YvanSimpleTable.vue";
 import YvanComplexTable from "@/components/elements/yvan-table/YvanComplexTable.vue";
@@ -17,6 +18,7 @@ export const install = (Vue) => {
     YvanQrcode.install(Vue)
     YvanBarcode.install(Vue)
     YvanRichText.install(Vue)
+    YvanTextField.install(Vue)
     YvanSimpleText.install(Vue)
     YvanSimpleTable.install(Vue)
     YvanComplexTable.install(Vue)

+ 2 - 2
src/components/yvan-editor/Area.vue

@@ -31,8 +31,8 @@ export default {
 
 <style lang="less" scoped>
 .yvan-print-designer-selected-area {
-  border: 1px solid var(--roy-color-primary-light-5);
-  background: rgba(var(--roy-color-primary-rgb), 0.1);
+  border: 1px solid var(--yvan-color-primary-light-5);
+  background: rgba(var(--yvan-color-primary-rgb), 0.1);
   position: absolute;
 }
 </style>

+ 51 - 56
src/components/yvan-editor/ComponentAdjuster.vue

@@ -3,25 +3,23 @@
       ref="adjuster"
       :style="adjusterStyle"
       class="yvan-print-component-adjuster"
-      @click="selectCurComponent"
-      @mousedown="handleMouseDownOnShape"
+      @click.stop.prevent="selectCurComponent($event)"
+      @mousedown.stop="handleMouseDownOnShape($event)"
   >
     <span
         v-show="isActive && showRotate"
         class="fa fa-rotate-left yvan-print-component-adjuster__rotate"
-        @mousedown="handleRotate"
-    ></span>
-    <span v-show="element.isLock" class="fa fa-lock yvan-print-component-adjuster__lock"></span>
+        @mousedown="handleRotate"/>
+    <span v-show="element.isLock" class="fa fa-lock yvan-print-component-adjuster__lock"/>
     <span
         v-show="element.bindValue"
         class="ri-link-unlink-m yvan-print-component-adjuster__bind"
-        @click="unlinkElement"
-    ></span>
-    <span
-        :class="element.icon"
-        class="yvan-print-component-adjuster__move"
-        @mousedown="handleMouseMoveItem"
-    ></span>
+        @click="unlinkElement"/>
+    <!--<span-->
+    <!--    :class="element.icon"-->
+    <!--    class="yvan-print-component-adjuster__move"-->
+    <!--    @mousedown="handleMouseMoveItem"-->
+    <!--&gt;</span>-->
     <div
         v-for="item in isActive ? pointList : []"
         :key="item"
@@ -29,24 +27,22 @@
         :style="getPointStyle(item)"
         @mousedown="handleMouseDownOnPoint(item, $event)"
     ></div>
-    <div ref="slot" class="adjuster-container">
+    <div ref="slot" class="adjuster-container" @mousedown="handleMouseMoveItem">
       <slot></slot>
     </div>
   </div>
 </template>
 
-<script>
-import {mapState} from "pinia";
-import {globalStore} from "@/store";
-import commonMixin from '@/mixin/commonMixin'
+<script lang="js">
+import {mapActions, mapState} from "pinia";
+import {globalStore, modeStore} from "@/store";
+import eventBus from '@/utils/eventBus'
 import {mod360} from '@/utils/translate'
-import calculateComponentPositionAndSize from '@/utils/calculateComponentPositonAndSize'
+import commonMixin from '@/mixin/commonMixin'
 import {isPreventDrop} from '@/utils/html-util'
-import eventBus from '@/utils/eventBus'
+import {getEditorRect} from "@/utils/editorUtils";
+import calculateComponentPositionAndSize from '@/utils/calculateComponentPositonAndSize'
 
-/**
- * 组件小圆角
- */
 export default {
   name: 'ComponentAdjuster',
   mixins: [commonMixin],
@@ -129,7 +125,8 @@ export default {
     },
     adjusterStyle() {
       return {
-        border: this.isActive ? '0.5px dashed var(--yvan-text-color-secondary)' : undefined
+        // border: this.isActive ? '0.5px dashed var(--yvan-text-color-secondary)' : undefined
+        border: '0.5px dashed var(--yvan-text-color-secondary)'
       }
     }
   },
@@ -347,21 +344,19 @@ export default {
       }
       return false
     },
-    selectCurComponent(e) {
-      // 阻止向父组件冒泡
-      e.stopPropagation()
-      e.preventDefault()
+    selectCurComponent(event) {
+
     },
-    handleMouseDownOnShape(e) {
+    handleMouseDownOnShape(event) {
       this.$nextTick(() => eventBus.emit('componentClick'))
-
+      const elementRect = event.target.getBoundingClientRect();
+      console.log('>>> ', {top : elementRect.top, left: elementRect.right})
+      globalStore().showEditorMenu({top : elementRect.top, left: elementRect.right})
       globalStore().setInEditorStatus(true)
       globalStore().setClickComponentStatus(true)
       if (isPreventDrop(this.element.component)) {
-        e.preventDefault()
+        event.preventDefault()
       }
-
-      e.stopPropagation()
       globalStore().setCurComponent({
         component: this.element,
         index: this.index
@@ -369,8 +364,8 @@ export default {
       if (this.element.isLock) {
         return
       }
-
-      this.cursors = this.getCursor() // 根据旋转角度获取光标位置
+      // 根据旋转角度获取光标位置
+      this.cursors = this.getCursor()
     },
     handleMouseMoveItem(e) {
       if (!this.isActive) {
@@ -481,28 +476,28 @@ export default {
     z-index: 9;
   }
 
-  .yvan-print-component-adjuster__move {
-    position: absolute;
-    top: 0;
-    left: -25px;
-    z-index: 9;
-    padding: 2px;
-    font-size: 15px;
-    border-radius: 2px;
-    font-weight: 100;
-    cursor: move;
-    background: var(--yvan-color-primary-light-7);
-    color: #aaaaaa;
-
-    &:hover {
-      color: #212121;
-    }
-
-    &:active {
-      color: #212121;
-      background: var(--yvan-color-primary-light-3);
-    }
-  }
+  //.yvan-print-component-adjuster__move {
+  //  position: absolute;
+  //  top: 0;
+  //  left: -25px;
+  //  z-index: 9;
+  //  padding: 2px;
+  //  font-size: 15px;
+  //  border-radius: 2px;
+  //  font-weight: 100;
+  //  cursor: move;
+  //  background: var(--yvan-color-primary-light-7);
+  //  color: #aaaaaa;
+  //
+  //  &:hover {
+  //    color: #212121;
+  //  }
+  //
+  //  &:active {
+  //    color: #212121;
+  //    background: var(--yvan-color-primary-light-3);
+  //  }
+  //}
 
   .yvan-print-component-adjuster__bind {
     position: absolute;

+ 307 - 0
src/components/yvan-editor/ComponentBand.vue

@@ -0,0 +1,307 @@
+<template>
+  <div class="yvan-component-band">
+    <div
+        ref="topLineRef"
+        v-if="isFirst"
+        name="topLine"
+        class="yvan-component-layout__top"
+        :style="getTopLineStyle"
+        @mouseenter.stop="handleLineMouseEnter($event)"
+        @mousedown.stop="handleLineMouseDown($event)"/>
+
+    <div
+        class="yvan-component-layout__main"
+        :style="getBandStyle"
+        @dragover.prevent="handleDragOver"
+        @drop.stop.prevent="handleDrop"
+        @mousedown.stop="handleMouseDown"
+        @mouseup="deselectCurComponent">
+
+    <span
+        class="yvan-component-layout__title"
+        :style="getBandTitleStyle">{{ bandName }}</span>
+
+      <div class="yvan-component-adjuster">
+        <ComponentAdjuster
+            v-for="(item, index) in getElements"
+            :key="item.id"
+            :bandCode="bandCode"
+            :active="item.id === (curComponent || {}).id"
+            :class="{ lock: item.isLock }"
+            :default-style="item.style"
+            :element="item"
+            :index="index"
+            :scale="scale"
+            :style="getShapeStyle(item.style)"
+        >
+          <component
+              :is="item.component"
+              :id="'yvan-print-component-' + item.id"
+              :active="item.id === (curComponent || {}).id"
+              :bind-value="item.bindValue"
+              :element="item"
+              :prop-value="item.propValue"
+              :scale="scale"
+          />
+        </ComponentAdjuster>
+      </div>
+    </div>
+
+    <div
+        ref="bottomLineRef"
+        name="bottomLine"
+        class="yvan-component-layout__bottom"
+        :style="getBottomLineStyle"
+        @mouseenter.stop="handleLineMouseEnter($event)"
+        @mousedown.stop="handleLineMouseDown($event)"/>
+
+    <!--<div-->
+    <!--    class="yvan-component-layout__mark"-->
+    <!--    :style="calcMarkStyle"-->
+    <!--/>-->
+
+  </div>
+</template>
+
+<script>
+import Big from "big.js";
+import {mapActions, mapState} from "pinia";
+import {globalStore, modeStore, ruleStore} from "@/store";
+import commonMixin from '@/mixin/commonMixin';
+import {getComponentRotatedStyle, getShapeStyle} from '@/utils/style-util.js';
+import ComponentAdjuster from '@/components/yvan-editor/ComponentAdjuster.vue';
+
+export default {
+  name: "ComponentBand",
+  mixins: [commonMixin],
+  components: {
+    ComponentAdjuster
+  },
+  props: {
+    bandName: {
+      type: String,
+      default: ""
+    },
+    bandCode: {
+      type: String,
+      default: ""
+    },
+    bandHeight: {
+      type: Number,
+      default: 40
+    },
+    usedHeight: {
+      type: Number,
+      default: 0
+    },
+    isFirst: {
+      type: Boolean,
+      default: false
+    },
+    index: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    return {
+      layoutRef: null,
+      topLineRef: null,
+      bottomLineRef: null,
+      innerBandTop: 0,
+      innerBandHeight: 0,
+      activeLine: null,
+    }
+  },
+  computed: {
+    ...mapState(globalStore, {
+      componentData: (state) => state.componentData,
+      elementBands: (state) => state.elementBands,
+      curComponent: (state) => state.curComponent,
+      editor: (state) => state.editor,
+      isClickComponent: (store) => store.isClickComponent,
+      pageConfig: (state) => state.pageConfig
+    }),
+    ...mapState(ruleStore, {
+      realScale: (state) => state.scale,
+      rectWidth: (state) => state.rectWidth,
+      rectHeight: (state) => state.rectHeight,
+      needReDrawRuler: (state) => state.needReDrawRuler,
+      showRuler: (state) => state.showRuler,
+    }),
+    scale() {
+      return new Big(this.realScale).div(new Big(5)).toNumber()
+    },
+    getElements(){
+      const elementBand = this.elementBands ? this.elementBands.get(this.bandCode) : {}
+      return elementBand?.elements;
+    },
+    getTopLineStyle() {
+      const bandTop = this.innerBandTop;
+      return `top: ${bandTop}px;`;
+    },
+    getBottomLineStyle() {
+      const bottomLineTop = Number(this.innerBandTop) + Number(this.innerBandHeight);
+      return `top: ${bottomLineTop}px;`;
+    },
+    getBandStyle() {
+      const styles = [];
+      styles.push(`top: ${this.innerBandTop}px`)
+      styles.push(`height: ${this.innerBandHeight}px`)
+      return styles.join(';');
+    },
+    getBandTitleStyle() {
+      const styles = [];
+      styles.push(`line-height: ${this.innerBandHeight}px`)
+      return styles.join(';');
+    }
+  },
+  mounted() {
+    this.layoutRef = this.$refs.layoutRef;
+    this.topLineRef = this.$refs.topLineRef;
+    this.bottomLineRef = this.$refs.bottomLineRef;
+
+    this.innerBandTop = (this.pageConfig.topMargin + this.usedHeight) * this.realScale;
+    this.innerBandHeight = this.bandHeight * this.realScale;
+    const elementBand = {name: this.bandName, code: this.bandCode, height: this.bandHeight}
+    globalStore().addElementBand(elementBand)
+  },
+  methods: {
+    ...mapActions(globalStore, ['getElementBandByCode']),
+    getShapeStyle,
+    handleDragOver(event) {
+      event.dataTransfer.dropEffect = 'copy'
+    },
+    handleDrop(event) {
+      const elementCode = event.dataTransfer.getData('componentCode')
+      const rectInfo = document.querySelector('#designer-page').getBoundingClientRect()
+      if (elementCode) {
+        const element = this.deepCopy(this.findElementByCode(elementCode))
+        element.style = element.style ?? {}
+        element.style.top = event.clientY - rectInfo.y - this.innerBandTop
+        element.style.y = event.clientY - rectInfo.y
+        element.style.left = event.clientX - rectInfo.x
+        element.style.x = element.style.left
+        element.id = this.getUuid()
+        element.label = `${element.name}-${element.id}`
+        globalStore().addElement({bandCode: this.bandCode, element: element})
+        globalStore().recordSnapshot()
+      } else {
+        window['system'].toast('拖拽元素非页面组件,此次拖拽无效', 'info')
+      }
+    },
+    handleMouseDown(event) {
+      globalStore().setClickComponentStatus(false)
+      globalStore().setInEditorStatus(false)
+    },
+    deselectCurComponent() {
+      if (!this.isClickComponent) {
+        globalStore().hideEditorMenu()
+        globalStore().setCurComponent({component: null, index: null})
+      }
+    },
+    handleLineMouseDown(event) {
+      // 获取画布位移信息
+      const editorRectInfo = this.editor.getBoundingClientRect();
+      // 获取 point 与实际拖动基准点的差值
+      // const pointRect = event.target.getBoundingClientRect();
+
+      const targetName = event.target.attributes['name'].value;
+      const pageHeight = this.pageConfig.pageHeight * this.realScale;
+      const innerBandHeight = this.innerBandHeight;
+      let changeHeight = 0;
+
+      const move = (moveEvent) => {
+        let curPosY = (moveEvent.clientY - Math.round(editorRectInfo.top)) / this.scale;
+        // const curPosX = (moveEvent.clientX - Math.round(editorRectInfo.left)) / this.scale;
+        if (curPosY <= 0) {
+          curPosY = 0
+        }
+        if (curPosY >= pageHeight) {
+          curPosY = pageHeight;
+        }
+        // const curPosition = {x: curPosX, y: curPosY}
+        if (targetName === 'bottomLine') {
+          if (curPosY <= this.innerBandTop) {
+            curPosY = this.innerBandTop + 2;
+          }
+          changeHeight = curPosY - (this.innerBandTop + innerBandHeight)
+          this.innerBandHeight = curPosY - this.innerBandTop;
+        }
+        if (targetName === 'topLine') {
+          const originBandTop = this.innerBandTop;
+          const originBottomLineTop = this.innerBandHeight + originBandTop;
+          if (curPosY >= originBottomLineTop) {
+            curPosY = originBottomLineTop - 2;
+          }
+          this.innerBandTop = curPosY;
+          this.innerBandHeight = this.innerBandHeight + originBandTop - curPosY;
+        }
+        event.target.style.top = `${curPosY}px`;
+        globalStore().changeBandHeight({
+          code: this.bandCode,
+          height: new Big(this.innerBandHeight).div(this.realScale).toNumber()
+        });
+      }
+      const up = () => {
+        document.removeEventListener('mousemove', move)
+        document.removeEventListener('mouseup', up)
+        this.$emit("changeElementBandTop", {
+          currentElementBand: {},
+          changeHeight: changeHeight,
+          movePos: targetName,
+          index: this.index
+        })
+      }
+      document.addEventListener('mousemove', move)
+      document.addEventListener('mouseup', up)
+    },
+    handleLineMouseEnter(event) {
+      if (!this.isFirst) {
+        event.target.style.cursor = "n-resize";
+        return;
+      }
+      const targetName = event.target.attributes['name'].value;
+      if (this.isFirst && targetName !== 'topLine') {
+        event.target.style.cursor = "n-resize";
+      }
+    },
+    changeTop(changeHeight) {
+      const originBandTop = this.innerBandTop;
+      this.innerBandTop = originBandTop + changeHeight;
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+
+.yvan-component-band {
+
+  .yvan-component-layout__top, .yvan-component-layout__bottom {
+    position: absolute;
+    border-top: 1px dashed #ccc;
+    width: 100%;
+    z-index: 100;
+  }
+
+  .yvan-component-layout__main {
+    border-left: 3px solid var(--yvan-menu-bar-background);
+    position: absolute;
+    width: 100%;
+
+    .yvan-component-layout__title {
+      margin: auto;
+      color: #ccc;
+      user-select: none;
+    }
+
+    .yvan-component-layout__mark {
+      border-left: 3px solid var(--yvan-menu-bar-background);
+      margin-left: -3px;
+      width: 3px;
+      display: none;
+    }
+  }
+}
+</style>

+ 73 - 54
src/components/yvan-editor/Editor.vue

@@ -23,53 +23,45 @@
       <div ref="containerRef" class="screen-container">
         <div
             id="designer-page"
-            v-contextmenu="'contextmenu'"
             :style="canvasStyle"
             @contextmenu="handleContextMenu"
             @mousedown="handleMouseDown"
         >
-          <ComponentAdjuster
-              v-for="(item, index) in componentData"
-              :key="item.id"
-              :active="item.id === (curComponent || {}).id"
-              :class="{ lock: item.isLock }"
-              :default-style="item.style"
-              :element="item"
-              :index="index"
-              :scale="scale"
-              :style="getShapeStyle(item.style)"
-          >
-            <component
-                :is="item.component"
-                :id="'yvan-print-component-' + item.id"
-                :active="item.id === (curComponent || {}).id"
-                :bind-value="item.bindValue"
-                :element="item"
-                :prop-value="item.propValue"
-                :scale="scale"
-            />
-          </ComponentAdjuster>
+          <ComponentBand
+              v-for="(elementBand, idx) in getElementBands"
+              :key="idx"
+              :ref="getComponentBandRef(idx)"
+              :is-first="idx === 0"
+              :index="idx"
+              :band-name="elementBand.name"
+              :band-code="elementBand.code"
+              :band-height="elementBand.height"
+              :used-height="getUsedHeight(elementBand)"
+              @changeElementBandTop="changeElementBandTop"/>
+
           <!-- 选中区域 -->
           <Area v-show="isShowArea" :height="height" :start="start" :width="width"/>
           <!-- 标线 -->
           <EditorLine/>
           <!-- 上下边距线 -->
-          <div :style="`top: ${pageConfig?.margins?.top * realScale}px`" class="yvan-print-margin-top-line"/>
-          <div :style="`bottom: ${pageConfig?.margins?.bottom * realScale}px`" class="yvan-print-margin-bottom-line"/>
+          <!--<div :style="`top: ${pageConfig?.topMargin * realScale}px`" class="yvan-print-margin-top-line" />-->
+          <!--<div :style="`bottom: ${pageConfig?.bottomMargin * realScale}px`" class="yvan-print-margin-bottom-line"/>-->
         </div>
       </div>
     </div>
-    <Context ref="contextmenu" :theme="contextTheme">
-      <ContextItem
-          v-for="item in contextMenu"
-          :key="item.code"
-          :class="`yvan-print-context--${item.status}`"
-          @handle-click="item.event"
-      >
-        <i :class="item.icon"></i>
-        <span>{{ item.label }}</span>
-      </ContextItem>
-    </Context>
+    <!-- 菜单-->
+    <EditorMenu />
+    <!--<Context ref="contextmenu" :theme="contextTheme">-->
+    <!--  <ContextItem-->
+    <!--      v-for="item in contextMenu"-->
+    <!--      :key="item.code"-->
+    <!--      :class="`yvan-print-context&#45;&#45;${item.status}`"-->
+    <!--      @handle-click="item.event"-->
+    <!--  >-->
+    <!--    <i :class="item.icon"></i>-->
+    <!--    <span>{{ item.label }}</span>-->
+    <!--  </ContextItem>-->
+    <!--</Context>-->
     <!-- 坐标-->
     <EditorCoordinate/>
   </div>
@@ -77,36 +69,43 @@
 
 <script>
 import Big from 'big.js'
+import {mapActions, mapState} from "pinia";
+import {globalStore, ruleStore, modeStore} from "@/store";
 import eventBus from "@/utils/eventBus";
 import CONSTANT from '@/utils/constant'
 import commonMixin from '@/mixin/commonMixin'
-import {mapActions, mapState} from "pinia";
-import {globalStore, ruleStore, modeStore} from "@/store";
-import {Context, ContextItem, directive} from '@/components/yvan-context'
-import {getComponentRotatedStyle, getShapeStyle} from '@/utils/style-util.js'
-import {isPreventDrop} from '@/utils/html-util.js'
+import {isPreventDrop} from '@/utils/html-util'
+import {elementBands} from "@/components/config/globalConfig"
+// import {Context, ContextItem, directive} from '@/components/yvan-context'
+import {getComponentRotatedStyle, getShapeStyle} from '@/utils/style-util'
 import Area from '@/components/yvan-editor/Area.vue'
 import SketchRuler from '../sketch-ruler/sketchRuler.vue'
+import EditorMenu from "@/components/yvan-editor/EditorMenu.vue";
 import EditorLine from '@/components/yvan-editor/EditorLine.vue'
+import ComponentBand from '@/components/yvan-editor/ComponentBand.vue'
 import EditorCoordinate from '@/components/yvan-editor/EditorCoordinate.vue'
 import ComponentAdjuster from '@/components/yvan-editor/ComponentAdjuster.vue'
 
 const {MIN_SCALE, MAX_SCALE} = CONSTANT
 
+let elementUsedHeight = 0;
+
 export default {
   name: 'yvan-print-designer-editor',
-  directives: {
-    contextmenu: directive
-  },
+  // directives: {
+  //   contextmenu: directive
+  // },
   mixins: [commonMixin],
   components: {
-    EditorCoordinate,
+    Area,
+    EditorMenu,
     EditorLine,
     SketchRuler,
-    Context,
-    ContextItem,
+    // Context,
+    // ContextItem,
+    ComponentBand,
+    EditorCoordinate,
     ComponentAdjuster,
-    Area
   },
   props: {
     showRight: {
@@ -306,6 +305,9 @@ export default {
     },
     panelWidth() {
       return this.showRight ? 'width: calc(100% - 315px - 315px);' : 'width: calc(100% - 95px - 95px);'
+    },
+    getElementBands() {
+      return elementBands.filter(band => band.isDefaultShow);
     }
   },
   methods: {
@@ -390,10 +392,7 @@ export default {
 
       // 根据选中区域和区域中每个组件的位移信息来创建 Group 组件
       // 要遍历选择区域的每个组件,获取它们的 left top right bottom 信息来进行比较
-      let top = Infinity,
-          left = Infinity
-      let right = -Infinity,
-          bottom = -Infinity
+      let top = Infinity, left = Infinity, right = -Infinity, bottom = -Infinity
       areaData.forEach((component) => {
         let style = {}
         if (component.component === 'Group') {
@@ -502,7 +501,28 @@ export default {
       this.$nextTick(() => {
         this.handleScroll()
       })
-    }
+    },
+    getComponentBandRef(idx) {
+      // return `componentBandRef_${idx}`;
+      return `componentBandRef`;
+    },
+    getUsedHeight(elementBand) {
+      const resultUsedHeight = elementUsedHeight;
+      elementUsedHeight = resultUsedHeight + elementBand.height;
+      return resultUsedHeight;
+    },
+    changeElementBandTop({currentElementBand, changeHeight, movePos, index}) {
+      if (movePos === 'bottomLine') {
+        console.log(' >>> bottomLine')
+        const componentBandRefs = this.$refs.componentBandRef;
+        for (let i in componentBandRefs) {
+          if (i <= index) {
+            continue;
+          }
+          componentBandRefs[i].changeTop(changeHeight)
+        }
+      }
+    },
   },
   mounted() {
     this.rulerWidth = this.$el.offsetWidth
@@ -596,8 +616,7 @@ export default {
     }
   }
 
-  .yvan-print-margin-top-line,
-  .yvan-print-margin-bottom-line {
+  .yvan-print-margin-top-line, .yvan-print-margin-bottom-line {
     position: absolute;
     height: 0;
     width: 100%;

+ 86 - 0
src/components/yvan-editor/EditorMenu.vue

@@ -0,0 +1,86 @@
+<template>
+  <div
+      v-show="visible"
+      :style="getStyle"
+      class="yvan-print-designer-menu">
+    <ul>
+      <li v-for="menu in getEditorMenu()">
+        <i
+            :class="menu.icon"
+            :title="menu.label"
+            @click="menu.event"/>
+      </li>
+    </ul>
+  </div>
+</template>
+<script lang="js">
+import {mapState} from "pinia";
+import {globalStore} from "@/store";
+import eventBus from "@/utils/eventBus";
+import {getEditorMenu, getEditorRect} from '@/utils/editorUtils'
+
+export default {
+  data() {
+    return {}
+  },
+  computed: {
+    ...mapState(globalStore, {
+      editorMenuInfo: (state) => state.editorMenuInfo,
+      curComponent: (state) => state.curComponent
+    }),
+    visible() {
+      return this.editorMenuInfo?.visible;
+    },
+    getStyle() {
+      const styles = [];
+      const menuStyle = this.editorMenuInfo.style;
+      styles.push(`top: ${menuStyle.top}px`)
+      styles.push(`left: ${menuStyle.left}px`)
+      return styles.join(';');
+    }
+  },
+  mounted() {
+    this.initMounted();
+  },
+  methods: {
+    getEditorMenu,
+    initMounted() {
+      eventBus.on('move', (args) => {
+        // const {isDownward, isRightward, curX, curY} = args
+        const editorRect = getEditorRect();
+        const menuTop = this.curComponent.style.top + editorRect.top;
+        const menuLeft = this.curComponent.style.left + editorRect.left + this.curComponent.width;
+        globalStore().showEditorMenu({top: menuTop, left: menuLeft})
+      })
+      eventBus.on('unmove', () => {
+        // this.showCoordinate = false
+      })
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.yvan-print-designer-menu {
+  position: fixed;
+  width: 200px;
+  border: 0.5px dotted var(--yvan-menu-bar-background);
+
+  ul {
+    list-style: none;
+    display: flex;
+    justify-content: right;
+    margin: 2px 0;
+
+    li {
+      margin-right: 10px;
+
+      i:hover {
+        cursor: pointer;
+      }
+    }
+
+
+  }
+}
+</style>

+ 106 - 0
src/globalConfig.ts

@@ -0,0 +1,106 @@
+import {AxiosInstance, AxiosRequestConfig} from "axios";
+import {router} from "@/router";
+
+const httpStatus = {
+    200: "服务器成功返回请求的数据",
+    201: "新建或修改数据成功",
+    202: "一个请求已经进入后台排队(异步任务)",
+    204: "删除数据成功",
+    400: "发出的请求有错误,服务器没有进行新建或修改数据的操作",
+    401: "用户没有权限(令牌、用户名、密码错误)",
+    403: "用户得到授权,但是访问是被禁止的",
+    404: "发出的请求针对的是不存在的记录,服务器没有进行操作",
+    406: "请求的格式不可得",
+    410: "请求的资源被永久删除,且不会再得到的",
+    422: "当创建一个对象时,发生一个验证错误",
+    500: "服务器发生错误,请检查服务器",
+    502: "网关错误",
+    503: "服务不可用,服务器暂时过载或维护",
+    504: "网关超时",
+};
+
+/**
+ * axios请求默认配置
+ */
+const axiosRequestDef: () => AxiosRequestConfig = () => {
+    return {
+        baseURL: '',
+        method: 'post',
+        headers: {},
+        params: {},
+        timeout: 60_000,
+        withCredentials: true,
+        responseType: 'json',
+        responseEncoding: 'utf8',
+        validateStatus: status => (status >= 200 && status < 400),
+    };
+};
+
+/**
+ * 自定义全局axios实例
+ */
+const customAxios: (axiosInstance: AxiosInstance) => AxiosInstance = axiosInstance => {
+    // const notificationDef: any = {
+    //     duration: 3500,
+    //     position: "top-right",
+    //     title: "错误",
+    // };
+    // 全局请求拦截
+    axiosInstance.interceptors.request.use(
+        request => request,
+        error => {
+            // window.$notify?.error({
+            //     ...notificationDef,
+            //     title: "请求发送失败",
+            //     message: "发送请求给服务端失败,请检查电脑网络,再重试",
+            // });
+            return Promise.reject(error);
+        },
+    );
+    // 全局拦截配置
+    axiosInstance.interceptors.response.use(
+        response => response,
+        error => {
+            const {response} = error;
+            if (!error || !response) {
+                // window.$notify?.error({
+                //     ...notificationDef,
+                //     message: "请求服务端异常",
+                // });
+                return Promise.reject(error);
+            }
+            if (response?.status === 401) {
+                // 跳转到登录页面
+                // window.$notify?.error({
+                //     ...notificationDef,
+                //     message: "当前用户未登录",
+                // });
+                // 跳转到登录页
+                router.push({name: "login"}).finally();
+                return Promise.reject(error);
+            }
+            const {data: {message, validMessageList}} = response;
+            if (validMessageList) {
+                // window.$notify?.error({
+                //     ...notificationDef,
+                //     message: "请求参数校验失败",
+                // });
+                return Promise.reject(error.response);
+            } else if (message) {
+                // const errorText = message ?? httpStatus[response.status] ?? "服务器异常";
+                // window.$notify?.error({
+                //     ...notificationDef,
+                //     message: errorText,
+                // });
+            }
+            return Promise.reject(error);
+        }
+    );
+    return axiosInstance;
+};
+
+export default {
+    httpStatus,
+    axiosRequestDef,
+    customAxios
+}

+ 93 - 0
src/router.ts

@@ -0,0 +1,93 @@
+import lodash from "lodash"
+import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router'
+import typeUtils from './utils/typeUtils'
+
+// 基本页面
+const basePages = {
+    empty: () => import('@/components/Empty.vue'),
+    design: () => import('@/pages/Design.vue'),
+    studio: () => import('@/pages/Studio.vue'),
+    404: () => import('@/components/NotFound.vue'),
+}
+
+// 利用vite扫描所有的页面组件 | /src/pages/Demo.vue -> import('/src/pages/Demo.vue') -> Module.default
+const imports = import.meta.glob(
+    ['@/pages/**/*.vue'],
+    {eager: false},
+);
+const pages = {};
+lodash.forEach(imports, (cmp, key: string) => {
+    let component = cmp;
+    if (typeUtils.variableTypeOf(cmp) === typeUtils.typeEnum.function) {
+        component = async () => {
+            return cmp().then((module: any) => {
+                // console.log("@@@", menu.pagePath)
+                if (module?.default) module.default.__filename = key;
+                return module;
+            });
+        };
+    }
+    pages[key] = component;
+});
+window['pages'] = pages
+
+const designJS = {};
+lodash.forEach(pages, (value, key: string) => {
+    if (key.startsWith("/src/pages")) {
+        key = key.substring(11, key.length - 4);
+    }
+    designJS[key] = async () => {
+        const module: any = await value();
+        let vjson = {};
+        if (lodash.isFunction(module?.default?.mixins[0]?.data)) {
+            vjson = module.default.mixins[0].data().vjson || {};
+        }
+        return vjson;
+    };
+});
+window['designJS'] = designJS
+
+/** 获取路由数据 */
+async function getRoutes(): Promise<RouteRecordRaw[]> {
+    const routes: RouteRecordRaw[] = [];
+    // 全局404页面
+    routes.push({path: "/:pathMatch(.*)*", component: basePages["404"]});
+    return routes;
+}
+
+/** 全局的路由对象 */
+const router = createRouter({
+    history: createWebHashHistory(),
+    routes: [],
+    strict: true,
+    sensitive: true,
+});
+
+/** 删除路由的钩子函数 */
+const removeRoutesFuc: Array<() => void> = [];
+
+/** 初始化路由 */
+async function initRouter() {
+    // system.setStudioRequest(request)
+
+    const routes = await getRoutes();
+    removeRoutesFuc.forEach(removeRoute => removeRoute());
+    removeRoutesFuc.length = 0;
+    routes.forEach(route => removeRoutesFuc.push(router.addRoute(route)));
+    router.addRoute({
+        name: "design",
+        path: "/design",
+        component: basePages.design,
+    })
+    router.addRoute({
+        name: "studio",
+        path: "/studio",
+        component: basePages.studio,
+    })
+}
+
+export {
+    pages,
+    initRouter,
+    router,
+};

+ 1 - 0
src/store/compose.ts

@@ -21,6 +21,7 @@ export default {
     actions: {
         getEditor() {
             this.editor = document.querySelector('#designer-page')
+            return this.editor;
         },
 
         setAreaData(data) {

+ 101 - 36
src/store/global.ts

@@ -1,10 +1,11 @@
 import {defineStore} from 'pinia'
-import {ruleStore} from "@/store/ruler-things";
+import copy from '@/store/copy'
+import lock from '@/store/lock'
 import layer from '@/store/layer'
 import compose from '@/store/compose'
 import snapshot from '@/store/snapshot'
-import copy from '@/store/copy'
-import lock from '@/store/lock'
+import {ruleStore} from "@/store/ruler-things";
+import {getEditorMenuPos} from '@/utils/editorUtils'
 
 export const globalStore = defineStore('global', {
     state: () => {
@@ -15,10 +16,8 @@ export const globalStore = defineStore('global', {
             // 编辑器模式 edit preview
             editMode: 'edit',
             pageConfig: {
-                // 页面大小
-                pageSize: 'A4',
                 // 页面方向 h 水平方向 v 垂直方向
-                pageDirection: 'v',
+                pageDirection: 'Vertical',
                 // 页面长度:mm
                 pageWidth: 210,
                 // 页面高度:mm
@@ -28,37 +27,46 @@ export const globalStore = defineStore('global', {
                 // 模板编码
                 code: 'TEMPLATE',
                 // 默认缩放比例:100%
-                scale: 1,
-                // 页面背景
-                background: '#ffffff',
-                // 默认字体颜色
-                color: '#212121',
-                // 默认字号
-                fontSize: 12,
-                // 默认字体
-                fontFamily: 'simhei',
-                // 默认行高
-                lineHeight: 1,
                 // 页面边距 mm
-                margins: {
-                    top: 8,
-                    right: 8,
-                    bottom: 8,
-                    left: 8
+                topMargin: 8,
+                rightMargin: 8,
+                bottomMargin: 8,
+                leftMargin: 8,
+                style: {
+                    // 默认缩放比例:100%
+                    scale: 1,
+                    // 页面背景
+                    background: '#ffffff',
+                    // 默认字体颜色
+                    color: '#000000',
+                    // 默认字号
+                    fontSize: 12,
+                    // 默认字体
+                    fontFamily: '微软雅黑',
+                    // 默认行高
+                    lineHeight: 1,
                 }
             },
             // 是否在编辑器中,用于判断复制、粘贴组件时是否生效,如果在编辑器外,则无视这些操作
             isInEditor: false,
             componentData: [],
+            elementBands: new Map(),
             dataSource: [],
-            dataSet: {},
+            dataset: {},
             curComponent: null,
             curTableCell: null,
             curComponentIndex: null,
             // 点击画布时是否点中组件,主要用于取消选中组件用。
             // 如果没点中组件,并且在画布空白处弹起鼠标,则取消当前组件的选中状态
             isClickComponent: false,
-            curTableSettingId: null
+            curTableSettingId: null,
+            editorMenuInfo: {
+                visible: false,
+                style: {
+                    top: 0,
+                    left: 0
+                }
+            }
         }
     },
     actions: {
@@ -93,12 +101,11 @@ export const globalStore = defineStore('global', {
             this.curTableCell = component
         },
 
-        setPageSize({pageSize, w, h}) {
-            this.pageConfig.pageSize = pageSize
-            this.pageConfig.pageWidth = w
-            this.pageConfig.pageHeight = h
-            console.log('w, h', w, h)
-            ruleStore().setRect({w: w, h: h})
+        setPageSize({pageWidth, pageHeight}) {
+            this.pageConfig.pageWidth = pageWidth
+            this.pageConfig.pageHeight = pageHeight
+            // console.log('w, h', pageWidth, pageHeight)
+            ruleStore().setRect({pageWidth, pageHeight})
             ruleStore().setReDrawRuler()
         },
 
@@ -106,8 +113,8 @@ export const globalStore = defineStore('global', {
             const pageConfig = this.pageConfig
             pageConfig.pageDirection = pageDirection
             ruleStore().setRect({
-                w: pageDirection === 'v' ? pageConfig.pageWidth : pageConfig.pageHeight,
-                h: pageDirection === 'v' ? pageConfig.pageHeight : pageConfig.pageWidth
+                pageWidth: pageDirection === 'Vertical' ? pageConfig.pageWidth : pageConfig.pageHeight,
+                pageHeight: pageDirection === 'Vertical' ? pageConfig.pageHeight : pageConfig.pageWidth
             })
             ruleStore().setReDrawRuler()
         },
@@ -115,8 +122,8 @@ export const globalStore = defineStore('global', {
         setPageConfig(pageConfig) {
             this.pageConfig = pageConfig
             ruleStore().setRect({
-                w: pageConfig.pageDirection === 'p' ? pageConfig.pageWidth : pageConfig.pageHeight,
-                h: pageConfig.pageDirection === 'p' ? pageConfig.pageHeight : pageConfig.pageWidth
+                pageWidth: pageConfig.pageDirection === 'p' ? pageConfig.pageWidth : pageConfig.pageHeight,
+                pageHeight: pageConfig.pageDirection === 'p' ? pageConfig.pageHeight : pageConfig.pageWidth
             })
             ruleStore().setReDrawRuler()
         },
@@ -200,7 +207,47 @@ export const globalStore = defineStore('global', {
         setComponentData(state, componentData = []) {
             state.componentData = componentData
         },
-
+        getElementBandByCode(bandCode) {
+            return this.elementBands.get(bandCode);
+        },
+        addElementBand(elementBand) {
+            this.elementBands.set(elementBand.code, {
+                band: {height: elementBand.height, splitType: 'Stretch'},
+                elements: []
+            })
+        },
+        changeBandHeight({code, height}) {
+            const originElementBand = this.elementBands.get(code)
+            if (originElementBand !== undefined) {
+                originElementBand.band.height = height;
+            }
+        },
+        deleteElementBand(elementBandCode) {
+            if (elementBandCode !== undefined) {
+                this.elementBands.delete(elementBandCode)
+            }
+        },
+        addElement({bandCode, element, index}) {
+            const elementBand = this.elementBands.get(bandCode);
+            if (index !== undefined) {
+                elementBand.elements.splice(index, 0, element)
+            } else {
+                elementBand.elements.push(element)
+            }
+        },
+        deleteElement({elementBandCode, index}) {
+            const elementBand = this.elementBands.get(elementBandCode);
+            if (index === undefined) {
+                index = this.curComponentIndex
+            }
+            if (index === this.curComponentIndex) {
+                this.curComponentIndex = null
+                this.curComponent = null
+            }
+            if (/\d/.test(index)) {
+                elementBand.elements.splice(index, 1)
+            }
+        },
         addComponent({component, index}) {
             if (index !== undefined) {
                 this.componentData.splice(index, 0, component)
@@ -208,7 +255,6 @@ export const globalStore = defineStore('global', {
                 this.componentData.push(component)
             }
         },
-
         deleteComponent(index) {
             if (index === undefined) {
                 index = this.curComponentIndex
@@ -223,6 +269,25 @@ export const globalStore = defineStore('global', {
                 this.componentData.splice(index, 1)
             }
         },
+        showEditorMenu({top, left}) {
+            const editorMenuPos = getEditorMenuPos(top, left);
+            this.editorMenuInfo = {
+                visible: true,
+                style: {
+                    top: editorMenuPos.top,
+                    left: editorMenuPos.left
+                }
+            };
+        },
+        hideEditorMenu() {
+            this.editorMenuInfo = {
+                visible: false,
+                style: {
+                    top: 0,
+                    left: 0
+                }
+            };
+        }
     },
     getters: {
         getPageConfig(state) {

+ 3 - 3
src/store/ruler-things.ts

@@ -38,9 +38,9 @@ export const ruleStore = defineStore('ruleThings', {
         setReDrawRuler() {
             this.needReDrawRuler += 1
         },
-        setRect({w, h}) {
-            this.rectWidth = w
-            this.rectHeight = h
+        setRect({pageWidth, pageHeight}) {
+            this.rectWidth = pageWidth
+            this.rectHeight = pageHeight
         },
         rotateRect() {
             const tmpHeight = this.rectHeight

+ 126 - 0
src/utils/editorUtils.js

@@ -0,0 +1,126 @@
+import {globalStore} from "@/store";
+
+export function getEditorRect() {
+    return globalStore().getEditor()?.getBoundingClientRect();
+}
+
+export function getEditorMenuPos(eleTop, eleLeft) {
+    const editorRect = globalStore().getEditor()?.getBoundingClientRect();
+    if (editorRect) {
+        return {
+            top: eleTop - 28,
+            left: eleLeft - 200
+        }
+    }
+    return {}
+}
+
+export function getEditorMenu() {
+    const currentElement = globalStore().curComponent;
+    if (!currentElement) {
+        return [
+            {
+                code: 'paste',
+                icon: 'fa fa-paste',
+                label: '粘贴',
+                status: 'default',
+                event: () => {
+                    globalStore().paste(true)
+                    globalStore().recordSnapshot()
+                }
+            }
+        ]
+    }
+    if (currentElement?.isLock) {
+        return [
+            {
+                code: 'unlock',
+                icon: 'fa fa-unlock',
+                label: '解锁',
+                status: 'default',
+                event: () => {
+                    globalStore().unlock({curComponent: currentElement})
+                }
+            }
+        ]
+    }
+    return [
+        {
+            code: 'copy',
+            icon: 'fa fa-copy',
+            label: '复制',
+            status: 'default',
+            event: () => {
+                globalStore().copy()
+            }
+        },
+        {
+            code: 'cut',
+            icon: 'fa fa-cut',
+            label: '剪切',
+            status: 'default',
+            event: () => {
+                globalStore().cut()
+            }
+        },
+        {
+            code: 'del',
+            icon: 'fa fa-trash',
+            label: '删除',
+            status: 'danger',
+            event: () => {
+                globalStore().deleteComponent(globalStore().curComponentIndex)
+                globalStore().recordSnapshot()
+            }
+        },
+        {
+            code: 'lock',
+            icon: 'fa fa-lock',
+            label: '锁定',
+            status: 'default',
+            event: () => {
+                globalStore().lock({curComponent: currentElement})
+            }
+        },
+        {
+            code: 'top',
+            icon: 'fa fa-angle-double-up',
+            label: '置顶',
+            status: 'default',
+            event: () => {
+                // this.$store.commit('printTemplateModule/topComponent')
+                // this.$store.commit('printTemplateModule/recordSnapshot')
+            }
+        },
+        {
+            code: 'bottom',
+            icon: 'fa fa-angle-double-down',
+            label: '置底',
+            status: 'default',
+            event: () => {
+                // this.$store.commit('printTemplateModule/bottomComponent')
+                // this.$store.commit('printTemplateModule/recordSnapshot')
+            }
+        },
+        {
+            code: 'up',
+            icon: 'fa fa-arrow-circle-up',
+            label: '上移',
+            status: 'default',
+            event: () => {
+                // this.$store.commit('printTemplateModule/upComponent')
+                // this.$store.commit('printTemplateModule/recordSnapshot')
+            }
+        },
+        {
+            code: 'down',
+            icon: 'fa fa-arrow-circle-down',
+            label: '下移',
+            status: 'default',
+            event: () => {
+                // this.$store.commit('printTemplateModule/downComponent')
+                // this.$store.commit('printTemplateModule/recordSnapshot')
+            }
+        }
+    ]
+}

+ 125 - 0
src/utils/request.ts

@@ -0,0 +1,125 @@
+import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
+import globalConfig from "@/globalConfig";
+import lodash from "lodash";
+
+interface InnerRequestConfig extends AxiosRequestConfig {
+    /** 请求之前的处理 */
+    beforeRequest?: (config: AxiosRequestConfig) => AxiosRequestConfig;
+    /** 请求响应后的处理 */
+    afterResponse?: (response: AxiosResponse) => any;
+}
+
+class Request {
+    /** axios实例 */
+    private axiosInstance: AxiosInstance;
+
+    constructor(axiosInstance: AxiosInstance) {
+        this.axiosInstance = axiosInstance;
+    }
+
+    /** 自定义请求配置逻辑 */
+    protected initConfig(config: InnerRequestConfig = {}): AxiosRequestConfig {
+        const { beforeRequest, afterResponse, ...other } = config;
+        let newConfig = lodash.defaultsDeep(other, globalConfig.axiosRequestDef());
+        if (newConfig.transformRequest?.length > 0) {
+            newConfig.transformRequest = [
+                ...(axios.defaults.transformRequest as []),
+                ...(newConfig.transformRequest as []),
+            ];
+        }
+        if (newConfig.transformResponse?.length > 0) {
+            newConfig.transformResponse = [
+                ...(axios.defaults.transformResponse as []),
+                ...(newConfig.transformResponse as []),
+            ];
+        }
+        if (beforeRequest) {
+            newConfig = beforeRequest(newConfig);
+        }
+        if(afterResponse){
+            newConfig.afterResponse = afterResponse
+        }
+        return newConfig;
+    }
+
+    /** 自定义响应 */
+    protected getResponse<T = any>(config: InnerRequestConfig = {}, request: Promise<AxiosResponse>): Promise<T> {
+        if (config.afterResponse) {
+            return request.then(response => config.afterResponse!(response));
+        }
+        return request.then(response => (response?.data ?? null));
+    }
+
+    public get<T = any>(url: string, config?: InnerRequestConfig): Promise<T> {
+        config = this.initConfig(config);
+        const request = this.axiosInstance.get(url, config);
+        return this.getResponse<T>(config, request);
+    }
+
+    public delete<T = any>(url: string, config?: InnerRequestConfig): Promise<T> {
+        config = this.initConfig(config);
+        const request = this.axiosInstance.delete(url, config);
+        return this.getResponse<T>(config, request);
+    }
+
+    public head<T = any>(url: string, config?: InnerRequestConfig): Promise<T> {
+        config = this.initConfig(config);
+        const request = this.axiosInstance.head(url, config);
+        return this.getResponse<T>(config, request);
+    }
+
+    public options<T = any>(url: string, config?: InnerRequestConfig): Promise<T> {
+        config = this.initConfig(config);
+        const request = this.axiosInstance.options(url, config);
+        return this.getResponse<T>(config, request);
+    }
+
+    public post<T = any>(url: string, data?: any, config?: InnerRequestConfig): Promise<T> {
+        config = this.initConfig(config);
+        const request = this.axiosInstance.post(url, data, config);
+        return this.getResponse<T>(config, request);
+    }
+
+    public put<T = any>(url: string, data?: any, config?: InnerRequestConfig): Promise<T> {
+        config = this.initConfig(config);
+        const request = this.axiosInstance.put(url, data, config);
+        return this.getResponse<T>(config, request);
+    }
+
+    public patch<T = any>(url: string, data?: any, config?: InnerRequestConfig): Promise<T> {
+        config = this.initConfig(config);
+        const request = this.axiosInstance.patch(url, data, config);
+        return this.getResponse<T>(config, request);
+    }
+
+    /**
+     * config 可以是 RequestConfig 类型
+     */
+    public request<T = any>(config: InnerRequestConfig): Promise<T> {
+        const request = this.axiosInstance.request(this.initConfig(config));
+        return this.getResponse<T>(config, request);
+    }
+
+    public invoke<T = any>(url: string, args: any[] = [], config?: InnerRequestConfig): Promise<T> {
+        config = this.initConfig(config);
+        config.url = url;
+        config.data = { args };
+        const request = this.axiosInstance.request(config)
+        return this.getResponse<T>(config, request);
+    }
+}
+
+/** 创建一个axios实例对象 */
+function axiosCreate(config?: AxiosRequestConfig): AxiosInstance {
+    return axios.create({
+        validateStatus: () => true,
+        ...config,
+    });
+}
+
+const axiosInstance = axiosCreate(globalConfig.axiosRequestDef());
+globalConfig.customAxios(axiosInstance);
+
+const request = new Request(axiosInstance);
+
+export { axiosCreate, request };

+ 92 - 0
src/utils/typeUtils.ts

@@ -0,0 +1,92 @@
+/** 所有变量类型 */
+enum typeEnum {
+    string = 'string',
+    number = 'number',
+    object = 'object',
+    array = 'array',
+    function = 'function',
+    null = 'null',
+    boolean = 'boolean',
+    symbol = 'symbol',
+    json = 'json',
+    math = 'math',
+    regexp = 'regexp',
+    date = 'date',
+    undefined = 'undefined',
+    nan = 'nan',
+    reactNode = 'react_node',
+}
+
+/**
+ * 判断对象类型
+ * @param object 对象
+ */
+const variableTypeOf = (object?: any): typeEnum => {
+    const typeStr = Object.prototype.toString.call(object);
+    let typeName: typeEnum;
+    switch (`${typeStr}`.toLowerCase()) {
+        case '[object string]':
+            typeName = typeEnum.string;
+            break;
+        case '[object number]':
+            if (Number.isNaN(object)) {
+                return typeEnum.nan;
+            }
+            typeName = typeEnum.number;
+            break;
+        case '[object object]':
+            if (object.$$typeof && object.props) {
+                const type = variableTypeOf(object.$$typeof);
+                if (type === typeEnum.symbol || type === typeEnum.reactNode) {
+                    return typeEnum.reactNode;
+                }
+            }
+            typeName = typeEnum.object;
+            break;
+        case '[object array]':
+            typeName = typeEnum.array;
+            break;
+        case '[object function]':
+            typeName = typeEnum.function;
+            break;
+        case '[object null]':
+            typeName = typeEnum.null;
+            break;
+        case '[object boolean]':
+            typeName = typeEnum.boolean;
+            break;
+        case '[object date]':
+            typeName = typeEnum.date;
+            break;
+        // 不常用
+        case '[object symbol]':
+            typeName = typeEnum.symbol;
+            break;
+        case '[object json]':
+            typeName = typeEnum.json;
+            break;
+        case '[object math]':
+            typeName = typeEnum.math;
+            break;
+        case '[object regexp]':
+            typeName = typeEnum.regexp;
+            break;
+        // 貌似不会走的分支
+        case '[object undefined]':
+            typeName = typeEnum.undefined;
+            break;
+        // 无法识别
+        default:
+            if (object === undefined) {
+                return typeEnum.undefined;
+            }
+            console.log('varTypeOf -> ', object, ' | -> ', typeStr, ' | ', `${typeStr}`.toLowerCase());
+            typeName = typeEnum.object;
+    }
+    return typeName;
+};
+
+export default {
+    typeEnum,
+    variableTypeOf,
+};

+ 5 - 1
vite.config.ts

@@ -34,6 +34,10 @@ export default defineConfig({
         },
     },
     server: {
-        host: true
+        host: true,
+        port: 5170,
+        proxy: {
+
+        }
     }
 })