Alsmile 2 年之前
父节点
当前提交
a5c57438bb

+ 23 - 0
src/services/common.ts

@@ -436,3 +436,26 @@ export const delAttrs = [
   'star',
   'recommend',
 ];
+
+export const typeOptions = [
+  {
+    label: '字符串',
+    value: 'string',
+  },
+  {
+    label: '数字',
+    value: 'number',
+  },
+  {
+    label: '布尔',
+    value: 'bool',
+  },
+  {
+    label: '对象',
+    value: 'object',
+  },
+  {
+    label: '数组',
+    value: 'array',
+  },
+];

+ 0 - 48
src/services/icons.ts

@@ -1,48 +0,0 @@
-import axios from '@/http';
-import { parseSvg } from '@meta2d/svg';
-import { cdn } from './api';
-import { getFolders } from './png';
-
-const normalFolder = import.meta.env.VITE_BASEURL ? '/2d/svg/' : '/svg/';
-/**
- * 请求 svg 的目录
- * @returns
- */
-export async function getIconFolders() {
-  return await getFolders(normalFolder);
-}
-/**
- * 请求 svg 目录下的所有 svg
- * @param name 目录名
- * @returns
- */
-export async function getIcons(name: string) {
-  const files = (await axios.get(normalFolder + name + '/')) as any[];
-  return await Promise.all(files.map((f) => svgToPens(f, name)));
-}
-
-export function filename(str: string) {
-  const i = str.lastIndexOf('.');
-  return str.substring(0, i);
-}
-
-async function svgToPens(f: any, diretoryName: string) {
-  const _name = filename(f.name);
-  const name = globalThis.fileJson
-    ? globalThis.fileJson[_name]
-      ? globalThis.fileJson[_name]
-      : _name
-    : _name;
-  const image = cdn + normalFolder + diretoryName + '/' + f.name;
-  const svgDom: string = await axios.get(image);
-  let _svgDom = svgDom.replace('stroke:#333;', 'stroke:#bdc7db;');
-  const data = parseSvg(_svgDom);
-  return {
-    name,
-    pinyin: globalThis.fileJson ? _name : null,
-    // image, // image 只作为缩略图
-    componentDatas: data,
-    svg: _svgDom,
-    component: true,
-  };
-}

+ 56 - 40
src/services/png.ts

@@ -1,51 +1,26 @@
 import axios from '@/http';
+import { parseSvg } from '@meta2d/svg';
 import { cdn } from './api';
 
-const market = import.meta.env.VITE_BASEURL;
+const baseurl = import.meta.env.VITE_BASEURL || '/';
 
