Bläddra i källkod

Merge branch 'main' of https://github.com/le5le-com/visualization-design

ananzhusen 2 år sedan
förälder
incheckning
f24fcaf7d3

+ 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();
+}

+ 1 - 1
src/services/defaults.ts

@@ -242,7 +242,7 @@ export const shapeLib = [
             {
               key: 'text',
               type: 'text',
-              name: '文',
+              name: '文',
             },
           ] as FormItemType[],
         },

+ 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;
 }

+ 48 - 6
src/styles/props.css

@@ -54,13 +54,21 @@
       .t-input__inner {
         text-overflow: unset;
         &::placeholder {
-          color: var(--color-desc);
+          color: var(--color-gray);
         }
       }
       &:hover,
       &.t-is-focused {
         border-color: var(--color-border-input);
       }
+
+      &.t-is-disabled {
+        border-color: var(--color-border-input) !important;
+
+        .t-input__inner {
+          color: var(--color-desc);
+        }
+      }
     }
 
     & > input {
@@ -70,7 +78,7 @@
       color: var(--color-desc);
 
       &::placeholder {
-        color: var(--color-desc);
+        color: var(--color-gray);
       }
     }
 
@@ -89,6 +97,17 @@
     }
   }
 
+  .t-dialog {
+    .form-item {
+      .t-input {
+        border-color: var(--color-border-input);
+        &:hover {
+          border-color: var(--color-primary);
+        }
+      }
+    }
+  }
+
   .t-collapse.t--border-less {
     .t-collapse-panel__header {
       font-size: 13px;
@@ -135,19 +154,42 @@
     }
   }
 
+  .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);
 
-    &.t-button--theme-primary {
-      border-color: var(--color-primary);
-    }
-
     &:hover {
       border-color: var(--color-primary);
       color: var(--color-primary);
     }
 
+    &.t-button--variant-base {
+      border-color: var(--td-bg-color-component);
+      &:hover {
+        color: var(--color);
+        border-color: var(--color-border-input-hover);
+        background-color: var(--color-border-input-hover);
+      }
+
+      &.t-button--theme-primary {
+        border-color: var(--color-primary);
+        &:hover {
+          color: var(--color);
+          border-color: var(--color-primary-hover);
+          background-color: var(--color-primary-hover);
+        }
+      }
+    }
+
     &.icon {
       border-color: transparent;
       padding: 0;

+ 30 - 35
src/styles/tdesign.css

@@ -123,6 +123,13 @@
         }
       }
     }
+
+    &.t-dropdown__item--disabled {
+      .t-dropdown__item-text {
+        background-color: var(--color-background-popup) !important;
+        color: var(--color-desc);
+      }
+    }
   }
 
   .t-divider {
@@ -179,44 +186,15 @@
 .t-table {
   font-size: 13px;
 
-  th {
-    background: #f7f8fa;
-    color: #7f838c;
-    border-color: #edeef0;
-  }
-
-  td {
-    color: #474e59;
-    border-color: #edeef0;
-  }
+  .t-table__pagination {
+    padding: 16px 0;
 
-  &.border-none {
-    th {
-      background: none !important;
-      border: none;
-    }
-    td {
-      border: none;
-    }
-
-    .t-table__row--selected {
-      background-color: var(--color-primary-background);
-    }
-  }
-
-  &.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;
 }
@@ -407,11 +385,10 @@
 .t-popup__arrow {
   &::before {
     background-color: var(--color-background-popup);
-    box-shadow: none !important;
   }
 }
 
-.t-popup[data-popper-placement^="top"] .t-popup__arrow::before {
+.t-popup[data-popper-placement^='top'] .t-popup__arrow::before {
   top: -4px;
 }
 
@@ -452,3 +429,21 @@
 .t-divider--horizontal {
   margin: 0;
 }
+
+.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);
+  }
+}

+ 5 - 1
src/styles/var.css

@@ -13,7 +13,7 @@
   --color-bland-hover: #fa541c;
 
   --color-primary: #4583ff;
-  --color-primary-hover: #1677ff;
+  --color-primary-hover: #0c56eb;
   --color-primary-disabled: #bae7ff;
   --color-success: #52c41a;
   --color-success-hover: #3fad09;
@@ -36,6 +36,7 @@
   --color-border: #000000;
   --color-sub-border: #f7f7f7;
   --color-border-input: #535f79;
+  --color-border-input-hover: #454f64;
 
   --color-scrollbar: #dad7d7;
   --color-scrollbar-hover: #e5e5e5;
@@ -62,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;
 }

+ 449 - 19
src/views/components/PenDatas.vue

@@ -1,18 +1,44 @@
 <template>
   <div class="props">
