Alsmile 2 年 前
コミット
6a289048d0

+ 25 - 28
src/services/common.ts

@@ -1,28 +1,13 @@
-import { ref } from 'vue';
-import { useRouter, useRoute } from 'vue-router';
+import { reactive, ref } from 'vue';
 import router from '@/router/index';
 import { useUser } from '@/services/user';
-import {
-  showNotification,
-  Meta2dBackData,
-  dealwithFormatbeforeOpen,
-  gotoAccount,
-  checkData,
-} from '@/services/utils';
-import { NotifyPlugin, MessagePlugin } from 'tdesign-vue-next';
+import { showNotification, Meta2dBackData, checkData } from '@/services/utils';
+import { MessagePlugin } from 'tdesign-vue-next';
 import localforage from 'localforage';
 import { noLoginTip, localMeta2dDataName } from '@/services/utils';
-import { readFile, upload, dataURLtoBlob } from '@/services/file';
-import {
-  delImage,
-  getFolders,
-  addCollection,
-  updateCollection,
-  updateFolders,
-  cdn,
-  upCdn,
-} from '@/services/api';
-import { compareVersion, baseVer, upgrade } from '@/services/upgrade';
+import { upload, dataURLtoBlob } from '@/services/file';
+import { delImage, addCollection, updateCollection } from '@/services/api';
+import { baseVer } from '@/services/upgrade';
 
 const dot = ref(false);
 
@@ -32,6 +17,9 @@ export const useDot = () => {
   };
   const setDot = async (value: boolean) => {
     dot.value = value;
+    if (value) {
+      tree.patch = true;
+    }
   };
 
   return {
@@ -406,17 +394,26 @@ export const newfile = async (noRouter: boolean = false) => {
     });
 };
 
