Selaa lähdekoodia

feat: 增强拓扑编辑器组件配置与素材分组

- 新增图标、危险标识等素材分组注册,支持从本地资源目录批量加载图标与图片素材
- 新增线框组件,支持配置背景色、边框颜色、边框样式、边框宽度和圆角
- 扩展自由连线配置,支持实线、虚线、点线、点划线等线条样式,并同步绘制预览与画布渲染效果
- 优化键值对组件,支持背景色、圆角、文字对齐方式,并同步文字组件的字体选项
- 优化图形结构树,新增图层上移、下移操作,支持通过结构树调整元素层级顺序
- 优化颜色属性配置面板,支持透明度配置并避免不同组件同名颜色属性切换时串值
- 优化拓扑编辑页进入逻辑,进入编辑器时清理上一次画布状态,避免新建或无数据项目复用旧画布内容
- 补充危险标识、图标、工艺流程图和素材相关静态资源
Zimo 3 päivää sitten
vanhempi
commit
8a8a671e8c
33 muutettua tiedostoa jossa 570 lisäystä ja 72 poistoa
  1. 48 0
      src/App.vue
  2. BIN
      src/assets/images/danger/地刺.png
  3. BIN
      src/assets/images/danger/当心硫化氢气体.png
  4. BIN
      src/assets/images/danger/当心触电.png
  5. BIN
      src/assets/images/danger/当心高温.png
  6. BIN
      src/assets/images/danger/注意噪音防护.png
  7. BIN
      src/assets/images/danger/防冲撞栏.png
  8. BIN
      src/assets/images/danger/高压危险.png
  9. 3 0
      src/assets/images/icons/人群.svg
  10. 3 0
      src/assets/images/icons/扳手.svg
  11. 3 0
      src/assets/images/icons/账户.svg
  12. 3 0
      src/assets/images/icons/跑.svg
  13. 3 0
      src/assets/images/icons/锅.svg
  14. 3 0
      src/assets/images/icons/门.svg
  15. BIN
      src/assets/images/materials/安保设施.png
  16. BIN
      src/assets/images/materials/气防设施.png
  17. BIN
      src/assets/images/materials/消防设施.png
  18. BIN
      src/assets/images/materials/路障.png
  19. BIN
      src/assets/images/process-demo/空压机.png
  20. BIN
      src/assets/images/process-demo/空压机1.png
  21. 46 16
      src/components/custom-components/kv-vue/index.vue
  22. 49 0
      src/components/custom-components/wireframe-vue/index.vue
  23. 48 7
      src/components/mt-edit/components/done-tree/index.vue
  24. 22 5
      src/components/mt-edit/components/draw-line-render/index.vue
  25. 2 0
      src/components/mt-edit/components/layout/right-aside/index.vue
  26. 16 2
      src/components/mt-edit/components/layout/right-aside/select-item-props-setting.vue
  27. 3 1
      src/components/mt-edit/components/layout/right-aside/select-item-setting.vue
  28. 23 8
      src/components/mt-edit/components/line-render/index.vue
  29. 4 0
      src/components/mt-edit/components/render-core/index.vue
  30. 39 4
      src/components/mt-edit/index.vue
  31. 182 4
      src/components/mt-edit/store/config.ts
  32. 58 23
      src/components/mt-edit/store/global.ts
  33. 12 2
      src/views/maotu/edit.vue

+ 48 - 0
src/App.vue

@@ -28,6 +28,21 @@ const materials_files = import.meta.glob('./assets/images/materials/**', {
   query: '?url',
   import: 'default'
 })