-    <div class="grid px-16 py-12" v-if="pen.realTimes">
-      <div class="title">数据名称</div>
-      <div class="title">值</div>
-      <div>
+    <div class="px-16 py-16" v-if="pen.realTimes && pen.realTimes.length">
+      <div class="grid" style="line-height: 30px">
+        <div class="title">数据名</div>
+        <div class="title ml-8">值</div>
+        <div>
+          <div style="text-align: right; padding-right: 2px">
+            <t-icon name="more" />
+          </div>
+        </div>
+      </div>
+      <div class="grid" v-for="(item, i) in pen.realTimes">
+        <t-tooltip :content="item.key" placement="top">
+          <label class="label">{{ item.label }}</label>
+        </t-tooltip>
+        <div class="value">
+          <t-input v-model="item.value" />
+        </div>
+        <div class="actions">
+          <t-tooltip content="变量绑定" placement="top">
+            <t-icon name="link" class="hover" @click="onBind(item)" />
+          </t-tooltip>
+          <t-dropdown
+            :options="moreOptions"
+            @click="onMenuMore($event, item, i)"
+            :minColumnWidth="80"
+          >
+            <t-icon name="more" class="more hover" />
+          </t-dropdown>
+        </div>
+      </div>
+      <div class="mt-8">
         <t-dropdown
           :options="options"
-          @click="addRealTime(i, $event)"
+          @click="addRealTime"
           :minColumnWidth="150"
         >
-          <t-icon name="add-circle" class="hover ml-4" />
+          <a> <t-icon name="add-rectangle" /> 添加动态数据 </a>
         </t-dropdown>
       </div>
-      <template v-for="item in pen.realTimes"> </template>
     </div>
     <div class="flex column center blank" v-else>
       <img src="/img/blank.png" />
@@ -20,84 +46,458 @@
       <div class="mt-8">
         <t-dropdown
           :options="options"
-          @click="addRealTime(i, $event)"
+          @click="addRealTime"
           :minColumnWidth="150"
-          trigger="click"
         >
           <t-button style="height: 30px"> 添加动态数据 </t-button>
         </t-dropdown>
       </div>
     </div>
   </div>
+
+  <t-dialog
+    v-if="addDataDialog.show"
+    :visible="true"
+    class="data-dialog"
+    :header="addDataDialog.header"
+    @close="addDataDialog.show = false"
+    @confirm="onConfirmData"
+  >
+    <div class="form-item mt-16">
+      <label>数据名</label>
+      <t-input
+        v-model="addDataDialog.data.label"
+        placeholder="简短描述"
+        :disabled="!!addDataDialog.data.keywords"
+        @blur="onChangeLabel"
+      />
+    </div>
+    <div class="form-item mt-16">
+      <label>属性名</label>
+      <t-input
+        v-model="addDataDialog.data.key"
+        placeholder="关键字"
+        :disabled="!!addDataDialog.data.keywords"
+      />
+    </div>
+    <div class="form-item mt-16">
+      <label>类型</label>
+      <t-select
+        class="w-full"
+        :options="typeOptions"
+        v-model="addDataDialog.data.type"
+        placeholder="字符串"
+        :disabled="!!addDataDialog.data.keywords"
+        @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>
+      <t-select
+        v-else-if="addDataDialog.data.type === 'bool'"
+        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="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="dataBindDialog.show"
+    :visible="true"
+    class="data-link-dialog"
+    header="变量绑定"
+    @cancel="
+      dataBindDialog.data.binds = dataBindDialog.bkBinds;
+      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"
+          trigger="click"
+        >
+          <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: '自定义',
+    divider: true,
+  },
   {
     value: 'x',
-    content: 'X坐标',
+    content: 'X',
     type: 'number',
+    keywords: true,
   },
   {
     value: 'y',
-    content: 'Y坐标',
+    content: 'Y',
     type: 'number',
+    keywords: true,
   },
   {
     value: 'width',
     content: '宽',
     type: 'number',
+    keywords: true,
   },
   {
     value: 'height',
     content: '高',
     type: 'number',
+    keywords: true,
   },
   {
     value: 'visible',
     content: '显示',
     type: 'bool',
+    keywords: true,
   },
   {
     value: 'text',
     content: '文字',
+    keywords: true,
   },
   {
     value: 'progress',
     content: '进度',
+    keywords: true,
   },
   {
     value: 'showChild',
     content: '状态',
+    keywords: true,
   },
   {
     value: 'rotate',
     content: '旋转',
     type: 'number',
-    divider: true,
+    keywords: true,
+  },
+]);
+
+const moreOptions = ref<any>([
+  {
+    value: 'edit',
+    content: '编辑',
+  },
+  {
+    value: 'delete',
+    content: '移除',
   },
 ]);
 