+const tree = reactive({
+  list: [],
+  patch: true,
+});
+
 export const getPenTree = () => {
-  const tree = [];
-  for (const item of meta2d.store.data.pens) {
-    if (item.parentId) {
-      continue;
+  if (tree.patch) {
+    tree.patch = false;
+    const list = [];
+    for (const item of meta2d.store.data.pens) {
+      if (item.parentId) {
+        continue;
+      }
+      const elem = calcElem(item);
+      elem && list.push(elem);
     }
-    const elem = calcElem(item);
-    elem && tree.push(elem);
+    tree.list = list;
   }
 
-  return tree;
+  return tree.list;
 };
 
 const calcElem = (node: any) => {

+ 1 - 0
src/styles/var.css

@@ -64,6 +64,7 @@
   --td-bg-color-component: var(--color-border-input);
   --td-brand-color-light: var(--color-border-input);
   --td-mask-disabled: var(--color-background);
+  --td-text-color-disabled: var(--color-gray);
 
   --color-dialog-border: transparent;
 

+ 328 - 0
src/views/components/Actions.vue

@@ -0,0 +1,328 @@
+<template>
+  <div class="props">
+    <div v-for="(a, index) in data.actions" class="mb-12">
+      <div class="flex middle between">
+        <div class="flex middle">
+          <t-icon name="arrow-right" class="mr-4" />
+          动作{{ index + 1 }}
+        </div>
+        <t-icon
+          name="close"
+          class="hover"
+          @click="data.actions.splice(index, 1)"
+        />
+      </div>
+      <div class="px-16 py-4">
+        <div class="form-item mt-4">
+          <label>动作类型</label>
+          <t-select
+            v-model="a.action"
+            placeholder="请选择"
+            @change="onChangeAction(a)"
+          >
+            <t-option
+              v-for="option in actionOptions"
+              :key="option.value"
+              :value="option.value"
+              :label="option.label"
+            />
+          </t-select>
+        </div>
+        <template v-if="a.action == 0">
+          <div class="form-item mt-8">
+            <label>链接地址</label>
+            <t-input v-model="a.value" placeholder="URL" />
+          </div>
+          <div class="form-item mt-8">
+            <label>打开方式</label>
+            <t-radio-group v-model="a.params">
+              <t-radio value="_blank">新页面</t-radio>
+              <t-radio value="_self">当前页面</t-radio>
+            </t-radio-group>
+          </div>
+        </template>
+        <template v-if="a.action == 13">
+          <div class="form-item mt-8">
+            <label>视图</label>
+            <t-input v-model="a.value" placeholder="ID" />
+          </div>
+        </template>
+        <template v-if="a.action == 2 || a.action == 3 || a.action == 4">
+          <div class="form-item mt-8">
+            <label>对象类型</label>
+            <t-radio-group v-model="a.targetType" @change="a.value = ''">
+              <t-radio value="id">图元</t-radio>
+              <t-radio value="tag">组</t-radio>
+            </t-radio-group>
+          </div>
+          <div class="form-item mt-8">
+            <label>播放对象</label>
+            <t-tree-select
+              v-if="a.targetType === 'id'"
+              v-model="a.value"
+              :data="penTree"
+              filterable
+              placeholder="默认自己"
+            />
+            <t-select
+              v-else
+              v-model="a.value"
+              :options="groups"
+              placeholder="组"
+            />
+          </div>
+        </template>
+        <template v-if="a.action == 1">
+          <div class="form-item mt-8">
+            <label>对象类型</label>
+            <t-radio-group v-model="a.targetType" @change="a.params = ''">
+              <t-radio value="id">图元</t-radio>
+              <t-radio value="tag">组</t-radio>
+            </t-radio-group>
+          </div>
+          <div class="form-item mt-8">
+            <label>更改对象</label>
+            <t-tree-select
+              v-if="a.targetType === 'id'"
+              v-model="a.params"
+              :data="penTree"
+              filterable
+              placeholder="默认自己"
+              @change="getProps(a)"
+            />
+            <t-select
+              v-else
+              v-model="a.params"
+              :options="groups"
+              placeholder="组"
+            />
+          </div>
+          <div class="form-item mt-8">
+            <label>属性数据</label>
+            <div class="w-full">
+              <div class="prop-grid head">
+                <div>属性名</div>
+                <div>属性值</div>
+                <div>
+                  <t-dropdown :min-column-width="100">
+                    <t-icon name="add-circle" class="hover" />
+
+                    <t-dropdown-menu>
+                      <t-dropdown-item
+                        v-for="prop in a.props"
+                        :key="prop.value"
+                        :value="prop.value"
+                      >
+                        {{ prop.label }}
+                      </t-dropdown-item>
+
+                      <t-dropdown-item key="custom" value="custom">
+                        <t-input />
+                      </t-dropdown-item>
+                    </t-dropdown-menu>
+                  </t-dropdown>
+                </div>
+              </div>
+              <template
+                v-if="Object.keys(a.value).length"
+                class="center gray mt-8"
+              ></template>
+              <div v-else class="center gray mt-8">暂无数据</div>
+            </div>
+          </div>
+        </template>
+      </div>
+    </div>
+    <div class="mt-8">
+      <a @click="data.actions.push({})"> + 添加动作 </a>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onBeforeMount, ref } from 'vue';
+
+import { getPenTree } from '@/services/common';
+
+const { data } = defineProps<{
+  data: any;
+}>();
+
+const actionOptions = [
+  {
+    label: '打开链接',
+    value: 0,
+  },
+  {
+    label: '打开视图',
+    value: 13,
+  },
+  {
+    label: '播放动画',
+    value: 2,
+  },
+  {
+    label: '暂停动画',
+    value: 3,
+  },
+  {
+    label: '停止动画',
+    value: 4,
+  },
+  {
+    label: '更改属性',
+    value: 1,
+  },
+  {
+    label: '打开弹框',
+    value: 14,
+  },
+  {
+    label: '发送消息',
+    value: 7,
+  },
+  {
+    label: '发送MQTT',
+    value: 15,
+  },
+  {
+    label: '发送Websocket',
+    value: 16,
+  },
+  {
+    label: '发送HTTP(s)',
+    value: 17,
+  },
+  {
+    label: '播放视频',
+    value: 8,
+  },
+  {
+    label: '暂停视频',
+    value: 9,
+  },
+  {
+    label: '停止视频',
+    value: 10,
+  },
+  {
+    label: '自定义函数',
+    value: 18,
+  },
+];
+
+const penTree: any = ref([]);
+const groups: any = ref([]);
+
+onBeforeMount(() => {
+  if (!data.actions) {
+    data.actions = [];
+  }
+
+  penTree.value = getPenTree();
+  groups.value = [];
+  const d = meta2d.store.data as any;
+  if (d.groups) {
+    for (const item of d.groups) {
+      groups.value.push({ label: item, value: item });
+    }
+  }
+});
+
+const onChangeAction = (action: any) => {
+  switch (action.action) {
+    case 0:
+      action.value = '';
+      action.params = '_blank';
+      break;
+    case 1:
+      action.params = '';
+      action.value = {};
+      action.targetType = 'id';
+      getProps(action);
+      break;
+    case 2:
+    case 3:
+    case 4:
+      action.value = '';
+      action.targetType = 'id';
+      break;
+
+    default:
+      break;
+  }
+};
+
+const getProps = (c: any) => {
+  c.props = [
+    {
+      value: 'x',
+      label: 'X',
+    },
+    {
+      value: 'y',
+      label: 'Y',
+    },
+    {
+      value: 'width',
+      label: '宽',
+    },
+    {
+      value: 'height',
+      label: '高',
+    },
+    {
+      value: 'visible',
+      label: '显示',
+    },
+    {
+      value: 'text',
+      label: '文字',
+    },
+    {
+      value: 'progress',
+      label: '进度',
+    },
+    {
+      value: 'showChild',
+      label: '状态',
+    },
+    {
+      value: 'rotate',
+      label: '旋转',
+    },
+  ];
+
+  let target: any;
+  if (c.params) {
+    target = meta2d.findOne(c.params);
+  } else if (meta2d.store.active) {
+    target = meta2d.store.active[0];
+  }
+  if (target) {
+    for (const item of target.realTimes) {
+      const found = c.props.findIndex((elem: any) => elem.value === item.key);
+      if (found < 0) {
+        c.props.push({
+          value: item.key,
+          label: item.label,
+        });
+      }
+    }
+  }
+};
+</script>
+<style lang="postcss" scoped>
+.props {
+  .prop-grid {
+    display: grid;
+    grid-template-columns: 2fr 3fr 20px;
+    line-height: 30px;
+
+    &.head {
+      background: var(--color-background-input);
+      padding: 0 8px;
+    }
+  }
+}
+</style>

+ 155 - 171
src/views/components/Header.vue

@@ -114,30 +114,13 @@
             </div>
           </a>
         </t-dropdown-item>
-        <t-dropdown-item divider="true">
+        <t-dropdown-item>
           <a @click="onPaste">
             <div class="flex">
               粘贴 <span class="flex-grow"></span> Ctrl + V
             </div>
           </a>
         </t-dropdown-item>
-        <!-- <t-dropdown-item>
-          <a @click="onAddAnchorHand">
-            <div class="flex">添加手柄 <span class="flex-grow"></span> H</div>
-          </a>
-        </t-dropdown-item>
-        <t-dropdown-item>
-          <a @click="onRemoveAnchorHand">
-            <div class="flex">删除手柄 <span class="flex-grow"></span> D</div>
-          </a>
-        </t-dropdown-item>
-        <t-dropdown-item>
-          <a @click="onToggleAnchorHand">
-            <div class="flex">
-              切换手柄 <span class="flex-grow"></span> Shift
-            </div>
-          </a>
-        </t-dropdown-item> -->
       </t-dropdown-menu>
     </t-dropdown>
     <t-dropdown
@@ -194,9 +177,12 @@
         </t-dropdown-item>
         <t-dropdown-item>
           <a @click="onToggleAnchor">
-            <div class="flex"  :style="{
-              color:showAnchor?'':'#4f5b75'}
-              ">
+            <div
+              class="flex"
+              :style="{
+                color: showAnchor ? '' : '#4f5b75',
+              }"
+            >
               添加/删除锚点 <span class="flex-grow"></span> A
             </div>
           </a>
