Alsmile 2 rokov pred
rodič
commit
1116a58873

+ 14 - 57
src/http.ts

@@ -1,60 +1,17 @@
-import { MessagePlugin } from "tdesign-vue-next";
-import axios from "axios";
-import { getCookie } from "@/services/cookie";
-import router from "./router";
+import { MessagePlugin } from 'tdesign-vue-next';
+import axios from 'axios';
+import { getCookie } from '@/services/cookie';
+import router from './router';
 
 // axios 配置
 axios.defaults.timeout = 60000;
 axios.defaults.withCredentials = false;
 
-const requestDebounceMap = new Map();
-const requestThrottleSet = new Set();
-
 // http request 拦截器
 axios.interceptors.request.use(
   (config: any) => {
     config.headers.Authorization =
-      "Bearer " + (localStorage.token || getCookie("token") || "");
-
-    if (config.params) {
-      // 防抖, 比如输入搜索
-      if (config.params.debounce > 0) {
-        const url = config.method + config.url;
-
-        const cache: any = requestDebounceMap.get(url);
-        if (cache) {
-          clearTimeout(cache.timer);
-          cache.reject();
-        }
-
-        delete config.params.debounce;
-        delete config.params.throttle;
-
-        return new Promise((resolve, reject) => {
-          const newCache: any = { reject };
-          newCache.timer = setTimeout(() => {
-            requestDebounceMap.delete(url);
-            resolve(config);
-          }, config.params.debounce);
-          requestDebounceMap.set(url, newCache);
-        });
-      }
-      // 节流,避免短时间重复请求,比如点击确定
-      else if (config.params.throttle > 0) {
-        const url = config.method + config.url;
-        // 已经存在,取消重复请求
-        if (requestThrottleSet.has(url)) {
-          return Promise.reject("Repeated request.");
-        }
-        requestThrottleSet.add(url);
-        setTimeout(() => {
-          requestThrottleSet.delete(url);
-        }, config.params.throttle);
-      }
-
-      delete config.params.debounce;
-      delete config.params.throttle;
-    }
+      'Bearer ' + (localStorage.token || getCookie('token') || '');
 
     return config;
   },
@@ -78,7 +35,7 @@ axios.interceptors.response.use(
       return;
     }
     if (error && error.response) {
-      if (error.response.config.url === "/api/account/profile") {
+      if (error.response.config.url === '/api/account/profile') {
         return;
       }
 
@@ -94,26 +51,26 @@ axios.interceptors.response.use(
 
       switch (error.response.status) {
         case 401:
-          sessionStorage.setItem("cb", encodeURIComponent(location.href));
-          router.replace({ path: "/login" });
+          sessionStorage.setItem('cb', encodeURIComponent(location.href));
+          router.replace({ path: '/login' });
           break;
         case 403:
-          MessagePlugin.error("请求错误,不合法的请求!");
+          MessagePlugin.error('请求错误,不合法的请求!');
 
           break;
         case 404:
-          if (error.response.config.url.indexOf("/data/") !== 0) {
-            MessagePlugin.error("访问数据不存在,请检查后重试!");
+          if (error.response.config.url.indexOf('/data/') !== 0) {
+            MessagePlugin.error('访问数据不存在,请检查后重试!');
           }
           break;
         case 500:
-          MessagePlugin.error("请求服务错误,请稍后重试!");
+          MessagePlugin.error('请求服务错误,请稍后重试!');
           break;
         case 504:
-          MessagePlugin.error("网络超时,请检测你的网络!");
+          MessagePlugin.error('网络超时,请检测你的网络!');
           break;
         default:
-          MessagePlugin.error("未知网络错误!");
+          MessagePlugin.error('未知网络错误!');
           break;
       }
     }

+ 29 - 0
src/services/debouce.ts

@@ -0,0 +1,29 @@
+const debounces = new WeakMap();
+const throttles = new WeakMap();
+
+export function debounce(fn: Function, delay: number) {
+  let cache: any = debounces.get(fn);
+  if (cache) {
+    clearTimeout(cache.timer);
+  } else {
+    cache = {};
+    debounces.set(fn, cache);
+  }
+  return new Promise((resolve, reject) => {
+    cache.timer = setTimeout(async () => {
+      resolve(await fn());
+      debounces.delete(fn);
+    }, delay);
+  });
+}
+
+export async function throttle(fn: Function, delay: number) {
+  const now = new Date().getTime();
+  const start: number = debounces.get(fn);
+  throttles.set(fn, now);
+  if (start && now - start < delay) {
+    return;
+  }
+
+  return await fn();
+}

+ 5 - 0
src/styles/app.css

@@ -241,10 +241,15 @@ a.hover:hover {
 .ml-8 {
   margin-left: 8px;
 }
+
 .-ml-8 {
   margin-left: -8px;
 }
 
+.mb-8 {
+  margin-bottom: 8px;
+}
+
 .ml-12 {
   margin-left: 12px;
 }

+ 9 - 0
src/styles/props.css

@@ -154,6 +154,15 @@
     }
   }
 
+  .t-table__th-cell-inner {
+    .t-is-indeterminate .t-checkbox__input {
+      &::after {
+        left: -1px !important;
+        top: 4.5px !important;
+      }
+    }
+  }
+
   .t-button {
     height: 24px;
     border-color: var(--color-desc);

+ 14 - 33
src/styles/tdesign.css

@@ -186,44 +186,15 @@
 .t-table {
   font-size: 13px;
 
-  th {
-    background: #f7f8fa;
-    color: #7f838c;
-    border-color: #edeef0;
-  }
-
-  td {
-    color: #474e59;
-    border-color: #edeef0;
-  }
-
-  &.border-none {
-    th {
-      background: none !important;
-      border: none;
-    }
-    td {
-      border: none;
-    }
-
-    .t-table__row--selected {
-      background-color: var(--color-primary-background);
-    }
-  }
+  .t-table__pagination {
+    padding: 16px 0;
 
-  &.hide-checkbox {
-    .t-table__cell-check {
-      * {
-        display: none;
-      }
+    svg {
+      color: var(--color);
     }
   }
 }
 
-.t-alert__description {
-  color: #002da0;
-}
-
 .t-button--variant-outline {
   background-color: transparent !important;
 }
@@ -462,8 +433,18 @@
 
 .t-dialog {
   padding: 16px 20px;
+  border-color: var(--color-dialog-border);
 
   .t-dialog__header {
     font-size: 14px;
   }
+
+  .t-dialog__close:hover {
+    background: none;
+    color: var(--color-primary);
+  }
+
+  .t-table__empty {
+    color: var(--color-gray);
+  }
 }

+ 3 - 0
src/styles/var.css

@@ -63,4 +63,7 @@
   --td-brand-color: var(--color-primary);
   --td-bg-color-component: var(--color-border-input);
   --td-brand-color-light: var(--color-border-input);
+  --td-mask-disabled: #1e2430;
+
+  --color-dialog-border: transparent;
 }

+ 207 - 9
src/views/components/PenDatas.vue

@@ -18,8 +18,8 @@
           <t-input v-model="item.value" />
         </div>
         <div class="actions">
-          <t-tooltip content="数据绑定" placement="top">
-            <t-icon name="link" class="hover" style="visibility: visible" />
+          <t-tooltip content="变量绑定" placement="top">
+            <t-icon name="link" class="hover" @click="onBind(item)" />
           </t-tooltip>
           <t-dropdown
             :options="moreOptions"
@@ -128,18 +128,75 @@
       </div>
     </div>
   </t-dialog>
+
+  <t-dialog
+    v-if="dataBindDialog.show"
+    :visible="true"
+    class="data-link-dialog"
+    header="变量绑定"
+    @close="dataBindDialog.show = false"
+    @confirm="dataBindDialog.show = false"
+    :width="700"
+  >
+    <div class="form-item">
+      <label>当前绑定:</label>
+      <div class="label" v-if="dataBindDialog.data.binds?.length">
+        <t-tooltip
+          v-for="(tag, index) in dataBindDialog.data.binds"
+          :key="index"
+          :content="tag.id"
+        >
+          <t-tag class="mr-8 mb-8" closable @close="onRemoveBind(index)">
+            {{ tag.label }}
+          </t-tag>
+        </t-tooltip>
+      </div>
+      <div class="label gray" v-else>无</div>
+    </div>
+    <div class="form-item mt-8">
+      <t-input
+        placeholder="搜索"
+        v-model="dataBindDialog.input"
+        @change="onSearchDataSet"
+        @enter="onSearchDataSet"
+      >
+        <template #suffixIcon>
+          <t-icon name="search" class="hover" @click="onSearchDataSet" />
+        </template>
+      </t-input>
+    </div>
+    <t-table
+      class="mt-12 data-list"
+      row-key="id"
+      :data="dataBindDialog.dataSet"
+      :columns="dataSetColumns"
+      size="small"
+      bordered
+      :loading="dataBindDialog.loading"
+      :pagination="query"
+      @page-change="onChangePagination"
+      :selected-row-keys="dataBindDialog.selectedIds"
+      @select-change="onSelectBindsChange"
+    >
+    </t-table>
+  </t-dialog>
 </template>
 
 <script lang="ts" setup>
-import { onBeforeMount, reactive, ref } from 'vue';
-
+import { onBeforeMount, reactive, ref, toRaw } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 import { MessagePlugin } from 'tdesign-vue-next';
+import axios from 'axios';
+import { debounce } from '@/services/debouce';
+
+const route = useRoute();
+const router = useRouter();
 
 const { pen } = defineProps<{
   pen: any;
 }>();
 
-const options: any[] = ref([
+const options = ref<any>([
   {
     value: '',
     content: '自定义',
@@ -198,7 +255,7 @@ const options: any[] = ref([
   },
 ]);
 
-const moreOptions: any[] = ref([
+const moreOptions = ref<any>([
   {
     value: 'edit',
     content: '编辑',
@@ -223,11 +280,53 @@ const typeOptions = [
   },
 ];
 
-const addDataDialog = reactive({
+const addDataDialog = reactive<any>({
   show: false,
   data: undefined,
 });
 
+const dataBindDialog = reactive<any>({
+  show: false,
+  data: undefined,
+});
+
+const dataSetColumns = [
+  {
+    colKey: 'row-select',
+    type: 'multiple',
+    width: 50,
+  },
+  {
+    colKey: 'id',
+    title: '编号',
+    width: 150,
+    ellipsis: { theme: 'light', trigger: 'context-menu' },
+  },
+  {
+    colKey: 'label',
+    title: '变量名称',
+    width: 220,
+    ellipsis: { theme: 'light', trigger: 'context-menu' },
+  },
+  {
+    colKey: 'case',
+    title: '场景',
+    ellipsis: { theme: 'light', trigger: 'context-menu' },
+  },
+];
+
+const query = reactive<{
+  current: number;
+  pageSize: number;
+  total: number;
+  range: any[];
+}>({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  range: [],
+});
+
 onBeforeMount(() => {
   if (pen.realTimesOptions) {
     options.value[options.value.length - 1].divider = true;
@@ -236,6 +335,20 @@ onBeforeMount(() => {
 });
 
 const addRealTime = (e: any) => {
+  if (e.keywords) {
+    if (!pen.realTimes) {
+      pen.realTimes = [];
+    }
+
+    pen.realTimes.push({
+      label: e.content,
+      key: e.value,
+      type: e.type,
+      keywords: e.keywords,
+    });
+    return;
+  }
+
   addDataDialog.header = '添加动态数据';
 
   addDataDialog.data = {
@@ -244,7 +357,7 @@ const addRealTime = (e: any) => {
     type: e.type,
     keywords: e.keywords,
   };
-  if (!e.keywords) {
+  if (e.keywords) {
     addDataDialog.data.label = '';
   }
   addDataDialog.show = true;
@@ -266,7 +379,7 @@ const onConfirmData = () => {
   }
 
   if (addDataDialog.header === '添加动态数据') {
-    const found = pen.realTimes.findIndex((item) => {
+    const found = pen.realTimes.findIndex((item: any) => {
       return item.key === addDataDialog.data.key;
     });
     if (found > -1) {
@@ -293,6 +406,86 @@ const onMenuMore = (e: any, item: any, i: number) => {
       break;
   }
 };
+
+const onBind = (item: any) => {
+  if (!item.binds) {
+    item.binds = [];
+  }
+  dataBindDialog.data = item;
+  dataBindDialog.input = '';
+  dataBindDialog.selectedIds = [];
+  for (const i of item.binds) {
+    dataBindDialog.selectedIds.push(i.id);
+  }
+
+  dataBindDialog.show = true;
+
+  getDataSet();
+};
+
+const onSearchDataSet = () => {
+  debounce(getDataSet, 300);
+};
+
+const getDataSet = async () => {
+  // @ts-ignore
+  const data: Meta2dBackData = meta2d.data();
+
+  dataBindDialog.loading = true;
+
+  // 应该从data获取url或结果列表
+  const ret: any = await axios.get(
+    `/api/device/data/set?mock=1&q=${dataBindDialog.input}&current=${query.current}&pageSize=${query.pageSize}`
+  );
+
+  dataBindDialog.dataSet = ret.list;
+  query.total = ret.total;
+  dataBindDialog.loading = false;
+};
+
+const onChangePagination = (pageInfo: any) => {
+  router.push({
+    path: route.path,
+    query: { current: pageInfo.current, pageSize: pageInfo.pageSize },
+  });
+  query.current = pageInfo.current;
+  query.pageSize = pageInfo.pageSize;
+  getDataSet();
+};
+
+const onSelectBindsChange = (value: string[], options: any) => {
+  dataBindDialog.selectedIds = value;
+  console.log(options);
+
+  if (options.type === 'check') {
+    for (const item of options.selectedRowData) {
+      const found = dataBindDialog.data.binds.findIndex((elem: any) => {
+        return elem.id === item.id;
+      });
+      if (found < 0) {
+        dataBindDialog.data.binds.push(toRaw(item));
+      }
+    }
+  } else if (options.type === 'uncheck') {
+    if (options.currentRowKey === 'CHECK_ALL_BOX') {
+      for (const data of dataBindDialog.dataSet) {
+        const found = dataBindDialog.data.binds.findIndex((elem: any) => {
+          return elem.id === data.id;
+        });
+        if (found > -1) {
+          dataBindDialog.data.binds.splice(found, 1);
+        }
+      }
+    } else {
+      const found = dataBindDialog.data.binds.findIndex((elem: any) => {
+        return elem.id === options.currentRowKey;
+      });
+      if (found > -1) {
+        dataBindDialog.data.binds.splice(found, 1);
+      }
+    }
+  }
+};
 </script>
 <style lang="postcss" scoped>
 .props {
@@ -334,5 +527,10 @@ const onMenuMore = (e: any, item: any, i: number) => {
       margin-left: 6px;
     }
   }
+
+  .data-list {
+    height: 300px;
+    overflow: auto;
+  }
 }
 </style>

+ 22 - 21
vite.config.ts

@@ -1,44 +1,45 @@
-import { defineConfig, Plugin, ViteDevServer } from "vite";
-import vue from "@vitejs/plugin-vue";
-import vueJsx from "@vitejs/plugin-vue-jsx";
-import * as path from "path";
-import monacoEditorPlugin from "vite-plugin-monaco-editor";
-import * as fs from "fs";
-import formidable from "formidable";
+import { defineConfig, Plugin, ViteDevServer } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import vueJsx from '@vitejs/plugin-vue-jsx';
+import * as path from 'path';
+import monacoEditorPlugin from 'vite-plugin-monaco-editor';
+import * as fs from 'fs';
+import formidable from 'formidable';
 
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [vue(), vueJsx(), monacoEditorPlugin({}), fileList()],
   resolve: {
     alias: {
-      "@": path.resolve(__dirname, "./src/"),
-      "@meta2d": path.resolve(__dirname, "../meta2d.js/packages"),
+      '@': path.resolve(__dirname, './src/'),
+      '@meta2d': path.resolve(__dirname, '../meta2d.js/packages'),
     },
   },
   build: {
-    outDir: "v",
+    outDir: 'v',
   },
   server: {
     proxy: {
-      "/image": "https://2d.le5le.com/",
-      "/file": "https://2d.le5le.com/",
-      "/api": "https://2d.le5le.com/",
+      '/image': 'https://2d.le5le.com/',
+      '/file': 'https://2d.le5le.com/',
+      '/api': 'https://2d.le5le.com/',
+      '/api/device': 'http://127.0.0.1:777/',
     },
   },
 });
 
 function fileList(): Plugin {
   return {
-    name: "vite-plugin-svg-png-files",
+    name: 'vite-plugin-svg-png-files',
     configureServer(server: ViteDevServer) {
       server.middlewares.use((req, res, next) => {
         const url = req.url as string;
 
         if (
-          (url.startsWith("/svg/") || url.startsWith("/png/")) &&
-          url.endsWith("/")
+          (url.startsWith('/svg/') || url.startsWith('/png/')) &&
+          url.endsWith('/')
         ) {
-          const pwd = decodeURI(path.join(__dirname, "public", url));
+          const pwd = decodeURI(path.join(__dirname, 'public', url));
           const files = fs.readdirSync(pwd, {
             withFileTypes: true,
           });
@@ -49,21 +50,21 @@ function fileList(): Plugin {
           }[] = [];
           for (const item of files) {
             if (item.isDirectory()) {
-              list.push({ name: item.name, type: "directory" });
+              list.push({ name: item.name, type: 'directory' });
             } else {
               list.push({ name: item.name });
             }
           }
           res.end(JSON.stringify(list));
-        } else if (url === "/img" && req.method === "POST") {
+        } else if (url === '/img' && req.method === 'POST') {
           const form = formidable({
-            uploadDir: decodeURI(path.join(__dirname, "public", "/img")),
+            uploadDir: decodeURI(path.join(__dirname, 'public', '/img')),
             keepExtensions: true,
           });
           form.parse(req, (err, fields, files) => {
             if (!err) {
               res.end(
-                JSON.stringify({ url: "/img/" + files.file.newFilename })
+                JSON.stringify({ url: '/img/' + files.file.newFilename })
               );
             }
           });