+const typeOptions = [
+  {
+    label: '字符串',
+  },
+  {
+    label: '数字',
+    value: 'number',
+  },
+  {
+    label: '布尔',
+    value: 'bool',
+  },
+];
+
+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;
     options.value.push(...pen.realTimesOptions);
   }
-  options.value[options.value.length - 1].divider = true;
-  options.value.push({ content: '自定义', value: '' });
 });
 
-const addRealTime = () => {
+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 = {
+    label: e.content,
+    key: e.value,
+    type: e.type,
+    keywords: e.keywords,
+  };
+  if (e.keywords) {
+    addDataDialog.data.label = '';
+  }
+  addDataDialog.show = true;
+};
+
+const onChangeLabel = () => {
+  if (!addDataDialog.data.key) {
+    addDataDialog.data.key = addDataDialog.data.label;
+  }
+};
+
+const onConfirmData = () => {
   if (!pen.realTimes) {
     pen.realTimes = [];
   }
+  if (!addDataDialog.data.label || !addDataDialog.data.key) {
+    MessagePlugin.error('数据名或属性名不能为空!');
+    return;
+  }
+
+  if (addDataDialog.header === '添加动态数据') {
+    const found = pen.realTimes.findIndex((item: any) => {
+      return item.key === addDataDialog.data.key;
+    });
+    if (found > -1) {
+      MessagePlugin.error('已经存在相同属性数据!');
+      return;
+    }
+    pen.realTimes.push(addDataDialog.data);
+  }
 
-  pen.realTimes.push({});
+  addDataDialog.show = false;
+};
+
+const onMenuMore = (e: any, item: any, i: number) => {
+  switch (e.value) {
+    case 'edit':
+      addDataDialog.header = '编辑动态数据';
+      addDataDialog.data = item;
+      addDataDialog.show = true;
+      break;
+    case 'delete':
+      pen.realTimes.splice(i, 1);
+      break;
+    default:
+      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.bkBinds = [];
+  dataBindDialog.bkBinds.push(...item.binds);
+  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;
+
+  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);
+      }
+    }
+  }
+};
+
+const onRemoveBind = (index: number) => {
+  dataBindDialog.data.binds.splice(index, 1);
+
+  dataBindDialog.selectedIds = [];
+  for (const i of dataBindDialog.data.binds) {
+    dataBindDialog.selectedIds.push(i.id);
+  }
 };
 </script>
 <style lang="postcss" scoped>
@@ -105,7 +505,7 @@ const addRealTime = () => {
   height: 100%;
 
   .grid {
-    grid-template-columns: 1fr 1fr 30px;
+    grid-template-columns: 80px 154px 40px;
   }
 
   .blank {
@@ -115,5 +515,35 @@ const addRealTime = () => {
       opacity: 0.9;
     }
   }
+
+  .label {
+    width: fit-content;
+    font-size: 10px;
+    line-height: 28px;
+    color: var(--color-desc);
+  }
+
+  .value {
+    padding-right: 8px;
+
+    :deep(.t-input) {
+      height: 26px;
+      border-color: transparent;
+      &:hover {
+        border-color: var(--color-primary);
+      }
+    }
+  }
+
+  .actions {
+    svg {
+      margin-left: 6px;
+    }
+  }
+
+  .data-list {
+    height: 300px;
+    overflow: auto;
+  }
 }
 </style>

+ 4 - 4
src/views/components/PenProps.vue

@@ -44,7 +44,7 @@
             <t-input
               class="ml-4"
               label="X"
-              placeholder="x坐标"
+              placeholder="X"
               v-model.number="data.rect.x"
               style="width: 80px"
               :format="decimalPlaces"
@@ -54,7 +54,7 @@
             <t-input
               class="ml-4"
               label="Y"
-              placeholder="y坐标"
+              placeholder="Y"
               v-model.number="data.rect.y"
               style="width: 80px"
               :format="decimalPlaces"
@@ -1040,7 +1040,7 @@ onBeforeMount(() => {
   // 测试代码
   data.pen.props.custom = [
     { label: '数字', key: 'a', type: 'number', placeholder: '输入提示' },
-    { label: '文', key: 'b' },
+    { label: '文', key: 'b' },
     { label: '布尔', key: 'c', type: 'bool', placeholder: '输入提示' },
     { label: '颜色', key: 'd', type: 'color', placeholder: '输入提示' },
     {
@@ -1171,7 +1171,7 @@ const titleOptions: monacoOption[] = [
   {
     key: 'title',
     value: 0,
-    name: '文',
+    name: '文',
     tip: '支持markdown',
     code: '',
     language: 'markdown',

+ 21 - 21
vite.config.ts

@@ -1,44 +1,44 @@
-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/',
     },
   },
 });
 
 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 +49,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 })
               );
             }
           });