+const danger_files = import.meta.glob('./assets/images/danger/**', {
+  eager: true,
+  query: '?url',
+  import: 'default'
+})
+const icon_modules_files = {
+  ...import.meta.glob('./assets/images/icons/**.svg', {
+    eager: true,
+    as: 'raw'
+  })
+}
+
+const normalizeFillSvg = (svg: string) => {
+  return svg.replace(/\sfill="(?!none\b)[^"]*"/gi, '')
+}
 
 const electrical_register_config: any = []
 for (const key in electrical_modules_files) {
@@ -69,6 +84,27 @@ for (const key in electrical_stroke_modules_files) {
 }
 leftAsideStore.registerConfig('电气符号', electrical_register_config)
 
+const icon_register_config: any = []
+for (const key in icon_modules_files) {
+  const name = key.split('/').pop()!.split('.')[0]
+  const svg = normalizeFillSvg(icon_modules_files[key])
+  icon_register_config.push({
+    id: `icon-${name}`,
+    title: name,
+    type: 'svg',
+    thumbnail: 'data:image/svg+xml;utf8,' + encodeURIComponent(svg),
+    svg,
+    props: {
+      fill: {
+        type: 'color',
+        val: '#FF0000',
+        title: '填充色'
+      }
+    }
+  })
+}
+leftAsideStore.registerConfig('图标', icon_register_config)
+
 const process_demo_register_config: any[] = Object.entries(process_demo_image_files).map(
   ([key, url]) => {
     const name = key.split('/').pop()!.split('.')[0]
@@ -95,6 +131,18 @@ const materials_register_config: any[] = Object.entries(materials_files).map(([k
 })
 leftAsideStore.registerConfig('素材', materials_register_config)
 
+const danger_register_config: any[] = Object.entries(danger_files).map(([key, url]) => {
+  const name = key.split('/').pop()!.split('.')[0]
+  return {
+    id: `danger-${name}`,
+    title: name,
+    type: 'img',
+    thumbnail: url,
+    props: {}
+  }
+})
+leftAsideStore.registerConfig('危险标识', danger_register_config)
+
 const route = useRoute()
 const { addListeners, removeListeners } = useAutoLogout()
 

BIN
src/assets/images/danger/地刺.png


BIN
src/assets/images/danger/当心硫化氢气体.png


BIN
src/assets/images/danger/当心触电.png


BIN
src/assets/images/danger/当心高温.png


BIN
src/assets/images/danger/注意噪音防护.png


BIN
src/assets/images/danger/防冲撞栏.png


BIN
src/assets/images/danger/高压危险.png


+ 3 - 0
src/assets/images/icons/人群.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+	<path fill="currentColor" d="M14.754 10c.966 0 1.75.784 1.75 1.75v4.749a4.501 4.501 0 0 1-9.002 0V11.75c0-.966.783-1.75 1.75-1.75zm-7.623 0a2.7 2.7 0 0 0-.62 1.53l-.01.22v4.749c0 .847.192 1.649.534 2.365Q6.539 18.999 6 19a4 4 0 0 1-4-4.001V11.75a1.75 1.75 0 0 1 1.606-1.744L3.75 10zm9.744 0h3.375c.966 0 1.75.784 1.75 1.75V15a4 4 0 0 1-5.03 3.866c.3-.628.484-1.32.525-2.052l.009-.315V11.75c0-.665-.236-1.275-.63-1.75M12 3a3 3 0 1 1 0 6a3 3 0 0 1 0-6m6.5 1a2.5 2.5 0 1 1 0 5a2.5 2.5 0 0 1 0-5m-13 0a2.5 2.5 0 1 1 0 5a2.5 2.5 0 0 1 0-5" />
+</svg>

+ 3 - 0
src/assets/images/icons/扳手.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+	<path fill="currentColor" d="m22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9c-2-2-5-2.4-7.4-1.3L9 6L6 9L1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4" />
+</svg>

+ 3 - 0
src/assets/images/icons/账户.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+	<path fill="currentColor" d="M12 2q1.25 0 2.125.875T15 5t-.875 2.125T12 8t-2.125-.875T9 5t.875-2.125T12 2m0 7q1.175 0 2.325.275t2.075.775q.95.475 1.525 1.125T18.5 12.6v5.8q0 .425-.2.838t-.55.762t-.812.65t-1.038.55v-2.25q0-.95-1.312-1.55T12 16.8q-1.25 0-2.412.513T8.15 18.65q.95.375 1.95.525t2.05.175H13v2.6q-.175.05-.362.05h-.388q-.9 0-2.062-.2t-2.213-.625t-1.762-1.112T5.5 18.4v-5.8q0-.775.575-1.425t1.5-1.125q.95-.5 2.1-.775T12 9m0 6q.825 0 1.413-.587T14 13t-.587-1.412T12 11t-1.412.588T10 13t.588 1.413T12 15" />
+</svg>

+ 3 - 0
src/assets/images/icons/跑.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+	<path fill="currentColor" d="M13.5 5.5c1.09 0 2-.92 2-2a2 2 0 0 0-2-2c-1.11 0-2 .88-2 2c0 1.08.89 2 2 2M9.89 19.38l1-4.38L13 17v6h2v-7.5l-2.11-2l.61-3A7.3 7.3 0 0 0 19 13v-2c-1.91 0-3.5-1-4.31-2.42l-1-1.58c-.4-.62-1-1-1.69-1c-.31 0-.5.08-.81.08L6 8.28V13h2V9.58l1.79-.7L8.19 17l-4.9-1l-.4 2z" />
+</svg>

+ 3 - 0
src/assets/images/icons/锅.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+	<path fill="currentColor" fill-rule="evenodd" d="M20 11v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-7H3a1 1 0 0 1 0-2h18a1 1 0 0 1 0 2zM6 11v7h12v-7zm5-5V5a1 1 0 0 1 2 0v1h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z" />
+</svg>

+ 3 - 0
src/assets/images/icons/门.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+	<path fill="currentColor" d="M4.5 20v-1h2V4h8v1h3v14h2v1h-3V6h-2v14zm7.54-7.46q.23-.23.23-.54t-.23-.54t-.54-.23t-.54.23t-.23.54t.23.54t.54.23t.54-.23" />
+</svg>

BIN
src/assets/images/materials/安保设施.png


BIN
src/assets/images/materials/气防设施.png


BIN
src/assets/images/materials/消防设施.png


BIN
src/assets/images/materials/路障.png


BIN
src/assets/images/process-demo/空压机.png


BIN
src/assets/images/process-demo/空压机1.png


+ 46 - 16
src/components/custom-components/kv-vue/index.vue

@@ -1,18 +1,22 @@
 <template>
-  <table class="w-1/1 h-1/1 kvTable">
-    <colgroup>
-      <col class="kvKeyCol" />
-      <col class="kvValueCol" />
-    </colgroup>
-    <tbody>
-      <tr>
-        <td class="kvKey kvKeyValue" colspan="1">{{ props.label }}</td>
-        <td class="kvValue kvKeyValue" colspan="1">{{ props.value }}</td>
-      </tr>
-    </tbody>
-  </table>
+  <div class="kvBox" :style="kvBoxStyle">
+    <table class="w-1/1 h-1/1 kvTable">
+      <colgroup>
+        <col class="kvKeyCol" />
+        <col class="kvValueCol" />
+      </colgroup>
+      <tbody>
+        <tr>
+          <td class="kvKey kvKeyValue" colspan="1">{{ props.label }}</td>
+          <td class="kvValue kvKeyValue" colspan="1">{{ props.value }}</td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
 </template>
 <script setup lang="ts">
+import { computed } from 'vue'
+
 const props = defineProps({
   fontFamily: {
     type: String,
@@ -22,6 +26,10 @@ const props = defineProps({
     type: Number,
     default: 15
   },
+  textAlign: {
+    type: String,
+    default: 'left'
+  },
   label: {
     type: String,
     default: ''
@@ -46,17 +54,38 @@ const props = defineProps({
     type: Boolean,
     default: true
   },
+  backgroundColor: {
+    type: String,
+    default: 'rgba(255, 255, 255, 0)'
+  },
   borderColor: {
     type: String,
     default: ''
+  },
+  borderRadius: {
+    type: Number,
+    default: 0
   }
 })
+
+const kvBoxStyle = computed(() => ({
+  backgroundColor: props.backgroundColor,
+  border: `${props.border ? 1 : 0}px solid ${props.borderColor}`,
+  borderRadius: `${props.borderRadius}px`
+}))
 </script>
 <style scoped>
+.kvBox {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
 .kvTable {
-  border: v-bind('`${props.border?1:0}px solid ${props.borderColor}`');
+  border-collapse: separate;
+  border-spacing: 0;
   table-layout: fixed;
-  border-collapse: collapse;
 }
 
 .kvKeyCol {
@@ -68,12 +97,13 @@ const props = defineProps({
 }
 
 .kvKeyValue {
+  overflow: hidden;
   font-family: v-bind('`${props.fontFamily}`');
   font-size: v-bind('`${props.fontSize}px`');
   color: v-bind('`${props.color}`');
-  overflow: hidden;
-  white-space: nowrap;
+  text-align: v-bind('`${props.textAlign}`');
   text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 .kvKey {

+ 49 - 0
src/components/custom-components/wireframe-vue/index.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="wireframe" :style="wireframeStyle"></div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import type { PropType } from 'vue'
+
+type BorderStyle = 'solid' | 'dashed' | 'dotted' | 'double' | 'none'
+
+const props = defineProps({
+  fill: {
+    type: String,
+    default: 'rgba(255, 255, 255, 0)'
+  },
+  borderColor: {
+    type: String,
+    default: '#409eff'
+  },
+  borderStyle: {
+    type: String as PropType<BorderStyle>,
+    default: 'solid'
+  },
+  borderWidth: {
+    type: Number,
+    default: 2
+  },
+  borderRadius: {
+    type: Number,
+    default: 0
+  }
+})
+
+const wireframeStyle = computed(() => ({
+  backgroundColor: props.fill,
+  borderColor: props.borderColor,
+  borderStyle: props.borderStyle,
+  borderWidth: `${props.borderWidth}px`,
+  borderRadius: `${props.borderRadius}px`
+}))
+</script>
+
+<style scoped>
+.wireframe {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+}
+</style>

+ 48 - 7
src/components/mt-edit/components/done-tree/index.vue

@@ -9,13 +9,37 @@
     node-key="id"
     :current-node-key="current_node_key">
     <template #default="{ node, data }">
-      <div class="flex justify-between w-8/10">
+      <div class="flex justify-between w-9/10">
         <div>{{ node.label }}</div>
-        <el-button text circle size="small" class="mr-10px">
-          <el-icon :title="data.hide ? '隐藏' : '显示'" :size="20" @click.stop="changeHide(data)">
-            <svg-analysis :name="data.hide ? 'view-hide' : 'view-show'" />
-          </el-icon>
-        </el-button>
+        <div class="flex items-center">
+          <el-button
+            text
+            circle
+            size="small"
+            :disabled="!canMoveUp(data)"
+            title="上移一层"
+            @click.stop="moveLayer(data, 'up')">
+            <el-icon :size="16">
+              <ArrowUp />
+            </el-icon>
+          </el-button>
+          <el-button
+            text
+            circle
+            size="small"
+            :disabled="!canMoveDown(data)"
+            title="下移一层"
+            @click.stop="moveLayer(data, 'down')">
+            <el-icon :size="16">
+              <ArrowDown />
+            </el-icon>
+          </el-button>
+          <el-button text circle size="small" class="mr-10px">
+            <el-icon :title="data.hide ? '隐藏' : '显示'" :size="20" @click.stop="changeHide(data)">
+              <svg-analysis :name="data.hide ? 'view-hide' : 'view-show'" />
+            </el-icon>
+          </el-button>
+        </div>
       </div>
     </template>
   </el-tree>
@@ -23,6 +47,7 @@
 
 <script lang="ts" setup>
 import { ElTree, ElButton, ElIcon } from 'element-plus'
+import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
 import { computed } from 'vue'
 import type { IDoneJson } from '@/components/mt-edit/store/types'
 import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
@@ -32,7 +57,7 @@ type DoneTree = {
 }
 const doneTreeProps = withDefaults(defineProps<DoneTree>(), {})
 
-const emits = defineEmits(['updateSelectedItemsId', 'updateSelectedIdHide'])
+const emits = defineEmits(['updateSelectedItemsId', 'updateSelectedIdHide', 'moveLayer'])
 
 const current_node_key = computed(() => {
   return doneTreeProps.selectedItemsId.length == 1 ? doneTreeProps.selectedItemsId[0] : ''
@@ -43,6 +68,22 @@ const handleNodeClick = (data: IDoneJson) => {
 const changeHide = (data: IDoneJson) => {
   emits('updateSelectedIdHide', data.id)
 }
+const getLayerIndex = (data: IDoneJson) => {
+  return doneTreeProps.doneJson.findIndex((item) => item.id === data.id)
+}
+const canMoveUp = (data: IDoneJson) => {
+  const index = getLayerIndex(data)
+  return index >= 0 && index < doneTreeProps.doneJson.length - 1
+}
+const canMoveDown = (data: IDoneJson) => {
+  return getLayerIndex(data) > 0
+}
+const moveLayer = (data: IDoneJson, direction: 'up' | 'down') => {
+  emits('moveLayer', {
+    id: data.id,
+    direction
+  })
+}
 const defaultProps = {
   children: 'nochildren',
   label: 'title'

+ 22 - 5
src/components/mt-edit/components/draw-line-render/index.vue

@@ -51,13 +51,10 @@
             : lineRenderProps.itemJson.props.stroke.val
         "
         :stroke-width="lineRenderProps.itemJson.props['stroke-width'].val"
+        :stroke-dasharray="line_dasharray"
+        :stroke-linecap="line_stroke_linecap"
         style="cursor: move"
         stroke-dashoffset="0"
-        :stroke-dasharray="
-          lineRenderProps.itemJson.props.ani_type.val === 'electricity'
-            ? lineRenderProps.itemJson.props['stroke-width'].val * 3
-            : 0
-        "
         :marker-start="
           lineRenderProps.itemJson.props?.['marker-start']?.val
             ? `url(#markerArrowStart${lineRenderProps.itemJson.id})`
@@ -118,6 +115,26 @@ const arrow_marker_size = computed(() => {
   const stroke_width = Number(lineRenderProps.itemJson.props['stroke-width'].val) || 1
   return Math.min(Math.max(16 / stroke_width, 2.5), 6)
 })
+const line_style = computed(() => lineRenderProps.itemJson.props.lineStyle?.val || 'solid')
+const line_dasharray = computed(() => {
+  const stroke_width = Number(lineRenderProps.itemJson.props['stroke-width'].val) || 1
+
+  if (lineRenderProps.itemJson.props.ani_type.val === 'electricity') {
+    return stroke_width * 3
+  }
+
+  if (line_style.value === 'dashed') {
+    return `${stroke_width * 4} ${stroke_width * 2}`
+  }
+  if (line_style.value === 'dotted') {
+    return `0 ${stroke_width * 2.5}`
+  }
+  if (line_style.value === 'dash-dot') {
+    return `${stroke_width * 4} ${stroke_width * 2} ${stroke_width} ${stroke_width * 2}`
+  }
+  return 0
+})
+const line_stroke_linecap = computed(() => (line_style.value === 'dotted' ? 'round' : 'butt'))
 const onMouseDown = (de: MouseTouchEvent, point_index: number, item: { x: number; y: number }) => {
   de.stopPropagation()
   // 记录鼠标按下时实际点的坐标

+ 2 - 0
src/components/mt-edit/components/layout/right-aside/index.vue

@@ -2,6 +2,7 @@
   <div id="mt-right-aside" class="px-4">
     <select-item-setting
       v-if="globalStore.selected_items_id.length == 1"
+      :key="selected_item_id"
       v-model:item-json="globalStore.done_json[find_index_item_json]"
       :done-json="globalStore.done_json"
       @add-history="onAddHistory">
@@ -24,6 +25,7 @@ const slots = useSlots()
 const find_index_item_json = computed(() => {
   return globalStore.done_json.findIndex((f) => f.id == globalStore.selected_items_id[0])
 })
+const selected_item_id = computed(() => globalStore.selected_items_id[0] || '')
 const onAddHistory = () => {
   cacheStore.addHistory(globalStore.done_json)
 }

+ 16 - 2
src/components/mt-edit/components/layout/right-aside/select-item-props-setting.vue

@@ -1,6 +1,12 @@
 <template>
-  <div v-for="(attr_item, key) in selectItemPropsSettingProps.propsInfo" :key="key">
-    <el-form-item v-if="!attr_item.disabled" :label="attr_item.title" size="small">
+  <div
+    v-for="(attr_item, key) in selectItemPropsSettingProps.propsInfo"
+    :key="`${selectItemPropsSettingProps.itemId}-${key}`">
+    <el-form-item
+      v-if="!attr_item.disabled"
+      class="mt-edit-prop-form-item"
+      :label="attr_item.title"
+      size="small">
       <el-select
         v-if="attr_item.type === 'select' && !attr_item.disabled"
         v-model="attr_item.val"
@@ -55,7 +61,15 @@ import {
 } from 'element-plus'
 import JsonEdit from './json-edit.vue'
 type SelectItemPropsSettingProps = {
+  itemId?: string
   propsInfo: ILeftAsideConfigItemPublicProps | undefined
 }
 const selectItemPropsSettingProps = withDefaults(defineProps<SelectItemPropsSettingProps>(), {})
 </script>
+<style scoped>
+.mt-edit-prop-form-item :deep(.el-form-item__label) {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

+ 3 - 1
src/components/mt-edit/components/layout/right-aside/select-item-setting.vue

@@ -55,7 +55,9 @@
             <el-form-item v-if="!item_lock && !is_line" label="可旋转" size="small">
               <el-switch size="small" v-model="item_rotate" />
             </el-form-item>
-            <select-item-props-setting v-model:propsInfo="item_props" />
+            <select-item-props-setting
+              :item-id="selectItemSettingProps.itemJson.id"
+              v-model:propsInfo="item_props" />
           </el-form>
         </el-collapse-item>
         <el-collapse-item title="动画配置" name="2">

+ 23 - 8
src/components/mt-edit/components/line-render/index.vue

@@ -66,13 +66,10 @@
             : lineRenderProps.itemJson.props.stroke.val
         "
         :stroke-width="lineRenderProps.itemJson.props['stroke-width'].val"
+        :stroke-dasharray="line_dasharray"
+        :stroke-linecap="line_stroke_linecap"
         style="cursor: move"
         stroke-dashoffset="0"
-        :stroke-dasharray="
-          lineRenderProps.itemJson.props.ani_type.val === 'electricity'
-            ? lineRenderProps.itemJson.props['stroke-width'].val * 3
-            : 0
-        "
         :marker-start="
           lineRenderProps.itemJson.props?.['marker-start']?.val
             ? `url(#markerArrowStart${lineRenderProps.itemJson.id})`
@@ -163,9 +160,7 @@
         :r="lineRenderProps.itemJson.props['stroke-width'].val * 2"
         :fill="lineRenderProps.itemJson.props.ani_color.val">
         <animateMotion
-          :path="
-            line_path
-          "
+          :path="line_path"
           :dur="`${
             lineRenderProps.itemJson.props.ani_dur.val < 1
               ? 1
@@ -262,6 +257,26 @@ const arrow_marker_size = computed(() => {
   const stroke_width = Number(lineRenderProps.itemJson.props['stroke-width'].val) || 1
   return Math.min(Math.max(16 / stroke_width, 2.5), 6)
 })
+const line_style = computed(() => lineRenderProps.itemJson.props.lineStyle?.val || 'solid')
+const line_dasharray = computed(() => {
+  const stroke_width = Number(lineRenderProps.itemJson.props['stroke-width'].val) || 1
+
+  if (lineRenderProps.itemJson.props.ani_type.val === 'electricity') {
+    return stroke_width * 3
+  }
+
+  if (line_style.value === 'dashed') {
+    return `${stroke_width * 4} ${stroke_width * 2}`
+  }
+  if (line_style.value === 'dotted') {
+    return `0 ${stroke_width * 2.5}`
+  }
+  if (line_style.value === 'dash-dot') {
+    return `${stroke_width * 4} ${stroke_width * 2} ${stroke_width} ${stroke_width * 2}`
+  }
+  return 0
+})
+const line_stroke_linecap = computed(() => (line_style.value === 'dotted' ? 'round' : 'butt'))
 const line_path = computed(() =>
   positionArrarToPath(
     lineRenderProps.itemJson.props.point_position.val,

+ 4 - 0
src/components/mt-edit/components/render-core/index.vue

@@ -91,6 +91,7 @@ import CardVue from '@/components/custom-components/card-vue/index.vue'
 import NowTimeVue from '@/components/custom-components/now-time-vue/index.vue'
 import KvVue from '@/components/custom-components/kv-vue/index.vue'
 import SysButtonVue from '@/components/custom-components/sys-button-vue/index.vue'
+import WireframeVue from '@/components/custom-components/wireframe-vue/index.vue'
 import { ElPopover } from 'element-plus'
 const instance = getCurrentInstance()
 const now_include_keys = Object.keys(instance?.appContext?.components as any)
@@ -109,6 +110,9 @@ if (!now_include_keys.includes('kv-vue')) {
 if (!now_include_keys.includes('sys-button-vue')) {
   instance?.appContext.app.component('sys-button-vue', SysButtonVue)
 }
+if (!now_include_keys.includes('wireframe-vue')) {
+  instance?.appContext.app.component('wireframe-vue', WireframeVue)
+}
 type RenderCoreProps = {
   doneJson: IDoneJson[]
   canvasCfg: IGlobalStoreCanvasCfg

+ 39 - 4
src/components/mt-edit/index.vue

@@ -90,7 +90,8 @@
         :done-json="globalStore.done_json"
         :selected-items-id="globalStore.selected_items_id"
         @update-selected-items-id="onTreeUpdateSelectedItemsId"
-        @update-selected-id-hide="onDoneTreeUpdateSelectedIdHide" />
+        @update-selected-id-hide="onDoneTreeUpdateSelectedIdHide"
+        @move-layer="onDoneTreeMoveLayer" />
     </el-drawer>
   </div>
 </template>
@@ -110,7 +111,7 @@ import {
   ElButton,
   ElMessage
 } from 'element-plus'
-import { globalStore } from '@/components/mt-edit/store/global'
+import { globalStore, resetGlobalStore } from '@/components/mt-edit/store/global'
 import { computed, reactive, ref, useSlots } from 'vue'
 import DoneTree from '@/components/mt-edit/components/done-tree/index.vue'
 import { cacheStore } from './store/cache'
@@ -182,6 +183,26 @@ const onDoneTreeUpdateSelectedIdHide = (id: string) => {
     item.hide = !item.hide
   }
 }
+const onDoneTreeMoveLayer = ({ id, direction }: { id: string; direction: 'up' | 'down' }) => {
+  const index = globalStore.done_json.findIndex((f) => f.id === id)
+  if (index < 0) {
+    return
+  }
+
+  const targetIndex = direction === 'up' ? index + 1 : index - 1
+  if (targetIndex < 0 || targetIndex >= globalStore.done_json.length) {
+    ElMessage.error(direction === 'up' ? '已经是最上层了' : '已经是最下层了')
+    return
+  }
+
+  const doneJson = [...globalStore.done_json]
+  const temp = doneJson[index]
+  doneJson[index] = doneJson[targetIndex]
+  doneJson[targetIndex] = temp
+  globalStore.setGlobalStoreDoneJson(doneJson)
+  globalStore.setSingleSelect(id)
+  cacheStore.addHistory(globalStore.done_json)
+}
 const onAlignSelected = (
   type:
     | 'left'
@@ -242,11 +263,25 @@ const setImportJson = (exportJson: IExportJson) => {
   globalStore.canvasCfg = canvasCfg
   globalStore.gridCfg = gridCfg
   globalStore.setGlobalStoreDoneJson(importDoneJson)
-  cacheStore.history[0] = importDoneJson
+  cacheStore.history = [objectDeepClone(importDoneJson)]
+  cacheStore.historyIndex = 0
   return true
 }
+const reset = () => {
+  resetGlobalStore()
+  cacheStore.setBoundingBox([])
+  cacheStore.setCopy([])
+  cacheStore.adsorbPoint = []
+  cacheStore.history = [[]]
+  cacheStore.historyIndex = 0
+  import_visible.value = false
+  export_visible.value = false
+  done_json_tree_visiable.value = false
+  line_append_enable.value = false
+}
 defineExpose({
-  setImportJson
+  setImportJson,
+  reset
 })
 </script>
 <style scoped>

+ 182 - 4
src/components/mt-edit/store/config.ts

@@ -17,6 +17,29 @@ const sysComponentItems: ILeftAsideConfigItem[] = [
         type: 'number',
         val: 2
       },
+      lineStyle: {
+        title: '线条样式',
+        type: 'select',
+        val: 'solid',
+        options: [
+          {
+            label: '实线',
+            value: 'solid'
+          },
+          {
+            label: '虚线',
+            value: 'dashed'
+          },
+          {
+            label: '点线',
+            value: 'dotted'
+          },
+          {
+            label: '点划线',
+            value: 'dash-dot'
+          }
+        ]
+      },
       'marker-start': {
         title: '起点箭头',
         type: 'switch',
@@ -102,6 +125,29 @@ const sysComponentItems: ILeftAsideConfigItem[] = [
         type: 'number',
         val: 2
       },
+      lineStyle: {
+        title: '线条样式',
+        type: 'select',
+        val: 'solid',
+        options: [
+          {
+            label: '实线',
+            value: 'solid'
+          },
+          {
+            label: '虚线',
+            value: 'dashed'
+          },
+          {
+            label: '点线',
+            value: 'dotted'
+          },
+          {
+            label: '点划线',
+            value: 'dash-dot'
+          }
+        ]
+      },
       'marker-start': {
         title: '起点箭头',
         type: 'switch',
@@ -280,6 +326,71 @@ const sysComponentItems: ILeftAsideConfigItem[] = [
       repeat: 'infinite'
     }
   },
+  {
+    id: 'wireframe-vue',
+    title: '线框',
+    type: 'vue',
+    thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0Ij48cmVjdCB4PSIxNjAiIHk9IjIyNCIgd2lkdGg9IjcwNCIgaGVpZ2h0PSI1NzYiIHJ4PSI2NCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDA5ZWZmIiBzdHJva2Utd2lkdGg9IjY0IiBzdHJva2UtZGFzaGFycmF5PSIxMjAgNjQiLz48L3N2Zz4=`,
+    props: {
+      fill: {
+        title: '背景色',
+        type: 'color',
+        val: 'rgba(255, 255, 255, 0)',
+        showAlpha: true,
+        colorFormat: 'rgb'
+      },
+      borderColor: {
+        title: '边框颜色',
+        type: 'color',
+        val: '#409eff',
+        showAlpha: true,
+        colorFormat: 'rgb'
+      },
+      borderStyle: {
+        title: '边框样式',
+        type: 'select',
+        val: 'solid',
+        options: [
+          {
+            value: 'solid',
+            label: '实线'
+          },
+          {
+            value: 'dashed',
+            label: '虚线'
+          },
+          {
+            value: 'dotted',
+            label: '点线'
+          },
+          {
+            value: 'double',
+            label: '双线'
+          },
+          {
+            value: 'none',
+            label: '无'
+          }
+        ]
+      },
+      borderWidth: {
+        title: '边框宽度',
+        type: 'number',
+        val: 2
+      },
+      borderRadius: {
+        title: '圆角',
+        type: 'number',
+        val: 0
+      }
+    },
+    common_animations: {
+      val: '',
+      delay: 'delay-0s',
+      speed: 'slow',
+      repeat: 'infinite'
+    }
+  },
   {
     id: 'card-vue',
     title: '卡片',
@@ -361,18 +472,61 @@ const sysComponentItems: ILeftAsideConfigItem[] = [
         type: 'switch',
         val: true
       },
+      backgroundColor: {
+        title: '背景色',
+        type: 'color',
+        val: 'rgba(255, 255, 255, 0)',
+        showAlpha: true,
+        colorFormat: 'rgb'
+      },
       fontFamily: {
         title: '字体',
         type: 'select',
-        val: '黑体',
+        val: 'Microsoft YaHei, PingFang SC, sans-serif',
         options: [
           {
-            value: '黑体',
-            label: '黑'
+            value: 'Microsoft YaHei, PingFang SC, sans-serif',
+            label: '微软雅黑'
           },
           {
-            value: '宋体',
+            value: 'SimSun, Songti SC, serif',
             label: '宋体'
+          },
+          {
+            value: 'SimHei, Heiti SC, sans-serif',
+            label: '黑体'
+          },
+          {
+            value: 'PingFang SC, Microsoft YaHei, sans-serif',
+            label: '苹方'
+          },
+          {
+            value: 'Noto Sans SC, Microsoft YaHei, sans-serif',
+            label: '思源黑体'
+          },
+          {
+            value: 'YouSheBiaoTiHei, Microsoft YaHei, sans-serif',
+            label: '优设标题黑'
+          },
+          {
+            value: 'KaiTi, Kaiti SC, serif',
+            label: '楷体'
+          },
+          {
+            value: 'FangSong, STFangsong, serif',
+            label: '仿宋'
+          },
+          {
+            value: 'Arial, Helvetica, sans-serif',
+            label: 'Arial'
+          },
+          {
+            value: 'DIN Alternate, Arial Narrow, Arial, sans-serif',
+            label: 'DIN 数字'
+          },
+          {
+            value: 'Consolas, Monaco, monospace',
+            label: '等宽字体'
           }
         ]
       },
@@ -381,6 +535,25 @@ const sysComponentItems: ILeftAsideConfigItem[] = [
         type: 'number',
         val: 18
       },
+      textAlign: {
+        title: '文字对齐',
+        type: 'select',
+        val: 'left',
+        options: [
+          {
+            value: 'left',
+            label: '左对齐'
+          },
+          {
+            value: 'center',
+            label: '居中'
+          },
+          {
+            value: 'right',
+            label: '右对齐'
+          }
+        ]
+      },
       label: {
         title: '键名',
         type: 'input',
@@ -410,6 +583,11 @@ const sysComponentItems: ILeftAsideConfigItem[] = [
         title: '边框颜色',
         type: 'color',
         val: '#000000'
+      },
+      borderRadius: {
+        title: '圆角',
+        type: 'number',
+        val: 0
       }
     },
     common_animations: {

+ 58 - 23
src/components/mt-edit/store/global.ts

@@ -3,37 +3,44 @@ import type {
   GlobalStoreIntention,
   IDoneJson,
   IGlobalStore,
+  IGlobalStoreCanvasCfg,
   IGlobalStoreCreateItemInfo,
+  IGlobalStoreGridCfg,
   IRealTimeData
 } from './types'
+
+export const createDefaultCanvasCfg = (): IGlobalStoreCanvasCfg => ({
+  width: 1920,
+  height: 1080,
+  scale: 1,
+  color: '',
+  img: '',
+  guide: true,
+  adsorp: true,
+  adsorp_diff: 5,
+  transform_origin: {
+    x: 0,
+    y: 0
+  },
+  drag_offset: {
+    x: 0,
+    y: 0
+  }
+})
+
+export const createDefaultGridCfg = (): IGlobalStoreGridCfg => ({
+  enabled: true,
+  align: true,
+  size: 10
+})
+
 export const globalStore: IGlobalStore = reactive({
   intention: 'none',
   create_item_info: null,
   selected_items_id: [],
   done_json: [],
-  canvasCfg: {
-    width: 1920,
-    height: 1080,
-    scale: 1,
-    color: '',
-    img: '',
-    guide: true,
-    adsorp: true,
-    adsorp_diff: 5,
-    transform_origin: {
-      x: 0,
-      y: 0
-    },
-    drag_offset: {
-      x: 0,
-      y: 0
-    }
-  },
-  gridCfg: {
-    enabled: true,
-    align: true,
-    size: 10
-  },
+  canvasCfg: createDefaultCanvasCfg(),
+  gridCfg: createDefaultGridCfg(),
   guideCfg: {
     x: {
       display: false,
@@ -110,3 +117,31 @@ export const globalStore: IGlobalStore = reactive({
     globalStore.real_time_data = val
   }
 })
+
+export const resetGlobalStore = () => {
+  globalStore.intention = 'none'
+  globalStore.create_item_info = null
+  globalStore.selected_items_id = []
+  globalStore.done_json = []
+  globalStore.canvasCfg = createDefaultCanvasCfg()
+  globalStore.gridCfg = createDefaultGridCfg()
+  globalStore.guideCfg = {
+    x: {
+      display: false,
+      top: 0
+    },
+    y: {
+      display: false,
+      left: 0
+    }
+  }
+  globalStore.lock = false
+  globalStore.real_time_data = {
+    show: false,
+    text: ''
+  }
+  globalStore.adsorp_diff = {
+    x: 0,
+    y: 0
+  }
+}

+ 12 - 2
src/views/maotu/edit.vue

@@ -10,7 +10,7 @@ import { MtEdit } from '@/export'
 import { Canvg } from 'canvg'
 import html2canvas from 'html2canvas'
 import { ElMessage } from 'element-plus'
-import { nextTick, onMounted, ref } from 'vue'
+import { nextTick, onMounted, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import DeviceBindPanel from './components/DeviceBindPanel.vue'
 const route = useRoute()
@@ -54,6 +54,14 @@ const loadProject = async () => {
   }
 }
 
+const resetAndLoadProject = async () => {
+  await nextTick()
+  MtEditRef.value?.reset()
+  projectDetail.value = undefined
+  dataModelParseError.value = ''
+  await loadProject()
+}
+
 const genThumbnailDataUrl = async (canvasId = 'mtCanvasArea') => {
   const el = document.querySelector<HTMLElement>(`#${canvasId}`)
   if (!el) {
@@ -152,8 +160,10 @@ const onThumbnailClick = () => {
 }
 
 onMounted(() => {
-  loadProject()
+  resetAndLoadProject()
 })
+
+watch(() => route.params.id, resetAndLoadProject)
 </script>
 
 <template>