@@ -270,22 +256,22 @@
 </template>
 
 <script lang="ts" setup>
-import { reactive, ref, onMounted, onUnmounted, nextTick } from "vue";
-import { useRouter, useRoute } from "vue-router";
-import { useUser } from "@/services/user";
-import { NotifyPlugin, MessagePlugin } from "tdesign-vue-next";
+import { reactive, ref, onMounted, onUnmounted, nextTick } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { useUser } from '@/services/user';
+import { NotifyPlugin, MessagePlugin } from 'tdesign-vue-next';
 import {
   showNotification,
   Meta2dBackData,
   dealwithFormatbeforeOpen,
   gotoAccount,
   checkData,
-} from "@/services/utils";
-import { readFile, upload, dataURLtoBlob } from "@/services/file";
-import { compareVersion, baseVer, upgrade } from "@/services/upgrade";
-import { parseSvg } from "@meta2d/svg";
-import { Pen, Rect, getGlobalColor, isShowChild } from "@meta2d/core";
-import localforage from "localforage";
+} from '@/services/utils';
+import { readFile, upload, dataURLtoBlob } from '@/services/file';
+import { compareVersion, baseVer, upgrade } from '@/services/upgrade';
+import { parseSvg } from '@meta2d/svg';
+import { Pen, Rect, getGlobalColor, isShowChild } from '@meta2d/core';
+import localforage from 'localforage';
 import {
   delImage,
   getFolders,
@@ -294,12 +280,12 @@ import {
   updateFolders,
   cdn,
   upCdn,
-} from "@/services/api";
-import JSZip from "jszip";
-import axios from "axios";
-import { switchTheme } from "@/services/theme";
-import { noLoginTip, localMeta2dDataName } from "@/services/utils";
-import { useDot, notificFn } from "@/services/common";
+} from '@/services/api';
+import JSZip from 'jszip';
+import axios from 'axios';
+import { switchTheme } from '@/services/theme';
+import { noLoginTip, localMeta2dDataName } from '@/services/utils';
+import { useDot, notificFn } from '@/services/common';
 import {
   save,
   newFile,
@@ -312,19 +298,19 @@ import {
   title,
   drawPen,
   map,
-  magnifier
-} from "@/services/common";
+  magnifier,
+} from '@/services/common';
 
 const router = useRouter();
 const route = useRoute();
 
 const market = import.meta.env.VITE_MARKET;
