Pārlūkot izejas kodu

Merge branch 'master' into staffNoFix

sunhongyao341504 2 gadi atpakaļ
vecāks
revīzija
5a9370efa4

+ 1 - 4
.env.development

@@ -7,9 +7,6 @@ VITE_PUBLIC_PATH = /skyeye-admin/
 # 是否开启mock
 VITE_USE_MOCK = false
 
-# 网站前缀
-VITE_BASE_URL = /
-
 # 是否删除console
 VITE_DROP_CONSOLE = true
 
@@ -22,7 +19,7 @@ VITE_PROXY=[["/skyeye-admin-api","http://192.168.1.102:8800/api"]]
 # API 接口地址
 VITE_GLOB_API_URL = 
 # 图片上传地址
-VITE_GLOB_UPLOAD_URL= http://172.16.23.144:8086
+VITE_GLOB_UPLOAD_URL=  
 
 # 图片前缀地址
 VITE_GLOB_IMG_URL = //192.168.1.102/skyeye_static/

+ 1 - 1
.env.production

@@ -12,7 +12,7 @@ VITE_DROP_CONSOLE = true
 VITE_GLOB_API_URL = 
 
 # 图片上传地址
-VITE_GLOB_UPLOAD_URL= http://172.16.23.144:8086
+VITE_GLOB_UPLOAD_URL= 
 
 # 图片前缀地址
 VITE_GLOB_IMG_URL= /skyeye_static

+ 1 - 1
.env.test

@@ -2,7 +2,7 @@
 VITE_USE_MOCK = true
 
 # 网站根目录
-VITE_PUBLIC_PATH = /
+VITE_PUBLIC_PATH = /cloud-admin/
 
 
 # 是否删除console

+ 1 - 0
.gitignore

@@ -24,3 +24,4 @@ pnpm-debug.log*
 *.sln
 *.sw?
 /components.d.ts
+pnpm-lock.yaml

+ 13 - 0
CHANGELOG.md

@@ -2,6 +2,19 @@
 
 ## Pending
 
+## 1.4.0 (2023-11-25)
+
+- 🌟 新增 `BasicForm.schemas` 支持 `hidden`,可配置成函数,示例:组件示例-表单-基础使用
+- 💎 优化 `useECharts` 方法,考虑菜单收起宽度变化
+- `依赖升级`
+
+## 1.3.9 (2023-07-26)
+
+- 🐞 修复 `mock` 配置异常
+- 🐞 修复 `GlobConfig` 类型缺失
+- 💎 优化 `ImportMeta` 类型定义
+- `依赖升级`
+
 ## 1.3.8 (2023-06-12)
 
 - 🐞 修复 `修改密码异常`

+ 35 - 21
build/vite/plugin/html.ts

@@ -6,8 +6,17 @@ import type { PluginOption } from 'vite';
 import { createHtmlPlugin } from 'vite-plugin-html';
 import pkg from '../../../package.json';
 import { GLOB_CONFIG_FILE_NAME } from '../../constant';
+import { simpleGit } from 'simple-git';
+const git = simpleGit();
 