-const normalFolder = market ? '/2d/png/' : '/png/';
-/**
- * 请求 png 的目录
- * @returns
- */
-export async function getPngFolders() {
-  return await getFolders(normalFolder);
-}
-
-/**
- * 请求 png 目录下的所有 png
- * @param name 目录名
- * @returns
- */
-export async function getPngs(name: string) {
-  const files = (await axios.get(normalFolder + name + '/')) as any[];
-  if (!files || !files.map) {
-    return [];
-  }
-  return files.map((f) => {
-    let fname = filename(f.name);
-    return {
-      name: globalThis.fileJson
-        ? globalThis.fileJson[fname]
-          ? globalThis.fileJson[fname]
-          : fname
-        : fname,
-      pinyin: globalThis.fileJson ? fname : null,
-      image: cdn + normalFolder + name + '/' + f.name,
-    };
-  });
+export function filename(str: string) {
+  const i = str.lastIndexOf('.');
+  return str.substring(0, i);
 }
 
-export async function getFolders(folderName: string) {
-  const ret = (await axios.get(folderName)) as any[];
+export async function getFolders(name: string, svg?: boolean) {
+  const folder = baseurl + name + '/';
+  const ret = (await axios.get(folder)) as any[];
 
   if (!ret || !ret.map) {
     return [];
   }
   return await Promise.all(
     ret.map(async (c: any) => {
-      const files = (await axios.get(folderName + c.name + '/')) as any[];
+      const files = (await axios.get(folder + c.name + '/')) as any[];
       return {
+        folder,
         name: globalThis.folderJson
           ? globalThis.folderJson[c.name]
             ? globalThis.folderJson[c.name]
@@ -55,14 +30,55 @@ export async function getFolders(folderName: string) {
         show: true,
         list: [],
         count: files.length,
-        // 用于区别 png 与 svg 文件夹
-        svgFolder: folderName === normalFolder ? true : false,
+        svg,
       };
     })
   );
 }
 
-export function filename(str: string) {
-  const i = str.lastIndexOf('.');
-  return str.substring(0, i);
+export async function getFiles(folder: string) {
+  if (folder[folder.length - 1] !== '/') {
+    folder += '/';
+  }
+  const files = (await axios.get(folder)) as any[];
+  if (!files || !files.map) {
+    return [];
+  }
+  return files.map((f) => {
+    let fname = filename(f.name);
+    return {
+      name: globalThis.fileJson
+        ? globalThis.fileJson[fname]
+          ? globalThis.fileJson[fname]
+          : fname
+        : fname,
+      pinyin: globalThis.fileJson ? fname : null,
+      image: cdn + folder + f.name,
+    };
+  });
+}
+
+export async function getIcons(folder: string) {
+  const files = (await axios.get(folder)) as any[];
+  return await Promise.all(files.map((f) => svgToPens(f, folder)));
+}
+
+async function svgToPens(f: any, folder: string) {
+  const _name = filename(f.name);
+  const name = globalThis.fileJson
+    ? globalThis.fileJson[_name]
+      ? globalThis.fileJson[_name]
+      : _name
+    : _name;
+  const image = cdn + folder + f.name;
+  const svgDom: string = await axios.get(image);
+  let _svgDom = svgDom.replace('stroke:#333;', 'stroke:#bdc7db;');
+  const data = parseSvg(_svgDom);
+  return {
+    name,
+    pinyin: globalThis.fileJson ? _name : null,
+    componentDatas: data,
+    svg: _svgDom,
+    component: true,
+  };
 }

+ 1 - 1
src/styles/tdesign.css

@@ -420,7 +420,7 @@
 }
 
 .t-popup[data-popper-placement^='top'] .t-popup__arrow::before {
-  top: -8px;
+  top: -3px;
   border-top-left-radius: 0;
 }
 

+ 90 - 83
src/views/components/Graphics.vue

@@ -17,8 +17,12 @@
           {{ group.name }}
         </div>
       </div>
-      <div class="list" :class="groupType ? 'two-list' : ''">
-        <t-collapse v-model:value="activedPanel" @change="onChangeGroupPanel">
+      <div class="list" :class="groupType ? 'two-columns' : ''">
+        <t-collapse
+          v-if="groupType < 2"
+          v-model:value="activedPanel"
+          @change="onChangeGroupPanel"
+        >
           <t-collapse-panel
             :value="item.name"
             :header="item.name"
@@ -36,14 +40,15 @@
               <div
                 class="graphic"
                 v-for="elem in item.list"
-                :draggable="!groupType || groupType >= 10"
+                :draggable="elem.draggable !== false"
                 @dragstart="dragStart($event, elem)"
                 @drag="drag($event, elem)"
                 @dragend="dragEnd()"
                 @click.stop="dragStart($event, elem)"
                 @dblclick.stop="open(elem)"
+                :title="elem.draggable === false ? '双击打开' : '拖拽到画布'"
               >
-                <t-image v-if="elem.image" :src="elem.image" />
+                <t-image v-if="elem.image" :src="elem.image" :lazy="true" />
                 <div class="svg-box" v-else-if="elem.svg" v-html="elem.svg" />
                 <svg v-else class="l-icon" aria-hidden="true">
                   <use :xlink:href="'#' + elem.icon"></use>
@@ -63,6 +68,7 @@
             </template>
           </t-collapse-panel>
         </t-collapse>
+        <template v-else></template>
       </div>
     </div>
   </div>
@@ -74,8 +80,7 @@ import { useRouter } from 'vue-router';
 import axios from 'axios';
 
 import { cases, shapes, charts, formComponents } from '@/services/defaults';
-import { getPngFolders, getPngs } from '@/services/png';
-import { getIconFolders, getIcons } from '@/services/icons';
+import { getFolders, getFiles, getIcons } from '@/services/png';
 import { getComponents, getComponentsList, getLe5leV } from '@/services/api';
 import { convertPen } from '@/services/upgrade';
 import { deepClone } from '@meta2d/core';
@@ -84,13 +89,14 @@ import { autoSave, delAttrs } from '@/services/common';
 
 const router = useRouter();
 
-const activedGroup = ref('图形');
+const activedGroup = ref('');
 
 const groups = reactive([
   {
     icon: 'desktop',
     name: '场景',
     key: '',
+    class: 'tow',
   },
   {
     icon: 'root-list',
@@ -103,7 +109,7 @@ const groups = reactive([
     key: 'chart',
   },
   {
-    icon: 'control-platform',
+    icon: 'relativity',
     name: '控件',
     key: '',
   },
@@ -113,8 +119,8 @@ const groups = reactive([
     key: '',
   },
   {
-    icon: 'file-icon',
-    name: '图',
+    icon: 'control-platform',
+    name: '图',
     key: '',
   },
   {
@@ -132,9 +138,11 @@ const subGroups = ref<any[]>([]);
 const groupType = ref(0);
 const activedPanel = ref([]);
 
-const templates = reactive<any>({});
-const materials = reactive([]);
-const icons = reactive([]);
+const caseCaches = reactive<any>({});
+const templates = ref([]);
+const materials = ref([]);
+const pngs = ref([]);
+const icons = ref([]);
 
 const groupChange = async (name: string) => {
   activedPanel.value = [];
@@ -145,27 +153,20 @@ const groupChange = async (name: string) => {
       groupType.value = 1;
       subGroups.value = cases;
       subGroups.value[0].loading = true;
-      if (!templates[name + cases[0].name]) {
-        templates[name + cases[0].name] = await getCaseProjects(
+      if (!caseCaches[name + cases[0].name]) {
+        caseCaches[name + cases[0].name] = await getCaseProjects(
           name,
           cases[0].name
         );
       }
-      subGroups.value[0].list = templates[name + cases[0].name];
+      subGroups.value[0].list = caseCaches[name + cases[0].name];
       subGroups.value[0].loading = false;
       break;
     case '模板':
       groupType.value = 2;
-      subGroups.value = cases;
-      subGroups.value[0].loading = true;
-      if (!templates[name + cases[0].name]) {
-        templates[name + cases[0].name] = await getCaseProjects(
-          name,
-          cases[0].name
-        );
+      if (!templates.value.length) {
       }
-      subGroups.value[0].list = templates[name + cases[0].name];
-      subGroups.value[0].loading = false;
+      subGroups.value = templates.value;
       break;
     case '图表':
       subGroups.value = charts;
@@ -174,23 +175,29 @@ const groupChange = async (name: string) => {
       subGroups.value = formComponents;
       break;
     case '素材':
-      if (!materials.length) {
-        materials.push(...(await getPngFolders()));
+      groupType.value = 2;
+      if (!materials.value.length) {
       }
-      subGroups.value = materials;
+      subGroups.value = materials.value;
       break;
-    case '图标':
-      if (!icons.length) {
-        icons.push(...(await getIconFolders()));
+    case '图元':
+      subGroups.value = [];
+      if (!pngs.value.length) {
+        pngs.value = await getFolders('png');
       }
-      subGroups.value = icons;
+      subGroups.value.push(...pngs.value);
+      if (!icons.value.length) {
+        icons.value = await getFolders('svg');
+      }
+      subGroups.value.push(...icons.value);
+      onChangeGroupPanel([subGroups.value[0].name]);
       break;
     case '图形':
       subGroups.value = shapes;
       break;
     case '我的':
       subGroups.value = await getPrivateCommponents();
-      groupType.value = 10;
+      groupType.value = 1;
       break;
   }
   activedPanel.value = [subGroups.value[0].name];
@@ -209,6 +216,9 @@ const getCaseProjects = async (name: string, group: string) => {
   if (!ret) {
     return [];
   }
+  for (const item of ret.list) {
+    item.draggable = false;
+  }
   return ret.list;
 };
 
@@ -249,40 +259,10 @@ const getPrivateCommponents = async () => {
   return list;
 };
 
-watch(
-  () => activedPanel.value,
-  async (newV: any[], oldV: any[]) => {
-    const newOpen: any = [];
-    for (let v of newV) {
-      !oldV.includes(v) && newOpen.push(v);
-    }
-    if (newOpen.length === 0) {
-      return;
-    }
-    if (activedGroup.value === '素材') {
-      const data: any = materials.find((item) => item.name === newOpen[0]);
-      if (!data.list || data.list.length === 0) {
-        data.list = await getPngs(
-          globalThis.folderJson ? data.pinyin : data.name
-        );
-        subGroups.value = deepClone(materials);
-      }
-    } else if (activedGroup.value === '图标') {
-      const data: any = icons.find((item) => item.name === newOpen[0]);
-      if (!data.list || data.list.length === 0) {
-        data.list = await getIcons(
-          globalThis.folderJson ? data.pinyin : data.name
-        );
-        subGroups.value = deepClone(icons);
-      }
-    }
-  }
-);
-
 const dragStart = async (event: DragEvent | MouseEvent, item: any) => {
   if (
-    (groupType.value > 0 && groupType.value < 10) ||
     !item ||
+    item.draggable === false ||
     (event instanceof DragEvent && !event.dataTransfer)
   ) {
     return;
@@ -363,27 +343,45 @@ const open = async (item: any) => {
 };
 
 const onChangeGroupPanel = async (val: string[]) => {
-  if (groupType.value > 0 && groupType.value < 10 && val?.length) {
+  if (val?.length) {
     for (const name of val) {
-      if (
-        !templates[activedGroup.value + name] ||
-        !templates[activedGroup.value + name].length
-      ) {
-        for (const item of subGroups.value) {
-          if (item.name === name) {
-            item.loading = true;
+      switch (activedGroup.value) {
+        case '场景':
+          if (
+            !caseCaches[activedGroup.value + name] ||
+            !caseCaches[activedGroup.value + name].length
+          ) {
+            for (const item of subGroups.value) {
+              if (item.name === name) {
+                item.loading = true;
+              }
+            }
+            caseCaches[activedGroup.value + name] = await getCaseProjects(
+              activedGroup.value,
+              name
+            );
+            for (const item of subGroups.value) {
+              if (item.name === name) {
+                item.list = caseCaches[activedGroup.value + name];
+                item.loading = false;
+              }
+            }
           }
-        }
-        templates[activedGroup.value + name] = await getCaseProjects(
-          activedGroup.value,
-          name
-        );
-        for (const item of subGroups.value) {
-          if (item.name === name) {
-            item.list = templates[activedGroup.value + name];
-            item.loading = false;
+          break;
+        case '图元':
+          for (const item of subGroups.value) {
+            if (item.name === name && !item.list.length) {
+              item.loading = true;
+              if (item.svg) {
+                item.list = await getIcons(item.folder);
+              } else {
+                console.log(item.folder + item.name);
+                item.list = await getFiles(item.folder + item.name);
+              }
+              item.loading = false;
+            }
           }
-        }
+          break;
       }
     }
   }
@@ -455,6 +453,7 @@ onUnmounted(() => {
       }
 
       :deep(.t-collapse) {
+        min-height: 100vh;
         border: none;
       }
       :deep(.t-collapse-panel__header) {
@@ -477,6 +476,14 @@ onUnmounted(() => {
         grid-template-columns: 1fr 1fr 1fr;
         grid-row-gap: 20px;
       }
+
+      :deep(.t-loading--center) {
+        width: 100px;
+        .t-loading__text {
+          margin-left: 8px;
+          height: 24px;
+        }
+      }
       .graphic {
         position: relative;
 
@@ -549,7 +556,7 @@ onUnmounted(() => {
       }
     }
 
-    .two-list {
+    .two-columns {
       :deep(.t-collapse-panel__content) {
         padding: 0 8px;
         grid-template-columns: 116px 116px;

+ 3 - 23
src/views/components/PenDatas.vue

@@ -419,7 +419,7 @@ import { useRoute, useRouter } from 'vue-router';
 import { MessagePlugin } from 'tdesign-vue-next';
 import axios from 'axios';
 import { debounce } from '@/services/debouce';
-import { getPenTree } from '@/services/common';
+import { getPenTree, typeOptions } from '@/services/common';
 import { updatePen } from './pen';
 
 import CodeEditor from '@/views/components/common/CodeEditor.vue';
@@ -508,28 +508,6 @@ const moreOptions = ref<any>([
   },
 ]);
 
-const typeOptions = [
-  {
-    label: '字符串',
-  },
-  {
-    label: '数字',
-    value: 'number',
-  },
-  {
-    label: '布尔',
-    value: 'bool',
-  },
-  {
-    label: '对象',
-    value: 'object',
-  },
-  {
-    label: '数组',
-    value: 'array',
-  },
-];
-
 const addDataDialog = reactive<any>({
   show: false,
   data: undefined,
@@ -662,6 +640,8 @@ const onConfirmData = () => {
     props.pen.realTimes.push(addDataDialog.data);
   }
 
+  meta2d.penMock(props.pen);
+
   addDataDialog.show = false;
 };
 

+ 156 - 17
src/views/components/View.vue

@@ -393,23 +393,41 @@
             <span><label class="vip-label ml-4">VIP</label></span>
           </template>
           <template #panel>
-            <div class="form-item mt-12">
-              <label>网络地址</label>
+            <div class="form-item mt-20">
+              <label style="width: 100px">
+                网络地址
+                <t-tooltip
+                  content="使用网络数据代替自定义数据。高优先级,生产环境使用"
+                >
+                  <t-icon
+                    name="help-circle"
+                    class="ml-4 hover"
+                    style="margin-top: -2px"
+                  />
+                </t-tooltip>
+              </label>
               <div class="w-full">
                 <t-input v-model="dataDialog.datasetUrl" />
-                <div class="desc mt-4">
-                  高优先级。存在网络地址时,用网络数据代替自定义数据
-                </div>
               </div>
             </div>
-            <div class="form-item mt-16">
-              <label>自定义</label>
-              <div class="w-full">
+            <div class="form-item" style="margin-top: 28px">
+              <label style="width: 100px">
+                自定义
+                <t-tooltip content="初始静态或模拟数据,开发设计阶段使用">
+                  <t-icon
+                    name="help-circle"
+                    class="ml-4 hover"
+                    style="margin-top: -2px"
+                  />
+                </t-tooltip>
+              </label>
+              <div class="w-full flex">
                 <t-button @click="importDataset">从Excel导入</t-button>
-                <a href="/data.xlsx" download class="ml-16">下载Excel示例</a>
-                <span class="desc ml-16">
-                  “自定义”数据集 - 方便构建项目时了解数据结构
-                </span>
+                <a href="/data.xlsx" download class="ml-16 mt-4">
+                  下载Excel示例
+                </a>
+                <div class="flex-grow"></div>
+                <a class="mt-4" @click="showAddData()"> + 添加数据 </a>
               </div>
             </div>
 
@@ -424,13 +442,20 @@
               <template #label="{ row }">
                 {{ `${row.label}(${row.key})` }}
               </template>
-              <template #actions="{ row }"> </template>
+              <template #type="{ row }">
+                {{ row.type || 'string' }}
+              </template>
+              <template #actions="{ row, rowIndex }">
+                <t-icon name="edit" class="hover" @click="showAddData(row)" />
+                <t-icon
+                  name="delete"
+                  class="ml-12 hover"
+                  @click="dataDialog.dataset.splice(rowIndex, 1)"
+                />
+              </template>
               <template #empty>
                 <div class="center">
-                  <div>暂无数据</div>
-                  <div class="mt-8">
-                    <a @click="importDataset"> 从Excel导入</a>
-                  </div>
+                  暂无数据, <a class="mt-4" @click="showAddData()"> + 添加 </a>
                 </div>
               </template>
             </t-table>
@@ -455,6 +480,83 @@
       </template>
     </t-dialog>
 
+    <t-dialog
+      v-if="addDataDialog.show"
+      :visible="true"
+      class="data-dialog"
+      :header="addDataDialog.header"
+      @close="addDataDialog.show = false"
+      @confirm="onOkAddData"
+    >
+      <div class="form-item mt-16">
+        <label>名称</label>
+        <t-input v-model="addDataDialog.data.label" placeholder="简短描述" />
+      </div>
+      <div class="form-item mt-16">
+        <label>数据ID</label>
+        <t-input v-model="addDataDialog.data.key" placeholder="数据ID" />
+      </div>
+      <div class="form-item mt-16">
+        <label>类型</label>
+        <t-select
+          class="w-full"
+          :options="typeOptions"
+          v-model="addDataDialog.data.type"
+          placeholder="字符串"
+          @change="addDataDialog.data.value = null"
+        />
+      </div>
+      <div class="form-item mt-16">
+        <label>值</label>
+        <div class="flex-grow" v-if="addDataDialog.data.type === 'number'">
+          <t-input
+            class="w-full"
+            v-model="addDataDialog.data.value"
+            placeholder="数字"
+          />
+          <div class="desc mt-8">
+            固定数字:直接输入数字。例如:5<br />
+            随机范围数字 :最小值-最大值。例如:0-1 或 0-100
+            <br />
+            随机指定数字 :数字1,数字2,数字3... 。 例如:1,5,10,20<br />
+          </div>
+        </div>
+        <div class="flex-grow" v-else-if="addDataDialog.data.type === 'bool'">
+          <t-select v-model="addDataDialog.data.value">
+            <t-option :key="true" :value="true" label="true"></t-option>
+            <t-option :key="false" :value="false" label="false"></t-option>
+            <t-option key="随机" label="随机"></t-option>
+          </t-select>
+          <div class="desc mt-8">
+            固定:指定true或false<br />
+            随机:随机生成一个布尔值<br />
+          </div>
+        </div>
+        <div
+          class="flex-grow"
+          v-else-if="
+            addDataDialog.data.type === 'array' ||
+            addDataDialog.data.type === 'object'
+          "
+        >
+          <CodeEditor v-model="addDataDialog.data.value" :json="true" />
+        </div>
+        <div class="flex-grow" v-else>
+          <t-input
+            class="w-full"
+            v-model="addDataDialog.data.value"
+            placeholder="字符串"
+          />
+          <div class="desc mt-8">
+            固定文字:直接输入。例如:大屏可视化<br />
+            随机文本:[文本长度]。例如:[8] 或 [16]<br />
+            随机指定文本:{文本1,文本2,文本3...} 。 例如:{大屏, 可视化}
+            <br />
+          </div>
+        </div>
+      </div>
+    </t-dialog>
+
     <t-dialog
       v-if="publishDialog.show"
       width="700px"
@@ -633,6 +735,7 @@ import {
   onScaleView,
   onScaleWindow,
   useDot,
+  typeOptions,
 } from '@/services/common';
 import { useSelection } from '@/services/selections';
 import { defaultFormat } from '@/services/defaults';
@@ -641,6 +744,7 @@ import { debounce } from '@/services/debouce';
 import { importExcel } from '@/services/excel';
 import { s8 } from '@/services/random';
 
+import CodeEditor from './common/CodeEditor.vue';
 import ContextMenu from './ContextMenu.vue';
 import Network from './Network.vue';
 import ChargeCloudPublish from './ChargeCloudPublish.vue';
@@ -676,6 +780,8 @@ const publishDialog = reactive<any>({});
 
 const publishChargeDialog = reactive<any>({});
 
+const addDataDialog = reactive<any>({});
+
 onMounted(() => {
   meta2d = new Meta2d('meta2d', meta2dOptions);
   registerBasicDiagram();
@@ -1192,6 +1298,11 @@ const datasetColumns = ref([
     title: '值',
     ellipsis: true,
   },
+  {
+    colKey: 'actions',
+    title: '操作',
+    width: 80,
+  },
 ]);
 
 const importDataset = async () => {
@@ -1221,6 +1332,34 @@ const importDataset = async () => {
   dataDialog.dataset = data;
 };
 
+const showAddData = (row?: any) => {
+  if (row) {
+    addDataDialog.header = '编辑数据';
+    addDataDialog.data = row;
+  } else {
+    addDataDialog.header = '添加数据';
+    addDataDialog.data = { type: 'string' };
+  }
+
+  addDataDialog.show = true;
+};
+
+const onOkAddData = () => {
+  if (!addDataDialog.data.label) {
+    MessagePlugin.error('请填写名称');
+    return;
+  }
+  if (!addDataDialog.data.key) {
+    MessagePlugin.error('请填写数据ID');
+    return;
+  }
+  if (!dataDialog.dataset) {
+    dataDialog.dataset = [];
+  }
+  dataDialog.dataset.push(addDataDialog.data);
+  addDataDialog.show = false;
+};
+
 const share = async () => {
   if (!route.query.id) {
     MessagePlugin.error('请先保存!');