-const baseUrl = import.meta.env.BASE_URL || "/";
+const baseUrl = import.meta.env.BASE_URL || '/';
 
 const { user, message, getUser, getMessage, signout } = useUser();
 const { dot } = useDot();
 const data = reactive({
-  name: "空白文件",
+  name: '空白文件',
 });
 
 const inputMeta2dName = () => {
@@ -332,15 +318,15 @@ const inputMeta2dName = () => {
 };
 
 const initMeta2dName = () => {
-  data.name = (meta2d.store.data as Meta2dBackData).name || "";
+  data.name = (meta2d.store.data as Meta2dBackData).name || '';
 };
 
 nextTick(() => {
-  meta2d.on("opened", initMeta2dName);
+  meta2d.on('opened', initMeta2dName);
 });
 
 onUnmounted(() => {
-  meta2d.off("opened", initMeta2dName);
+  meta2d.off('opened', initMeta2dName);
 });
 
 function login() {
@@ -359,24 +345,24 @@ function login() {
 }
 
 function load(newT: boolean = false) {
-  const input = document.createElement("input");
-  input.type = "file";
+  const input = document.createElement('input');
+  input.type = 'file';
   input.onchange = (event) => {
     const elem = event.target as HTMLInputElement;
     if (elem.files && elem.files[0]) {
       newT && newfile(true);
       // 路由跳转 可能在 openFile 后执行
-      if (elem.files[0].name.endsWith(".json")) {
+      if (elem.files[0].name.endsWith('.json')) {
         openJson(elem.files[0], newT);
-      } else if (elem.files[0].name.endsWith(".svg")) {
+      } else if (elem.files[0].name.endsWith('.svg')) {
         MessagePlugin.info(
-          "可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能"
+          '可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能'
         );
         openSvg(elem.files[0]);
-      } else if (elem.files[0].name.endsWith(".zip")) {
+      } else if (elem.files[0].name.endsWith('.zip')) {
         openZip(elem.files[0], newT);
       } else {
-        MessagePlugin.info("打开文件只支持 json,svg,zip 格式");
+        MessagePlugin.info('打开文件只支持 json,svg,zip 格式');
       }
     }
   };
@@ -388,7 +374,7 @@ const openJson = async (file: File, isNew: boolean = false) => {
   try {
     let data: Meta2dBackData = JSON.parse(text);
     if (!data.name) {
-      data.name = file.name.replace(".json", "");
+      data.name = file.name.replace('.json', '');
     }
     if (!data.version || compareVersion(data.version, baseVer) === -1) {
       // 如果版本号不存在或者版本号 version < 1.0.0
@@ -416,7 +402,7 @@ const openSvg = async (file: File) => {
   const text = await readFile(file);
   const pens: Pen[] = parseSvg(text);
   meta2d.canvas.addCaches = pens;
-  MessagePlugin.info("svg转换成功,请点击画布决定放置位置");
+  MessagePlugin.info('svg转换成功,请点击画布决定放置位置');
 };
 
 const openZip = async (file: File, isNew: boolean = false) => {
@@ -431,16 +417,16 @@ const openZip = async (file: File, isNew: boolean = false) => {
     return;
   }
 
-  const { default: JSZip } = await import("jszip");
+  const { default: JSZip } = await import('jszip');
   const zip = new JSZip();
   await zip.loadAsync(file);
 
-  let dataStr = "";
+  let dataStr = '';
   for (const key in zip.files) {
     if (zip.files[key].dir) {
       continue;
     }
-    if (key.endsWith(".json")) {
+    if (key.endsWith('.json')) {
       // 认为只有一个 json 文件
       // dataStr = await zip.file(key).async('string');
       break;
@@ -462,15 +448,15 @@ const openZip = async (file: File, isNew: boolean = false) => {
     let _keyLower = key.toLowerCase();
     // if (!key.endsWith('.json') && (_png !== -1 || _img !== -1 || _image !== -1 || _file !== -1)) {
     if (
-      _keyLower.endsWith(".png") ||
-      _keyLower.endsWith(".svg") ||
-      _keyLower.endsWith(".gif") ||
-      _keyLower.endsWith(".jpg") ||
-      _keyLower.endsWith(".jpeg")
+      _keyLower.endsWith('.png') ||
+      _keyLower.endsWith('.svg') ||
+      _keyLower.endsWith('.gif') ||
+      _keyLower.endsWith('.jpg') ||
+      _keyLower.endsWith('.jpeg')
     ) {
-      let filename = key.substr(key.lastIndexOf("/") + 1);
-      const extPos = filename.lastIndexOf(".");
-      let ext = "";
+      let filename = key.substr(key.lastIndexOf('/') + 1);
+      const extPos = filename.lastIndexOf('.');
+      let ext = '';
       if (extPos > 0) {
         ext = filename.substr(extPos);
       }
@@ -521,7 +507,7 @@ const openZip = async (file: File, isNew: boolean = false) => {
     let data: Meta2dBackData = JSON.parse(dataStr);
     if (data) {
       if (!data.name) {
-        data.name = file.name.replace(".zip", "");
+        data.name = file.name.replace('.zip', '');
       }
       if (!data.version || compareVersion(data.version, baseVer) === -1) {
         // 如果版本号不存在或者版本号 version < 1.0.0
@@ -566,15 +552,15 @@ const downloadJson = () => {
   const data: Meta2dBackData = meta2d.data();
   if (data._id) delete data._id;
   checkData(data);
-  import("file-saver").then(({ saveAs }) => {
+  import('file-saver').then(({ saveAs }) => {
     saveAs(
       new Blob(
-        [JSON.stringify(data).replaceAll(cdn, "").replaceAll(upCdn, "")],
+        [JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')],
         {
-          type: "text/plain;charset=utf-8",
+          type: 'text/plain;charset=utf-8',
         }
       ),
-      `${data.name || "le5le.meta2d"}.json`
+      `${data.name || 'le5le.meta2d'}.json`
     );
   });
 };
@@ -590,27 +576,27 @@ const downloadZip = async () => {
     return;
   }
 
-  MessagePlugin.info("正在下载打包中,可能需要几分钟,请耐心等待...");
+  MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
   const [{ default: JSZip }, { saveAs }] = await Promise.all([
-    import("jszip"),
-    import("file-saver"),
+    import('jszip'),
+    import('file-saver'),
   ]);
 
   const zip: any = new JSZip();
   const data: Meta2dBackData = meta2d.data();
   let _fileName =
-    (data.name && data.name.replace(/\//g, "_").replace(/:/g, "_")) ||
-    "le5le.meta2d";
+    (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
+    'le5le.meta2d';
   const _zip = zip.folder(`${_fileName}`);
   if (data._id) delete data._id;
   checkData(data);
   _zip.file(
     `${_fileName}.json`,
-    JSON.stringify(data).replaceAll(cdn, "").replaceAll(upCdn, "")
+    JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
   );
   await zipImages(_zip, meta2d.store.data.pens);
 
-  const blob = await zip.generateAsync({ type: "blob" });
+  const blob = await zip.generateAsync({ type: 'blob' });
   saveAs(blob, `${_fileName}.zip`);
 };
 
@@ -625,28 +611,28 @@ const downloadHtml = async () => {
     return;
   }
 
-  MessagePlugin.info("正在下载打包中,可能需要几分钟,请耐心等待...");
+  MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
 
   const data: Meta2dBackData = meta2d.data();
   if (data._id) delete data._id;
   checkData(data);
   const [{ default: JSZip }, { saveAs }] = await Promise.all([
-    import("jszip"),
-    import("file-saver"),
+    import('jszip'),
+    import('file-saver'),
   ]);
   const zip = new JSZip();
   let _fileName =
-    (data.name && data.name.replace(/\//g, "_").replace(/:/g, "_")) ||
-    "le5le.meta2d";
+    (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
+    'le5le.meta2d';
 
   //处理cdn图片地址
   const _zip: any = zip.folder(`${_fileName}`);
   _zip.file(
-    "data.json",
-    JSON.stringify(data).replaceAll(cdn, "").replaceAll(upCdn, "")
+    'data.json',
+    JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
   );
   await Promise.all([zipImages(_zip, meta2d.store.data.pens), zipFiles(_zip)]);
-  const blob = await zip.generateAsync({ type: "blob" });
+  const blob = await zip.generateAsync({ type: 'blob' });
   saveAs(blob, `${_fileName}.zip`);
 };
 
@@ -679,23 +665,23 @@ async function downloadAsFrame(type: Frame) {
     return;
   }
 
-  MessagePlugin.info("正在下载打包中,可能需要几分钟,请耐心等待...");
+  MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
 
   const data: Meta2dBackData = meta2d.data();
   if (data._id) delete data._id;
   checkData(data);
   const [{ default: JSZip }, { saveAs }] = await Promise.all([
-    import("jszip"),
-    import("file-saver"),
+    import('jszip'),
+    import('file-saver'),
   ]);
   const zip = new JSZip();
   let _fileName =
-    (data.name && data.name.replace(/\//g, "_").replace(/:/g, "_")) ||
-    "le5le.meta2d";
+    (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
+    'le5le.meta2d';
   const _zip: any = zip.folder(`${_fileName}`);
   _zip.file(
-    "data.json",
-    JSON.stringify(data).replaceAll(cdn, "").replaceAll(upCdn, "")
+    'data.json',
+    JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
   );
   await Promise.all([
     zipImages(_zip, meta2d.store.data.pens),
@@ -705,18 +691,18 @@ async function downloadAsFrame(type: Frame) {
       ? zipVue2Files(_zip)
       : zipReactFiles(_zip),
   ]);
-  const blob = await zip.generateAsync({ type: "blob" });
+  const blob = await zip.generateAsync({ type: 'blob' });
   saveAs(blob, `${_fileName}.zip`);
 }
 
 async function zipVue3Files(zip: JSZip) {
   const files = [
-    "/view/js/marked.min.js",
-    "/view/js/lcjs.iife.js",
-    "/view/vue3/Meta2d.vue",
-    "/view/index.html",
-    "/view/js/meta2d.js",
-    "/view/使用说明.md",
+    '/view/js/marked.min.js',
+    '/view/js/lcjs.iife.js',
+    '/view/vue3/Meta2d.vue',
+    '/view/index.html',
+    '/view/js/meta2d.js',
+    '/view/使用说明.md',
   ] as const;
   // 文件同时加载
   await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
@@ -724,12 +710,12 @@ async function zipVue3Files(zip: JSZip) {
 
 async function zipVue2Files(zip: JSZip) {
   const files = [
-    "/view/js/marked.min.js",
-    "/view/js/lcjs.iife.js",
-    "/view/vue2/Meta2d.vue",
-    "/view/index.html",
-    "/view/js/meta2d.js",
-    "/view/使用说明.md",
+    '/view/js/marked.min.js',
+    '/view/js/lcjs.iife.js',
+    '/view/vue2/Meta2d.vue',
+    '/view/index.html',
+    '/view/js/meta2d.js',
+    '/view/使用说明.md',
   ] as const;
   // 文件同时加载
   await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
@@ -737,13 +723,13 @@ async function zipVue2Files(zip: JSZip) {
 
 async function zipReactFiles(zip: JSZip) {
   const files = [
-    "/view/js/marked.min.js",
-    "/view/js/lcjs.iife.js",
-    "/view/react/Meta2d.jsx",
-    "/view/react/Meta2d.css",
-    "/view/index.html",
-    "/view/js/meta2d.js",
-    "/view/使用说明.md",
+    '/view/js/marked.min.js',
+    '/view/js/lcjs.iife.js',
+    '/view/react/Meta2d.jsx',
+    '/view/react/Meta2d.css',
+    '/view/index.html',
+    '/view/js/meta2d.js',
+    '/view/使用说明.md',
   ] as const;
   // 文件同时加载
   await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
@@ -751,24 +737,24 @@ async function zipReactFiles(zip: JSZip) {
 
 async function zipFiles(zip: JSZip) {
   const files = [
-    "/view/js/marked.min.js",
-    "/view/js/lcjs.iife.js",
-    "/view/js/index.js",
-    "/view/js/meta2d.js",
-    "/view/index.html",
-    "/view/index.css",
-    "/view/favicon.ico",
-    "/view/使用说明.pdf",
+    '/view/js/marked.min.js',
+    '/view/js/lcjs.iife.js',
+    '/view/js/index.js',
+    '/view/js/meta2d.js',
+    '/view/index.html',
+    '/view/index.css',
+    '/view/favicon.ico',
+    '/view/使用说明.pdf',
   ] as const;
   // 文件同时加载
   await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
 }
 
 async function zipFile(zip: JSZip, filePath: string) {
-  const res: Blob = await axios.get(cdn + "/2d" + filePath, {
-    responseType: "blob",
+  const res: Blob = await axios.get(cdn + '/2d' + filePath, {
+    responseType: 'blob',
   });
-  zip.file(filePath.replace("/view", ""), res, { createFolders: true });
+  zip.file(filePath.replace('/view', ''), res, { createFolders: true });
 }
 
 /**
@@ -783,10 +769,10 @@ async function zipImages(zip: JSZip, pens: Pen[]) {
   // 不止 image 上有图片, strokeImage ,backgroundImage 也有图片
   const imageKeys = [
     {
-      string: "image",
+      string: 'image',
     },
-    { string: "strokeImage" },
-    { string: "backgroundImage" },
+    { string: 'strokeImage' },
+    { string: 'backgroundImage' },
   ] as const;
   const images: string[] = [];
   for (const pen of pens) {
@@ -795,7 +781,7 @@ async function zipImages(zip: JSZip, pens: Pen[]) {
       if (image) {
         // HTMLImageElement 无法精确控制图片格式
         if (
-          image.startsWith("/") ||
+          image.startsWith('/') ||
           image.startsWith(cdn) ||
           image.startsWith(upCdn)
         ) {
@@ -813,23 +799,23 @@ async function zipImages(zip: JSZip, pens: Pen[]) {
 
 async function zipImage(zip: JSZip, image: string) {
   const res: Blob = await axios.get(image, {
-    responseType: "blob",
+    responseType: 'blob',
     params: {
       isZip: true,
     },
   });
-  zip.file(cdn ? image.replace(cdn, "").replace(upCdn, "") : image, res, {
+  zip.file(cdn ? image.replace(cdn, '').replace(upCdn, '') : image, res, {
     createFolders: true,
   });
 }
 
 const downloadImageTips =
-  "无法下载,宽度不合法,画布可能没有画笔/画布大小超出浏览器最大限制";
+  '无法下载,宽度不合法,画布可能没有画笔/画布大小超出浏览器最大限制';
 
 const downloadPng = () => {
   const name = (meta2d.store.data as Meta2dBackData).name;
   try {
-    meta2d.downloadPng(name ? name + ".png" : undefined);
+    meta2d.downloadPng(name ? name + '.png' : undefined);
   } catch (e) {
     MessagePlugin.warning(downloadImageTips);
   }
@@ -843,9 +829,9 @@ async function getIconDefs(url: string) {
 }
 
 const downloadSvg = async () => {
-  await import("@/assets/canvas2svg");
+  await import('@/assets/canvas2svg');
   if (!C2S) {
-    MessagePlugin.error("请先加载乐吾乐官网下的canvas2svg.js");
+    MessagePlugin.error('请先加载乐吾乐官网下的canvas2svg.js');
     return;
   }
 
@@ -857,7 +843,7 @@ const downloadSvg = async () => {
   rect.x -= 10;
   rect.y -= 10;
   const ctx = new C2S(rect.width + 20, rect.height + 20);
-  ctx.textBaseline = "middle";
+  ctx.textBaseline = 'middle';
   ctx.strokeStyle = getGlobalColor(meta2d.store);
   for (const pen of meta2d.store.data.pens) {
     // 不使用 calculative.inView 的原因是,如果 pen 在 view 之外,那么它的 calculative.inView 为 false,但是它的绘制还是需要的
@@ -873,18 +859,18 @@ const downloadSvg = async () => {
   );
   if (icon_pens && icon_pens.length > 0) {
     let iconList = [
-      "/icon/国家电网/iconfont.css",
-      "/icon/电气工程/iconfont.css",
-      "/icon/通用图标/iconfont.css",
+      '/icon/国家电网/iconfont.css',
+      '/icon/电气工程/iconfont.css',
+      '/icon/通用图标/iconfont.css',
     ];
     let defsList: any = await Promise.all(
       iconList.map((item) => getIconDefs(item))
     );
     mySerializedSVG = mySerializedSVG.replace(
-      "<defs/>",
+      '<defs/>',
       `<defs>
     <style type="text/css">
-${defsList.join("\n")}
+${defsList.join('\n')}
 </style>
 {{bk}}
   </defs>
@@ -906,30 +892,30 @@ ${defsList.join("\n")}
       );
 */
   if (meta2d.store.data.background) {
-    mySerializedSVG = mySerializedSVG.replace("{{bk}}", "");
+    mySerializedSVG = mySerializedSVG.replace('{{bk}}', '');
     mySerializedSVG = mySerializedSVG.replace(
-      "{{bkRect}}",
+      '{{bkRect}}',
       `<rect x="0" y="0" width="100%" height="100%" fill="${meta2d.store.data.background}"></rect>`
     );
   } else {
-    mySerializedSVG = mySerializedSVG.replace("{{bk}}", "");
-    mySerializedSVG = mySerializedSVG.replace("{{bkRect}}", "");
+    mySerializedSVG = mySerializedSVG.replace('{{bk}}', '');
+    mySerializedSVG = mySerializedSVG.replace('{{bkRect}}', '');
   }
 
-  mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, "&#x");
+  mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, '&#x');
 
   const urlObject: any = (window as any).URL || window;
   const export_blob = new Blob([mySerializedSVG]);
   const url = urlObject.createObjectURL(export_blob);
 
-  const a = document.createElement("a");
+  const a = document.createElement('a');
   a.setAttribute(
-    "download",
-    `${(meta2d.store.data as Meta2dBackData).name || "le5le.meta2d"}.svg`
+    'download',
+    `${(meta2d.store.data as Meta2dBackData).name || 'le5le.meta2d'}.svg`
   );
-  a.setAttribute("href", url);
-  const evt = document.createEvent("MouseEvents");
-  evt.initEvent("click", true, true);
+  a.setAttribute('href', url);
+  const evt = document.createEvent('MouseEvents');
+  evt.initEvent('click', true, true);
   a.dispatchEvent(evt);
 };
 
@@ -997,48 +983,46 @@ const onDisableAnchor = () => {
 };
 
 const changeDisableAnchor = () => {
-  const { disableAnchor, autoAnchor } =
-    meta2d.store.options;
-    showAnchor.value = (!disableAnchor) || false;
+  const { disableAnchor, autoAnchor } = meta2d.store.options;
+  showAnchor.value = !disableAnchor || false;
   if (disableAnchor && autoAnchor) {
     // 禁用瞄点开了,需要关闭自动瞄点
     onAutoAnchor();
   }
 };
 
-
 const helpList = [
   {
-    name: "产品介绍",
-    url: "https://doc.le5le.com/document/118756411",
-    target: "_blank",
+    name: '产品介绍',
+    url: 'https://doc.le5le.com/document/118756411',
+    target: '_blank',
   },
   {
-    name: "快速上手",
-    url: "https://doc.le5le.com/document/119363000",
-    target: "_blank",
+    name: '快速上手',
+    url: 'https://doc.le5le.com/document/119363000',
+    target: '_blank',
   },
   {
-    name: "使用手册",
-    url: "https://doc.le5le.com/document/118764244",
-    target: "_blank",
+    name: '使用手册',
+    url: 'https://doc.le5le.com/document/118764244',
+    target: '_blank',
   },
   {
-    name: "快捷键",
-    url: "https://doc.le5le.com/document/119620214",
-    target: "_blank",
+    name: '快捷键',
+    url: 'https://doc.le5le.com/document/119620214',
+    target: '_blank',
     divider: true,
   },
   {
-    name: "企业服务与支持",
-    url: "https://doc.le5le.com/document/119296274",
-    target: "_blank",
+    name: '企业服务与支持',
+    url: 'https://doc.le5le.com/document/119296274',
+    target: '_blank',
     divider: true,
   },
   {
-    name: "关于我们",
-    url: "https://le5le.com/about.html",
-    target: "_blank",
+    name: '关于我们',
+    url: 'https://le5le.com/about.html',
+    target: '_blank',
   },
 ];
 </script>

+ 21 - 104
src/views/components/PenDatas.vue

@@ -327,20 +327,26 @@
                         />
                         <t-select-input
                           v-model:inputValue="c.value"
-                          :value="c.value"
-                          v-model:popupVisible="triggersDialog.popupVisible"
+                          :value="c.label"
+                          v-model:popupVisible="c.popupVisible"
                           allow-input
-                          @focus="triggersDialog.popupVisible = true"
-                          @blur="triggersDialog.popupVisible = false"
+                          clearable
+                          @clear="c.label = undefined"
+                          @focus="c.popupVisible = true"
+                          @blur="c.popupVisible = undefined"
+                          @input-change="onInput(c)"
                           class="shrink-0"
                           style="width: 152px"
                         >
                           <template #panel>
                             <ul style="padding: 8px 12px">
                               <li
-                                v-for="item in triggersDialog.targetProps"
+                                v-for="item in c.targetProps"
                                 :key="item.value"
-                                @click="c.value = item.value"
+                                @click="
+                                  c.value = item.value;
+                                  c.label = item.label;
+                                "
                               >
                                 {{ item.label }}
                               </li>
@@ -365,37 +371,7 @@
             <div class="form-item banner mt-16">
               <label>执行动作</label>
             </div>
-            <div v-for="(a, index) in trigger.actions" class="mb-12">
-              <div class="flex middle between head">
-                <div class="flex middle">
-                  <t-icon name="arrow-right" class="mr-4" />
-                  动作{{ index + 1 }}
-                </div>
-                <t-icon
-                  name="close"
-                  class="hover"
-                  @click="trigger.actions.splice(index, 1)"
-                />
-              </div>
-              <div class="px-16 py-4">
-                <div class="form-item mt-4">
-                  <label>动作类型</label>
-                  <div>
-                    <t-select v-model="a.action" placeholder="请选择">
-                      <t-option
-                        v-for="option in actionOptions"
-                        :key="option.value"
-                        :value="option.value"
-                        :label="option.label"
-                      />
-                    </t-select>
-                  </div>
-                </div>
-              </div>
-            </div>
-            <div class="mt-8">
-              <a @click="trigger.actions.push({})"> + 添加动作 </a>
-            </div>
+            <Actions class="mt-8" :data="trigger" />
           </section>
         </t-collapse-panel>
       </t-collapse>
@@ -424,6 +400,7 @@ import { getPenTree } from '@/services/common';
 import { updatePen } from './pen';
 
 import CodeEditor from '@/views/components/common/CodeEditor.vue';
+import Actions from './Actions.vue';
 
 const route = useRoute();
 const router = useRouter();
@@ -520,69 +497,6 @@ const typeOptions = [
   },
 ];
 
-const actionOptions = [
-  {
-    label: '打开链接',
-    value: 0,
-  },
-  {
-    label: '打开视图',
-    value: 13,
-  },
-  {
-    label: '播放动画',
-    value: 2,
-  },
-  {
-    label: '暂停动画',
-    value: 3,
-  },
-  {
-    label: '停止动画',
-    value: 4,
-  },
-  {
-    label: '更改属性',
-    value: 1,
-  },
-  {
-    label: '打开弹框',
-    value: 14,
-  },
-  {
-    label: '发送消息',
-    value: 7,
-  },
-  {
-    label: '发送MQTT',
-    value: 15,
-  },
-  {
-    label: '发送Websocket',
-    value: 16,
-  },
-  {
-    label: '发送HTTP(s)',
-    value: 17,
-  },
-  {
-    label: '播放视频',
-    value: 8,
-  },
-  {
-    label: '暂停视频',
-    value: 9,
-  },
-  {
-    label: '停止视频',
-    value: 10,
-  },
-  {
-    label: '自定义函数',
-    value: 18,
-  },
-];
-
 const addDataDialog = reactive<any>({
   show: false,
   data: undefined,
@@ -846,7 +760,6 @@ const onTrigger = (item: any) => {
   }
   triggersDialog.openedCollapses = [0];
   triggersDialog.data = item;
-  triggersDialog.targetProps = [];
   triggersDialog.show = true;
 
   penTree.value = getPenTree();
@@ -873,7 +786,7 @@ const addTriggerCondition = (trigger: any) => {
 };
 
 const onChangeTriggerTarget = (c: any) => {
-  triggersDialog.targetProps = [
+  c.targetProps = [
     {
       value: 'x',
       label: 'X',
@@ -915,11 +828,11 @@ const onChangeTriggerTarget = (c: any) => {
   const target: any = meta2d.findOne(c.target);
   if (target) {
     for (const item of target.realTimes) {
-      const found = triggersDialog.targetProps.findIndex(
+      const found = c.targetProps.findIndex(
         (elem: any) => elem.value === item.key
       );
       if (found < 0) {
-        triggersDialog.targetProps.push({
+        c.targetProps.push({
           value: item.key,
           label: item.label,
         });
@@ -928,6 +841,10 @@ const onChangeTriggerTarget = (c: any) => {
   }
 };
 
+const onInput = (item: any) => {
+  item.label = item.value;
+};
+
 onUnmounted(() => {
   clearInterval(timer);
 });