-export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
+async function getLatestHash() {
+  const gitLog = await git.log();
+  /** 截取git hash值最后8位 */
+  const lastStrNum = 8;
+  return gitLog.latest?.hash.substring(-lastStrNum, lastStrNum);
+}
+
+export function configHtmlPlugin(env: ViteEnv, isBuild: boolean): Promise<PluginOption[]> {
   const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env;
 
   const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`;
@@ -16,25 +25,30 @@ export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
     return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
   };
 
-  const htmlPlugin: PluginOption[] = createHtmlPlugin({
-    minify: isBuild,
-    inject: {
-      // Inject data into ejs template
-      data: {
-        title: VITE_GLOB_APP_TITLE,
-      },
-      // Embed the generated app.config.js file
-      tags: isBuild
-        ? [
-            {
-              tag: 'script',
-              attrs: {
-                src: getAppConfigSrc(),
-              },
-            },
-          ]
-        : [],
-    },
+  return new Promise((resolve) => {
+    getLatestHash().then((hash) => {
+      const htmlPlugin: PluginOption[] = createHtmlPlugin({
+        minify: isBuild,
+        inject: {
+          // Inject data into ejs template
+          data: {
+            title: VITE_GLOB_APP_TITLE,
+            hash,
+          },
+          // Embed the generated app.config.js file
+          tags: isBuild
+            ? [
+                {
+                  tag: 'script',
+                  attrs: {
+                    src: getAppConfigSrc(),
+                  },
+                },
+              ]
+            : [],
+        },
+      });
+      resolve(htmlPlugin);
+    });
   });
-  return htmlPlugin;
 }

+ 120 - 104
index.html

@@ -1,124 +1,140 @@
 <!DOCTYPE html>
 <html lang="zh-cmn-Hans" id="htmlRoot" data-theme="light">
-  <head>
-    <meta charset="UTF-8" />
-    <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
-    <meta content="webkit" name="renderer" />
-    <meta
-      content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
-      name="viewport"
-    />
-    <link href="/favicon.ico" rel="icon" />
-    <title><%= title %></title>
-  </head>
-  <body>
-    <div id="app">
-      <style>
-        .first-loading-wrap {
-          display: flex;
-          width: 100%;
-          height: 100vh;
-          justify-content: center;
-          align-items: center;
-          flex-direction: column;
-        }
 
-        .first-loading-wrap > h1 {
-          font-size: 128px;
-        }
+<head>
+  <meta charset="UTF-8" />
+  <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
+  <meta content="webkit" name="renderer" />
+  <meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
+    name="viewport" />
+  <link href="/favicon.ico" rel="icon" />
+  <title>
+    <%= title %>
+  </title>
+</head>
 
-        .first-loading-wrap .loading-wrap {
-          padding: 98px;
-          display: flex;
-          justify-content: center;
-          align-items: center;
-        }
+<body>
+  <div id="app">
+    <style>
+      .first-loading-wrap {
+        display: flex;
+        width: 100%;
+        height: 100vh;
+        justify-content: center;
+        align-items: center;
+        flex-direction: column;
+      }
 
-        .dot {
-          animation: antRotate 1.2s infinite linear;
-          transform: rotate(45deg);
-          position: relative;
-          display: inline-block;
-          font-size: 32px;
-          width: 32px;
-          height: 32px;
-          box-sizing: border-box;
-        }
+      .first-loading-wrap>h1 {
+        font-size: 128px;
+      }
 
-        .dot i {
-          width: 14px;
-          height: 14px;
-          position: absolute;
-          display: block;
-          background-color: #1890ff;
-          border-radius: 100%;
-          transform: scale(0.75);
-          transform-origin: 50% 50%;
-          opacity: 0.3;
-          animation: antSpinMove 1s infinite linear alternate;
-        }
+      .first-loading-wrap .loading-wrap {
+        padding: 98px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
 
-        .dot i:nth-child(1) {
-          top: 0;
-          left: 0;
-        }
+      .dot {
+        animation: antRotate 1.2s infinite linear;
+        transform: rotate(45deg);
+        position: relative;
+        display: inline-block;
+        font-size: 32px;
+        width: 32px;
+        height: 32px;
+        box-sizing: border-box;
+      }
 
-        .dot i:nth-child(2) {
-          top: 0;
-          right: 0;
-          -webkit-animation-delay: 0.4s;
-          animation-delay: 0.4s;
-        }
+      .dot i {
+        width: 14px;
+        height: 14px;
+        position: absolute;
+        display: block;
+        background-color: #1890ff;
+        border-radius: 100%;
+        transform: scale(0.75);
+        transform-origin: 50% 50%;
+        opacity: 0.3;
+        animation: antSpinMove 1s infinite linear alternate;
+      }
 
-        .dot i:nth-child(3) {
-          right: 0;
-          bottom: 0;
-          -webkit-animation-delay: 0.8s;
-          animation-delay: 0.8s;
-        }
+      .dot i:nth-child(1) {
+        top: 0;
+        left: 0;
+      }
 
-        .dot i:nth-child(4) {
-          bottom: 0;
-          left: 0;
-          -webkit-animation-delay: 1.2s;
-          animation-delay: 1.2s;
-        }
+      .dot i:nth-child(2) {
+        top: 0;
+        right: 0;
+        -webkit-animation-delay: 0.4s;
+        animation-delay: 0.4s;
+      }
 
-        @keyframes antRotate {
-          to {
-            -webkit-transform: rotate(405deg);
-            transform: rotate(405deg);
-          }
+      .dot i:nth-child(3) {
+        right: 0;
+        bottom: 0;
+        -webkit-animation-delay: 0.8s;
+        animation-delay: 0.8s;
+      }
+
+      .dot i:nth-child(4) {
+        bottom: 0;
+        left: 0;
+        -webkit-animation-delay: 1.2s;
+        animation-delay: 1.2s;
+      }
+
+      @keyframes antRotate {
+        to {
+          -webkit-transform: rotate(405deg);
+          transform: rotate(405deg);
         }
+      }
 
-        @-webkit-keyframes antRotate {
-          to {
-            -webkit-transform: rotate(405deg);
-            transform: rotate(405deg);
-          }
+      @-webkit-keyframes antRotate {
+        to {
+          -webkit-transform: rotate(405deg);
+          transform: rotate(405deg);
         }
+      }
 
-        @keyframes antSpinMove {
-          to {
-            opacity: 1;
-          }
+      @keyframes antSpinMove {
+        to {
+          opacity: 1;
         }
+      }
 
-        @-webkit-keyframes antSpinMove {
-          to {
-            opacity: 1;
-          }
+      @-webkit-keyframes antSpinMove {
+        to {
+          opacity: 1;
         }
-      </style>
-      <div class="first-loading-wrap">
-        <div class="loading-wrap">
-          <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
-        </div>
+      }
+    </style>
+    <div class="first-loading-wrap">
+      <div class="loading-wrap">
+        <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
       </div>
     </div>
-    <script>
-      var globalThis = window;
-    </script>
-    <script src="/src/main.ts" type="module"></script>
-  </body>
-</html>
+  </div>
+  <div class="versionHash">
+    <span>version hash: <%= hash %></span>
+    <style>
+      .versionHash {
+        position: fixed;
+        bottom: 10px;
+        right: 20px;
+        font-size: 12px;
+        color: #ccc;
+        z-index: 10000;
+      }
+    </style>
+  </div>
+  <script>
+    var globalThis = window;
+  </script>
+  <script src="/src/main.ts" type="module"></script>
+</body>
+
+</html>

+ 5 - 5
mock/_util.ts

@@ -13,7 +13,7 @@ export function resultPageSuccess<T = any>(
   page: number,
   pageSize: number,
   list: T[],
-  { message = 'ok' } = {},
+  { msg = 'ok' } = {},
 ) {
   const pageData = pagination(page, pageSize, list);
 
@@ -24,15 +24,15 @@ export function resultPageSuccess<T = any>(
       pageCount: list.length,
       list: pageData,
     }),
-    message,
+    msg,
   };
 }
 
-export function resultError(message = 'Request failed', { code = -1, result = null } = {}) {
+export function resultError(msg = 'Request failed', { code = -1, data = null } = {}) {
   return {
     code,
-    result,
-    message,
+    data,
+    msg,
     type: 'error',
   };
 }

+ 4 - 2
mock/system/dictionary.ts

@@ -136,12 +136,14 @@ export default [
           return item.label.indexOf(keywords) != -1;
         });
       }
-      const count = list.length > Number(pageSize) ? Math.ceil(list.length / Number(pageSize)) : 0;
+      const count =
+        list.length > Number(pageSize) ? Math.ceil(list.length / Number(pageSize)) : list.length;
+      const itemCount = count > Number(pageSize) ? count * Number(pageSize) : count;
       return resultSuccess({
         page: Number(page),
         pageSize: Number(pageSize),
         pageCount: count,
-        itemCount: count * Number(pageSize),
+        itemCount,
         list,
       });
     },

+ 6 - 10
package.json

@@ -1,11 +1,6 @@
 {
   "name": "naive-admin-element-tenant",
-  "version": "1.3.8",
-  "author": {
-    "name": "Ahjung",
-    "email": "735878602@qq.com",
-    "url": "https://github.com/jekip"
-  },
+  "version": "1.3.9",
   "private": true,
   "scripts": {
     "bootstrap": "pnpm install",
@@ -66,7 +61,7 @@
     "url-join": "5.0.0",
     "vue": "3.3.4",
     "vue-hooks-plus": "1.8.6",
-    "vue-konva": "^3.0.2",
+    "vue-konva": "3.0.2",
     "vue-router": "4.1.2",
     "vue-types": "4.1.1",
     "vuedraggable": "4.1.0",
@@ -108,6 +103,7 @@
     "rimraf": "3.0.2",
     "rollup-plugin-visualizer": "5.8.3",
     "sass": "1.53.0",
+    "simple-git": "3.22.0",
     "stylelint": "14.9.1",
     "stylelint-config-prettier": "9.0.3",
     "stylelint-config-standard": "25.0.0",
@@ -116,9 +112,9 @@
     "tailwindcss": "3.3.2",
     "ts-node": "10.9.1",
     "typescript": "4.7.4",
-    "vite": "5.0.10",
+    "vite": "5.1.3",
     "vite-plugin-compression": "0.5.1",
-    "vite-plugin-html": "3.2.0",
+    "vite-plugin-html": "3.2.2",
     "vite-plugin-mock": "2.9.6",
     "vite-plugin-style-import": "2.0.0",
     "vite-plugin-svg-icons": "2.0.1",
@@ -155,4 +151,4 @@
       ]
     }
   }
-}
+}

+ 1 - 0
postcss.config.js

@@ -1,5 +1,6 @@
 module.exports = {
   plugins: {
+    'tailwindcss/nesting': {},
     tailwindcss: {},
     autoprefixer: {},
   },

BIN
public/upload-user-templete/templete.xlsx


+ 16 - 3
src/components/Form/src/BasicForm.vue

@@ -5,14 +5,14 @@
         v-bind="getCol"
         v-for="schema in getSchema"
         :key="schema.field"
-        :span="schema.hidden ? 0 : getCol.span"
+        :span="getHidden(schema) ? 0 : getCol.span"
       >
         <el-form-item
           v-bind="schema"
           :label="schema.label"
           :prop="schema.field"
           :showFeedback="schema.showFeedback"
-          v-if="!schema.hidden"
+          v-if="!getHidden(schema)"
         >
           <!--标签名右侧温馨提示-->
           <template #label v-if="schema.labelMessage">
@@ -170,7 +170,7 @@
   import type { Ref } from 'vue';
   import type { FormSchema, FormProps, FormActionType } from './types/form';
 
-  import { isArray } from '@/utils/is/index';
+  import { isArray, isBoolean, isFunction } from '@/utils/is/index';
   import { deepMerge } from '@/utils';
 
   const props = defineProps({ ...basicProps });
@@ -217,6 +217,19 @@
     };
   }
 
+  function getHidden(schema): boolean {
+    const hidden = schema.hidden;
+    const field = schema.field;
+    if (isBoolean(hidden)) return hidden;
+
+    if (isFunction(hidden)) {
+      const values = getFieldsValue();
+      const status = hidden({ schema, values, model: formModel, field });
+      return status;
+    }
+    return false;
+  }
+
   function getSpecComponentProps(schema) {
     const compProps = schema.componentProps ?? {};
     return {

+ 8 - 1
src/components/Form/src/types/form.ts

@@ -4,6 +4,13 @@ import type { CSSProperties } from 'vue';
 import type { ColProps } from 'element-plus/es/components/col/src/col';
 import type { ButtonProps } from 'element-plus/es/components/button/src/button';
 
+export interface RenderReturnParams {
+  schema: FormSchema;
+  values: Recordable;
+  model: Recordable;
+  field: string;
+}
+
 export interface componentProps {
   options?: any[];
   placeholder?: string;
@@ -29,7 +36,7 @@ export interface FormSchema {
   showFeedback?: boolean;
   showLabel?: boolean;
   requireMarkPlacement?: string;
-  hidden?: boolean;
+  hidden?: boolean | ((renderCallbackParams: RenderReturnParams) => boolean);
 }
 
 export interface FormProps {

+ 28 - 17
src/hooks/web/useECharts.ts

@@ -10,12 +10,14 @@ import { useBreakpoint } from '@/hooks/event/useBreakpoint';
 
 import echarts from '@/utils/lib/echarts';
 import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
+import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
 
 export function useECharts(
   elRef: Ref<HTMLDivElement>,
   theme: 'light' | 'dark' | 'default' = 'light',
 ) {
   const { getDarkTheme } = useDesignSetting();
+  const { getMenuSetting } = useProjectSetting();
 
   const getDarkMode = computed(() => {
     const appTheme = getDarkTheme.value ? 'dark' : 'light';
@@ -62,23 +64,26 @@ export function useECharts(
 
   function setOptions(options: EChartsOption, clear = true) {
     cacheOptions.value = options;
-    if (unref(elRef)?.offsetHeight === 0) {
-      useTimeoutFn(() => {
-        setOptions(unref(getOptions));
-      }, 30);
-      return;
-    }
-    nextTick(() => {
-      useTimeoutFn(() => {
-        if (!chartInstance) {
-          initCharts(getDarkMode.value as 'default');
-
-          if (!chartInstance) return;
-        }
-        clear && chartInstance?.clear();
-
-        chartInstance?.setOption(unref(getOptions));
-      }, 30);
+    return new Promise((resolve) => {
+      if (unref(elRef)?.offsetHeight === 0) {
+        useTimeoutFn(() => {
+          setOptions(unref(getOptions));
+          resolve(null);
+        }, 30);
+      }
+      nextTick(() => {
+        useTimeoutFn(() => {
+          if (!chartInstance) {
+            initCharts(getDarkMode.value as 'default');
+
+            if (!chartInstance) return;
+          }
+          clear && chartInstance?.clear();
+
+          chartInstance?.setOption(unref(getOptions));
+          resolve(null);
+        }, 30);
+      });
     });
   }
 
@@ -111,6 +116,12 @@ export function useECharts(
     return chartInstance;
   }
 
+  watch(getMenuSetting.value, (_) => {
+    useTimeoutFn(() => {
+      resizeFn();
+    }, 300);
+  });
+
   return {
     setOptions,
     resize,

+ 2 - 2
src/hooks/web/useTags.ts

@@ -1,5 +1,5 @@
 import { unref } from 'vue';
-import { useRouter } from 'vue-router';
+import { RouteLocationNormalized, useRouter } from 'vue-router';
 import type { Router } from 'vue-router';
 import { useTabsViewStore } from '@/store/modules/tabsView';
 import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
@@ -23,7 +23,7 @@ export function useTabs(_router?: Router) {
   }
 
   //更新tab标题
-  async function updateTabTitle(title: string, tab?: object) {
+  async function updateTabTitle(title: string, tab?: RouteLocationNormalized) {
     const targetTab = tab || getCurrentTab();
     await tabsViewStore.setTabTitle(title, targetTab);
   }

+ 2 - 0
src/main.ts

@@ -4,6 +4,7 @@ import './styles/index.scss';
 import 'element-plus/theme-chalk/display.css';
 import 'element-plus/theme-chalk/dark/css-vars.css';
 import 'nprogress/nprogress.css';
+import VueKonva from 'vue-konva';
 
 import { createApp } from 'vue';
 import App from './App.vue';
@@ -15,6 +16,7 @@ import { setupElement, setupDirectives, setupCustomComponents } from '@/plugins'
 
 async function bootstrap() {
   const app = createApp(App);
+  app.use(VueKonva);
 
   // 全局完整引入 element 组件
   setupElement(app);

+ 1 - 1
src/utils/http/axios/index.ts

@@ -10,7 +10,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
 
 import { useGlobSetting } from '@/hooks/setting';
 
-import { isString } from '@/utils/is/';
+import { isString } from '@/utils/is/index';
 import { deepMerge, isUrl } from '@/utils';
 import { setObjToUrlParams } from '@/utils/urlUtils';
 

+ 39 - 34
src/views/cameras/preview/components/CameraViewSetting/CameraViewSetting.vue

@@ -11,32 +11,38 @@
         :is-edit="isEdit"
       />
     </div>
-    <div class="cameraViewOverflow" :style="{ width: domWidth + 'px', height: domHeight + 'px' }">
-      <div
-        class="cameraViewSettingWrapper"
-        :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', scale: scale }"
-      >
-        <FenceEditor ref="fenceEditorRef" />
-        <div class="cameraVideo">
-          <CameraLiveVideo />
-        </div>
+    <div
+      class="cameraViewSettingWrapper"
+      :style="{ width: domWidth + 'px', height: domHeight + 'px' }"
+    >
+      <div class="fenceEditorWrapper">
+        <FenceEditor
+          ref="fenceEditorRef"
+          :dom-width="domWidth"
+          :canvas-size="{ width: canvasWidth, height: canvasHeight }"
+          :line-points="fenceStore.serverFencePoints || []"
+        />
       </div>
-      <div
-        class="presetAddWrapper"
-        :class="{ hidePresetControlCls: isEdit }"
-        v-if="!!cameraDetailStore.detail?.isPtz"
-      >
-        <CameraDirectionControl />
-        <ElButton
-          type="primary"
-          @click="handleAddPreset"
-          size="small"
-          style="margin-top: 20px; width: 100px"
-          >添加预置位</ElButton
-        >
-        <AddPresetModal v-if="addPresetModalVisible" @close="handleClose" @ok="handleAddPresetOk" />
+
+      <div class="cameraVideo">
+        <CameraLiveVideo />
       </div>
     </div>
+    <div
+      class="presetAddWrapper"
+      :class="{ hidePresetControlCls: isEdit }"
+      v-if="!!cameraDetailStore.detail?.isPtz"
+    >
+      <CameraDirectionControl />
+      <ElButton
+        type="primary"
+        @click="handleAddPreset"
+        size="small"
+        style="margin-top: 20px; width: 100px"
+        >添加预置位</ElButton
+      >
+      <AddPresetModal v-if="addPresetModalVisible" @close="handleClose" @ok="handleAddPresetOk" />
+    </div>
   </div>
   <div class="cameraParamsSettingWrapper">
     <div class="cameraParamsSetting">
@@ -48,7 +54,7 @@
 <script lang="ts" setup>
   import { computed, ref, watchEffect } from 'vue';
   import FenceToolbar from '../FenceToolbar/FenceToolbar.vue';
-  import FenceEditor from '../FenceEditor/FenceEditor.vue';
+  import FenceEditor from '../FenceEditorV2/FenceEditor.vue';
   import CameraLiveVideo from '../CameraLiveVideo/CameraLiveVideo.vue';
   import ViewWindowSetting from '../ViewWindowSetting/ViewWindowSetting.vue';
   import PresetSelect from '../PresetSelect/PresetSelect.vue';
@@ -152,17 +158,10 @@
         fenceEditorRef.value?.clear();
         return;
       }
-      const rawLinePoints = points.map((x) => {
-        const points: number[] = [];
-        x.forEach((line) => {
-          points.push(line[0], line[1]);
-        });
-        return points;
-      });
-      if (!rawLinePoints) return;
+
       /** 先清空原有的 */
       fenceEditorRef.value?.clear();
-      fenceEditorRef.value?.createLines(rawLinePoints);
+      // fenceEditorRef.value?.createLines(rawLinePoints);
       fenceEditorRef.value?.setEditMode();
       isEdit.value = true;
       return;
@@ -181,7 +180,6 @@
   .cameraViewSettingWrapper {
     position: relative;
     border: 1px solid #ccc;
-    transform-origin: left top;
   }
   .cameraViewOverflow {
     overflow: hidden;
@@ -190,6 +188,9 @@
 
   .cameraVideo {
     position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 8;
     background: #ccc;
     width: 100%;
     height: 100%;
@@ -230,4 +231,8 @@
   .hidePresetControlCls {
     display: none;
   }
+  .fenceEditorWrapper {
+    position: relative;
+    z-index: 9;
+  }
 </style>

+ 0 - 15
src/views/cameras/preview/components/FenceEditor/FenceEditor.vue

@@ -655,14 +655,6 @@
     return gropuPoints;
   };
 
-  const initStageByJSON = (param: { width: number; height: number }) => {
-    stage?.setAttrs({ width: param.width, height: param.height });
-  };
-
-  const toRawObject = () => {
-    return stage?.toObject();
-  };
-
   /** 退出编辑模式 */
   const exitEditMode = () => {
     setCurrentGroup(null);
@@ -678,20 +670,13 @@
     layer?.removeChildren();
   };
 
-  const setScale = (scale: number) => {
-    stage?.setAttr('scaleX', scale);
-  };
-
   defineExpose({
     remove: removeCurrent,
     toObject,
-    toRawObject,
     createLines,
-    initStageByJSON,
     exitEditMode,
     setEditMode,
     clear,
-    setScale,
   });
 </script>
 

+ 258 - 0
src/views/cameras/preview/components/FenceEditorV2/FenceEditor.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="overflowWrapper" :style="{ width: props.domWidth + 'px', height: domHeight + 'px' }">
+    <div
+      class="scaleWrapper"
+      :style="{
+        scale: scale,
+        width: props.canvasSize.width + 'px',
+        height: props.canvasSize.height + 'px',
+      }"
+    >
+      <v-stage
+        :config="configKonva"
+        @mouse-down="handleStageMouseDown"
+        @mouse-move="handleStageMouseMove"
+        ref="stageRef"
+      >
+        <v-layer>
+          <FenceItem
+            :fenceGroups="fenceGroups"
+            :draggable="!drawingGroupId"
+            @select-group="handleSelectGroup"
+            :is-edit="isEdit"
+          />
+        </v-layer>
+      </v-stage>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import Konva from 'konva';
+  import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
+  import FenceItem from './FenceItem.vue';
+  import { createCircleConfigItem, createGroupConfig } from './utils';
+  import { FenceGroup } from './types';
+  import { ElMessage } from 'element-plus';
+  import { GROUP_NAME } from './constants';
+
+  const props = defineProps<{
+    /** 电子围栏的坐标 */
+    linePoints: [number, number][][];
+    /** 画布的大小 */
+    canvasSize: { width: number; height: number };
+    /** dom的真实尺寸 */
+    domWidth: number;
+  }>();
+
+  const scale = computed(() => {
+    return props.domWidth / props.canvasSize.width;
+  });
+
+  const stageRef = ref();
+  const isEdit = ref(false);
+
+  const fenceGroups = ref<FenceGroup[]>([]);
+
+  /** 当前正在画的多边形的groupId */
+  const drawingGroupId = ref('');
+  /** 当前选中的多边形groupId,点击、拖拽、画线都会给它赋值 */
+  const currentGroupId = ref('');
+
+  watch(
+    () => props.linePoints,
+    (newLinePoints) => {
+      const configs: FenceGroup[] =
+        newLinePoints.map((points) => {
+          const flattenedPoints = points.reduce((total, next) => {
+            return [...total, ...next];
+          }, [] as number[]);
+          return createGroupConfig(flattenedPoints, scale.value);
+        }) || [];
+      fenceGroups.value = configs;
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  onMounted(() => {
+    /** 取消默认的右键 */
+    document.oncontextmenu = function () {
+      return false;
+    };
+  });
+
+  onUnmounted(() => {
+    /** 取消默认的右键 */
+    document.oncontextmenu = function () {
+      return true;
+    };
+  });
+
+  const configKonva = computed(() => {
+    return props.canvasSize;
+  });
+
+  const canvasRatio = computed(() => {
+    const size = props.canvasSize;
+    if (!size) return 1;
+    return size.height / size.width;
+  });
+
+  const domHeight = computed(() => {
+    return props.domWidth * canvasRatio.value;
+  });
+
+  const handleStageMouseDown = (e) => {
+    if (!isEdit.value) return;
+    /**
+     * parent存在,说明点击的不是stage
+     * !drawingGroupId,说明当前不处于绘制多边形中
+     * 这两种情况,都不能执行stage的点击事件,要执行点击对象的默认事件
+     */
+    if (e.target.parent && !drawingGroupId.value) return;
+    const stage = e.currentTarget as Konva.Stage;
+    // 获取当前鼠标相对舞台的位置
+    const mousePosition = stage.getPointerPosition();
+    if (!mousePosition?.x || !mousePosition?.y) return;
+    const point = [mousePosition.x, mousePosition.y] as [number, number];
+
+    /** 如果还没开始画线,那么增加第一个点 */
+    if (!drawingGroupId.value) {
+      const groupConfig = createGroupConfig(point, scale.value);
+      drawingGroupId.value = groupConfig.uid;
+      groupConfig._temp.points = point;
+      fenceGroups.value.push(groupConfig);
+    } else {
+      /** 右键点击,取消最后一个点 */
+      if (e.evt.button === 2) {
+        /** 否则就追加点 */
+        const groupConfig = fenceGroups.value.find((x) => x.uid === drawingGroupId.value);
+        if (!groupConfig) {
+          console.error('drawingGroupId无效', drawingGroupId.value);
+          return;
+        }
+        if ((groupConfig._temp.points.length || 0) <= 4) {
+          ElMessage({
+            message: '顶点数必须大于2个!',
+            type: 'warning',
+            center: true,
+            duration: 1000,
+          });
+          groupConfig.lineConfig.points = [];
+          groupConfig.circleConfigs = [];
+        } else {
+          groupConfig.lineConfig.points = groupConfig._temp.points;
+        }
+        drawingGroupId.value = '';
+        groupConfig._temp.points = [];
+        return;
+      }
+      /** 否则就追加点 */
+      const groupConfig = fenceGroups.value.find((x) => x.uid === drawingGroupId.value);
+      if (!groupConfig) {
+        console.error('drawingGroupId无效', drawingGroupId.value);
+        return;
+      }
+      const tempPoints = groupConfig._temp?.points || [];
+      const finalPoints = [...tempPoints, ...point];
+      groupConfig.lineConfig.points = finalPoints;
+      groupConfig._temp.points = finalPoints;
+
+      const circleConfig = createCircleConfigItem(
+        point,
+        groupConfig.circleConfigs.length,
+        scale.value,
+      );
+      groupConfig.circleConfigs.push(circleConfig);
+    }
+  };
+
+  const handleStageMouseMove = (e) => {
+    if (!isEdit.value) return;
+    const stage = e.currentTarget as Konva.Stage;
+    /** 获取当前鼠标的坐标 */
+    const mousePosition = stage.getPointerPosition();
+    if (!mousePosition?.x || !mousePosition?.y) return;
+    const newPoint = [mousePosition.x, mousePosition.y];
+    if (drawingGroupId.value) {
+      const groupConfig: FenceGroup | undefined = fenceGroups.value.find(
+        (x) => x.uid === drawingGroupId.value,
+      );
+      if (!groupConfig) {
+        console.error('drawingGroupId无效', drawingGroupId.value);
+        return;
+      }
+
+      /** 如果正在画线,那么替换最后一个点 */
+      const initialPoints = groupConfig.lineConfig.points as number[];
+      if (groupConfig._temp.points.length > 0) {
+        groupConfig.lineConfig.points = [...groupConfig._temp.points, ...newPoint];
+      } else {
+        groupConfig._temp.points = initialPoints;
+      }
+    }
+  };
+
+  const handleSelectGroup = (groupId: string) => {
+    currentGroupId.value = groupId;
+  };
+
+  /** 清空所有元素 */
+  const clear = () => {
+    fenceGroups.value = [];
+  };
+
+  /** 删除当前选中的group项 */
+  const remove = () => {
+    fenceGroups.value = fenceGroups.value.filter((x) => x.uid !== currentGroupId.value);
+  };
+
+  /** 导出为json格式 */
+  const toObject = () => {
+    const stage = stageRef.value.getStage();
+    const fenceGroups = stage?.find('.' + GROUP_NAME);
+    const gropuPoints = fenceGroups?.map((item) => {
+      const groupX = item.x();
+      const groupY = item.y();
+
+      const line = (item as Konva.Group).findOne((x: any) => x.className === 'Line') as Konva.Line;
+      const points = line?.points();
+      const newPoints: number[][] = [];
+      /** 存到后端的时候,只给点的坐标信息,不会给group的位置信息,所以要将点的坐标加上group的位移,才是之后点的最终坐标 */
+      for (let i = 0; i < points.length; i += 2) {
+        newPoints.push([Math.floor(points[i] + groupX), Math.floor(points[i + 1] + groupY)]);
+      }
+      return newPoints;
+    });
+    return gropuPoints;
+  };
+
+  /** 退出编辑模式 */
+  const exitEditMode = () => {
+    currentGroupId.value = '';
+    isEdit.value = false;
+  };
+  /** 进入编辑模式 */
+  const setEditMode = () => {
+    isEdit.value = true;
+  };
+
+  defineExpose({
+    clear,
+    remove,
+    toObject,
+    exitEditMode,
+    setEditMode,
+  });
+</script>
+
+<style scoped>
+  .scaleWrapper {
+    transform-origin: left top;
+  }
+  .overflowWrapper {
+    overflow: hidden;
+    border: 1px solid #ccc;
+  }
+</style>

+ 55 - 0
src/views/cameras/preview/components/FenceEditorV2/FenceItem.vue

@@ -0,0 +1,55 @@
+<!-- eslint-disable vue/no-use-v-if-with-v-for -->
+<template>
+  <v-group
+    v-for="group in props.fenceGroups"
+    :key="group.uid"
+    :groupId="group.uid"
+    :draggable="props.draggable && props.isEdit"
+    :name="group.name"
+    @mouse-down="handleGroupMouseDown"
+  >
+    <v-line :config="group.lineConfig" />
+    <v-circle
+      v-if="props.isEdit"
+      v-for="circleConfig in group.circleConfigs"
+      :config="circleConfig"
+      :key="circleConfig"
+      @mouse-down="handleCircleMouseDown"
+      @drag-move="handleCircleDragMove(circleConfig, $event)"
+    />
+  </v-group>
+</template>
+<script lang="ts" setup>
+  import { FenceCircleConfig, FenceGroup } from './types';
+
+  const props = defineProps<{
+    fenceGroups: FenceGroup[];
+    draggable: boolean;
+    isEdit: boolean;
+  }>();
+
+  const emits = defineEmits<{ (e: 'selectGroup', groupId: string): unknown }>();
+
+  const handleCircleDragMove = (circleConfig: FenceCircleConfig, e) => {
+    console.log('circle move', e);
+    console.log('circle move circleConfig', circleConfig);
+    const lineAttrs = e.target.parent.find('Line')[0].attrs;
+    const { x, y, idx } = e.target.attrs;
+    lineAttrs.points[idx * 2] = x;
+    lineAttrs.points[idx * 2 + 1] = y;
+    circleConfig.x = x;
+    circleConfig.y = y;
+  };
+
+  const handleCircleMouseDown = (e: { cancelBubble: boolean }) => {
+    /** 阻止冒泡 */
+    e.cancelBubble = true;
+  };
+
+  const handleGroupMouseDown = (e) => {
+    if (!props.isEdit) return;
+    e.target.parent.moveToTop();
+    emits('selectGroup', e.target.parent.attrs.groupId);
+  };
+</script>
+<style scoped></style>

+ 17 - 0
src/views/cameras/preview/components/FenceEditorV2/constants.ts

@@ -0,0 +1,17 @@
+export const defaultLineStyle = {
+  stroke: '#52FFDA',
+  strokeWidth: 3,
+  closed: true,
+  // fill: '#ff0000',
+};
+
+export const defaultCircleStyle = {
+  /** 圆的半径 */
+  radius: 4,
+  /** 点击区域 */
+  hitStrokeWidth: 10,
+  fill: '#52FFDA',
+  draggable: true,
+};
+
+export const GROUP_NAME = 'fenceGroup';

+ 17 - 0
src/views/cameras/preview/components/FenceEditorV2/types.ts

@@ -0,0 +1,17 @@
+import Konva from 'konva';
+
+export type FenceLineConfig = Konva.LineConfig;
+export interface FenceCircleConfig extends Konva.CircleConfig {
+  uid: string;
+}
+
+export interface FenceGroup {
+  lineConfig: FenceLineConfig;
+  /** 临时存放点坐标 */
+  _temp: {
+    points: number[];
+  };
+  circleConfigs: FenceCircleConfig[];
+  uid: string;
+  name: string;
+}

+ 41 - 0
src/views/cameras/preview/components/FenceEditorV2/utils.ts

@@ -0,0 +1,41 @@
+import { GROUP_NAME, defaultCircleStyle, defaultLineStyle } from './constants';
+import { uid } from 'uid';
+import { FenceGroup } from './types';
+
+export const getCircleConfig = (points: number[], scale: number) => {
+  const circlePoints = [];
+  for (let i = 0; i < points.length - 1; i += 2) {
+    circlePoints.push([points[i], points[i + 1]]);
+  }
+  return circlePoints.map((point, idx) => {
+    return createCircleConfigItem(point as [number, number], idx, scale);
+  });
+};
+
+export const createCircleConfigItem = (point: [number, number], idx: number, scale: number) => {
+  return {
+    ...defaultCircleStyle,
+    radius: defaultCircleStyle.radius / scale,
+    hitStrokeWidth: defaultCircleStyle.hitStrokeWidth / scale,
+    x: point[0],
+    y: point[1],
+    uid: uid(),
+    idx,
+  };
+};
+
+export const createGroupConfig = (points: number[], scale: number): FenceGroup => {
+  const lineConfig = {
+    ...defaultLineStyle,
+    strokeWidth: defaultLineStyle.strokeWidth / scale,
+    points: points,
+  };
+  const circleConfigs = getCircleConfig(points, scale);
+  return {
+    lineConfig,
+    name: GROUP_NAME,
+    circleConfigs,
+    uid: uid(),
+    _temp: { points: [] },
+  };
+};

+ 8 - 0
src/views/comp/form/basic.vue

@@ -92,8 +92,10 @@
       field: 'type',
       component: 'Select',
       label: '类型',
+      labelMessage: '选择类型会出现预约时间表单',
       componentProps: {
         placeholder: '请选择类型',
+        clearable: true,
         options: [
           {
             label: '舒适性',
@@ -133,6 +135,12 @@
           console.log(e);
         },
       },
+      // 根据 上面选择的类型,获取页面其他逻辑字段 处理显示表单
+      // 可用字段 schema, values, model, field
+      hidden: ({ model }) => {
+        return !model.type;
+      },
+      rules: [{ required: true, type: 'number', message: '请选择预约时间', trigger: ['change'] }],
     },
     {
       field: 'makeTime',

+ 0 - 1
src/views/list/basicList/index.vue

@@ -21,7 +21,6 @@
         :actionColumn="actionColumn"
         @checked-row-change="onCheckedRow"
         :scroll-x="1090"
-        :pagination="false"
       >
         <template #tableTitle>
           <el-button type="primary" @click="addTable">

+ 6 - 6
src/views/system/tenant/CreateDrawer.vue

@@ -69,13 +69,13 @@
       required: true,
       message: '请选择开始时间',
       trigger: 'change',
-      type: 'number',
+      type: 'date',
     },
     endDate: {
       required: true,
       message: '请选择结束时间',
       trigger: 'change',
-      type: 'number',
+      type: 'date',
     },
   };
 
@@ -130,8 +130,8 @@
         return message.error('请填写完整信息');
       }
       const params = cloneDeep(formParams.value);
-      params.beginDate = formatToDateTime(params.beginDate);
-      params.endDate = formatToDateTime(params.endDate);
+      params.beginDate = formatToDateTime(params.beginDate || '');
+      params.endDate = formatToDateTime(params.endDate || '');
       if (formParams.value.tenantId) {
         editTenant(params).then((_) => {
           message.success('编辑成功');
@@ -161,8 +161,8 @@
         tenantId: res.tenantId,
         tenantName: res.tenantName,
         tenantCode: res.tenantCode,
-        beginDate: new Date(res.beginDate).getTime(),
-        endDate: new Date(res.endDate).getTime(),
+        beginDate: new Date(res.beginDate).getTime().toString(),
+        endDate: new Date(res.endDate).getTime().toString(),
         tenantStatus: res.tenantStatus,
       };
       formParams.value = info;

+ 2 - 2
src/views/system/tenant/types/index.ts

@@ -2,7 +2,7 @@ export interface formParamsType {
   tenantId?: number | undefined;
   tenantName: string;
   tenantCode: string;
-  beginDate: number | undefined;
-  endDate: number | undefined;
+  beginDate: string | undefined;
+  endDate: string | undefined;
   tenantStatus: number | undefined;
 }

+ 5 - 1
tsconfig.json

@@ -43,6 +43,10 @@
     "src/**/*.ts",
     "src/**/*.d.ts",
     "src/**/*.tsx",
+    "src/**/*.svg",
+    "src/**/*.png",
+    "src/**/*.jpg",
+    "src/**/*.gif",
     "src/**/*.vue",
     "types/**/*.d.ts",
     "types/**/*.ts",
@@ -57,4 +61,4 @@
     "dist",
     "**/*.js"
   ]
-}
+}

+ 17 - 1
types/config.d.ts

@@ -63,7 +63,7 @@ export interface GlobEnvConfig {
   VITE_GLOB_API_URL: string;
   // 接口前缀
   VITE_GLOB_API_URL_PREFIX?: string;
-  // Project abbreviation
+  // 项目简称
   VITE_GLOB_APP_SHORT_NAME: string;
   // 图片上传地址
   VITE_GLOB_UPLOAD_URL?: string;
@@ -72,3 +72,19 @@ export interface GlobEnvConfig {
   //生产环境开启mock
   VITE_GLOB_PROD_MOCK: boolean;
 }
+export interface GlobConfig {
+  // 标题
+  title: string;
+  // 接口地址
+  apiUrl: string;
+  // 图片上传地址
+  uploadUrl?: string;
+  // api 接口前缀
+  urlPrefix?: string;
+  // 项目简称
+  shortName: string;
+  // 生产环境开启 mock
+  prodMock: boolean;
+  // 图片访问地址
+  imgUrl: string | undefined;
+}

+ 7 - 0
types/global.d.ts

@@ -22,6 +22,13 @@ declare global {
   //   __APP__: App<Element>;
   // }
 
+  // 此处 重新定义 ImportMeta 避免 ts 类型报错
+  // 目前框架只用到 env 和 glob
+  interface ImportMeta {
+    env: Record<string, string>;
+    glob: Record<function>;
+  }
+
   // vue
   declare type PropType<T> = VuePropType<T>;
   declare type VueNode = VNodeChild | JSX.Element;

+ 1 - 0
vite.config.ts

@@ -55,6 +55,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
       __APP_INFO__: JSON.stringify(__APP_INFO__),
     },
     css: {
+      devSourcemap: true,
       preprocessorOptions: {
         scss: {
           modifyVars: {},