Explorar el Código

fix: 制作需求

bill hace 1 año
padre
commit
f74562b50b

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "element-plus": "^2.6.3",
     "gl-matrix": "^3.4.3",
     "js-base64": "^3.7.7",
+    "jszip": "^3.10.1",
     "mitt": "^3.0.1",
     "ol": "^9.1.0",
     "proj4": "^2.11.0",

+ 71 - 0
pnpm-lock.yaml

@@ -8,6 +8,7 @@ specifiers:
   element-plus: ^2.6.3
   gl-matrix: ^3.4.3
   js-base64: ^3.7.7
+  jszip: ^3.10.1
   mitt: ^3.0.1
   ol: ^9.1.0
   proj4: ^2.11.0
@@ -25,6 +26,7 @@ dependencies:
   element-plus: 2.6.3_vue@3.4.21
   gl-matrix: 3.4.3
   js-base64: 3.7.7
+  jszip: 3.10.1
   mitt: 3.0.1
   ol: 9.1.0
   proj4: 2.11.0
@@ -701,6 +703,10 @@ packages:
     resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==}
     dev: true
 
+  /core-util-is/1.0.3:
+    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+    dev: false
+
   /crc-32/1.2.2:
     resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
     engines: {node: '>=0.8'}
@@ -843,10 +849,18 @@ packages:
     resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
     dev: false
 
+  /immediate/3.0.6:
+    resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+    dev: false
+
   /immutable/4.3.5:
     resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==}
     dev: true
 
+  /inherits/2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+    dev: false
+
   /is-binary-path/2.1.0:
     resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
     engines: {node: '>=8'}
@@ -871,14 +885,33 @@ packages:
     engines: {node: '>=0.12.0'}
     dev: true
 
+  /isarray/1.0.0:
+    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+    dev: false
+
   /js-base64/3.7.7:
     resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
     dev: false
 
+  /jszip/3.10.1:
+    resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
+    dependencies:
+      lie: 3.3.0
+      pako: 1.0.11
+      readable-stream: 2.3.8
+      setimmediate: 1.0.5
+    dev: false
+
   /lerc/3.0.0:
     resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==}
     dev: false
 
+  /lie/3.3.0:
+    resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+    dependencies:
+      immediate: 3.0.6
+    dev: false
+
   /lodash-es/4.17.21:
     resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
     dev: false
@@ -960,6 +993,10 @@ packages:
       rbush: 3.0.1
     dev: false
 
+  /pako/1.0.11:
+    resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
+    dev: false
+
   /pako/2.1.0:
     resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
     dev: false
@@ -996,6 +1033,10 @@ packages:
       picocolors: 1.0.0
       source-map-js: 1.2.0
 
+  /process-nextick-args/2.0.1:
+    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+    dev: false
+
   /proj4/2.11.0:
     resolution: {integrity: sha512-SasuTkAx8HnWQHfIyhkdUNJorSJqINHAN3EyMWYiQRVorftz9DHz650YraFgczwgtHOxqnfuDxSNv3C8MUnHeg==}
     dependencies:
@@ -1022,6 +1063,18 @@ packages:
       quickselect: 2.0.0
     dev: false
 
+  /readable-stream/2.3.8:
+    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+    dependencies:
+      core-util-is: 1.0.3
+      inherits: 2.0.4
+      isarray: 1.0.0
+      process-nextick-args: 2.0.1
+      safe-buffer: 5.1.2
+      string_decoder: 1.1.1
+      util-deprecate: 1.0.2
+    dev: false
+
   /readdirp/3.6.0:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
@@ -1060,6 +1113,10 @@ packages:
       fsevents: 2.3.3
     dev: true
 
+  /safe-buffer/5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+    dev: false
+
   /sass/1.72.0:
     resolution: {integrity: sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==}
     engines: {node: '>=14.0.0'}
@@ -1078,6 +1135,10 @@ packages:
       lru-cache: 6.0.0
     dev: true
 
+  /setimmediate/1.0.5:
+    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
+    dev: false
+
   /source-map-js/1.2.0:
     resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
     engines: {node: '>=0.10.0'}
@@ -1089,6 +1150,12 @@ packages:
       frac: 1.1.2
     dev: false
 
+  /string_decoder/1.1.1:
+    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+    dependencies:
+      safe-buffer: 5.1.2
+    dev: false
+
   /to-fast-properties/2.0.0:
     resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
     engines: {node: '>=4'}
@@ -1108,6 +1175,10 @@ packages:
   /undici-types/5.26.5:
     resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
 
+  /util-deprecate/1.0.2:
+    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+    dev: false
+
   /vite/5.2.7_2viu4ejjkxxzbuz7bxvuj6jcbe:
     resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==}
     engines: {node: ^18.0.0 || >=20.0.0}

BIN
public/template.xls


BIN
public/templaten.xls


+ 5 - 3
src/App.vue

@@ -1,16 +1,18 @@
 <template>
-  <el-config-provider :locale="zhCn" :message="{ max: 3 }">
+  <Locale>
     <RouterView v-slot="{ Component }">
       <KeepAlive>
         <component :is="Component" />
       </KeepAlive>
     </RouterView>
-  </el-config-provider>
+
+    <div id="dialog"></div>
+  </Locale>
 </template>
 
 <script lang="ts" setup>
+import Locale from "@/config/locale.vue";
 import { ElLoading } from "element-plus";
-import zhCn from "element-plus/es/locale/lang/zh-cn";
 import { lifeHook } from "./request/state";
 
 let loading: ReturnType<typeof ElLoading.service> | null = null;

+ 68 - 0
src/components/dialog/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <teleport to="#dialog">
+    <div class="dialog-bg" v-if="show">
+      <div class="dialog" :style="{ width: width + 'px' }">
+        <div class="head">
+          <h3>{{ title }}</h3>
+          <el-icon @click="closeHandle" v-if="showClose || cornerClose">
+            <Close />
+          </el-icon>
+        </div>
+        <div class="content">
+          <slot />
+        </div>
+        <div class="floot" v-if="showFloor">
+          <el-button type="danger" v-if="showDel" @click="deleteHandle">删 除</el-button>
+          <el-button @click="closeHandle" v-if="showClose">取 消</el-button>
+          <el-button type="primary" @click="enterHandle">
+            {{ enterText }}
+          </el-button>
+        </div>
+      </div>
+    </div>
+  </teleport>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, nextTick } from "vue";
+import { DialogProps } from "./type.ts";
+import { Close } from "@element-plus/icons-vue";
+
+const props = withDefaults(defineProps<DialogProps>(), {
+  width: 680,
+  show: false,
+  hideFloor: false,
+  showClose: true,
+  notSubmit: false,
+  enterText: "保 存",
+});
+
+const emit = defineEmits<{
+  (e: "update:show", show: boolean): void;
+  (e: "quit"): void;
+  (e: "submit"): void;
+  (e: "delete"): void;
+}>();
+
+const showDel = ref(props.showDelete);
+const showFloor = computed(() => !props.hideFloor);
+
+const closeHandle = () => {
+  emit("update:show", false);
+  emit("quit");
+};
+const enterHandle = () => {
+  emit("submit");
+};
+const deleteHandle = () => {
+  emit("delete");
+};
+
+if (!showFloor.value && !props.notSubmit) {
+  nextTick(() => enterHandle());
+}
+</script>
+
+<style lang="scss" scoped>
+@import "./style.scss";
+</style>

+ 53 - 0
src/components/dialog/style.scss

@@ -0,0 +1,53 @@
+.dialog-bg {
+  background-color: rgba(0, 0, 0, 0.5);
+  position        : fixed;
+  left            : 0;
+  top             : 0;
+  right           : 0;
+  bottom          : 0;
+  display         : flex;
+  align-items     : center;
+  justify-content : center;
+  z-index         : 99;
+
+}
+
+.dialog {
+  $padding        : 16px;
+  width           : 680px;
+  background-color: #fff;
+  border-radius   : 4px;
+
+  .head {
+    padding        : $padding;
+    display        : flex;
+    justify-content: space-between;
+    align-items    : center;
+
+    h3 {
+      font-size  : 16px;
+      color      : var(--colorColor);
+      font-weight: normal;
+      margin     : 0;
+    }
+
+    i {
+      font-size: 1.5rem;
+      color    : rgb(145, 148, 154);
+      cursor   : pointer;
+    }
+  }
+
+  .content {
+    padding      : 24px 30px;
+    border-top   : 1px solid var(--bgColor);
+    border-bottom: 1px solid var(--bgColor);
+    max-height   : 600px;
+    overflow-y   : auto;
+  }
+
+  .floot {
+    float  : right;
+    padding: $padding;
+  }
+}

+ 25 - 0
src/components/dialog/type.ts

@@ -0,0 +1,25 @@
+export type DialogProps = {
+  show?: boolean;
+  title: string;
+  hideFloor?: boolean;
+  notSubmit?: boolean,
+  enterText?: string;
+  width?: number;
+  power?: string;
+  showClose?: boolean;
+  showDelete?: boolean;
+  cornerClose?: boolean;
+};
+
+export const dialogPropsKeys: (keyof DialogProps)[] = [
+  "show",
+  "title",
+  "hideFloor",
+  "notSubmit",
+  "enterText",
+  "width",
+  "power",
+  "showClose",
+  "showDelete",
+  "cornerClose",
+];

+ 9 - 0
src/config/locale.vue

@@ -0,0 +1,9 @@
+<template>
+  <el-config-provider :locale="zhCn" :message="{ max: 3 }">
+    <slot />
+  </el-config-provider>
+</template>
+
+<script lang="ts" setup>
+import zhCn from "element-plus/es/locale/lang/zh-cn";
+</script>

+ 20 - 0
src/helper/loading.ts

@@ -0,0 +1,20 @@
+import { ElLoading } from "element-plus";
+
+let loading: ReturnType<typeof ElLoading.service> | null;
+
+export const openLoading = (url?: string) => {
+  if (loading) return;
+
+  loading = ElLoading.service({
+    lock: true,
+    text: "加载中",
+    background: "rgba(255, 255, 255, 0.4)",
+  });
+};
+
+export const closeLoading = () => {
+  if (loading) {
+    loading.close();
+    loading = null;
+  }
+};

+ 15 - 0
src/helper/message.ts

@@ -0,0 +1,15 @@
+import { InfoFilled, SuccessFilled } from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { markRaw } from "vue";
+
+export const confirm = (msg: string, okText = "确定") =>
+  ElMessageBox.confirm(msg, "系统提示", {
+    type: "warning",
+    confirmButtonText: okText,
+    // icon: markRaw(InfoFilled),
+  });
+export const alert = (msg: string) =>
+  ElMessageBox.alert(msg, "系统提示", {
+    type: "warning",
+    // icon: markRaw(SuccessFilled),
+  });

+ 156 - 0
src/helper/mount.ts

@@ -0,0 +1,156 @@
+import { router } from "@/router";
+import { createVNode, reactive, render, watch, watchEffect } from "vue";
+import Locale from "@/config/locale.vue";
+import type { App, Ref, VNode } from "vue";
+
+interface ComponentConstructor<P = any> {
+  new (...args: any[]): {
+    $props: P;
+  };
+}
+type Mutable<Type> = {
+  -readonly [Key in keyof Type]: Type[Key];
+};
+
+export type MountContext<P = any> = {
+  props?: P;
+  children?: unknown;
+  element?: HTMLElement;
+  app?: App;
+};
+function mount<P>(
+  component: Readonly<P>,
+  { props, children, element, app }: MountContext<P> = {}
+) {
+  let el = element;
+  let vNode: VNode | undefined = createVNode(
+    Locale,
+    {},
+    {
+      default: () => createVNode(component, props as any, children),
+    }
+  );
+
+  if (app && app._context) vNode.appContext = app._context;
+  if (el) {
+    render(vNode, el);
+  } else if (typeof document !== "undefined") {
+    render(vNode, (el = document.createElement("div")));
+  }
+
+  const destroy = () => {
+    if (el) render(null, el);
+    el = undefined;
+    vNode = undefined;
+  };
+
+  return { vNode, destroy, el };
+}
+
+let app: App;
+export const setApp = (application: App) => (app = application);
+
+export const extendProps = <T extends {}, E extends {}>(
+  origin: T,
+  append: E
+): T & E => {
+  const props = reactive({ ...append }) as T & E;
+  watchEffect(() => {
+    for (const key in origin) {
+      (props as any)[key] = origin[key];
+    }
+  });
+  return props;
+};
+
+export const mountComponent = <P>(
+  comp: ComponentConstructor<P>,
+  props: Mutable<P>,
+  children?: any
+) => {
+  const element = document.createElement("div");
+  const { destroy: destroyRaw } = mount(comp, {
+    element,
+    props,
+    app: app,
+    children,
+  } as any);
+  const destroy = () => {
+    destroyRaw();
+    if (document.body.contains(element)) {
+      document.body.removeChild(element);
+    }
+    stopWatch();
+  };
+
+  const stopWatch = watch(() => router.currentRoute.value.name, destroy);
+
+  return destroy;
+};
+
+import Dialog from "@/components/dialog/index.vue";
+import { DialogProps, dialogPropsKeys } from "@/components/dialog/type";
+
+export type QuiskExpose = {
+  submit?: () => void;
+  quit?: () => void;
+} & Partial<{ [key in keyof DialogProps]?: Ref<DialogProps[key]> }>;
+
+export const quiskMountFactory =
+  <P>(comp: ComponentConstructor<P>, dprops: DialogProps) =>
+  <T = boolean>(
+    props: Mutable<P>,
+    dRef?: (expose: { quit: () => void; submit: () => void }) => void
+  ): Promise<T> => {
+    let ref: QuiskExpose;
+
+    return new Promise((resolve) => {
+      const api = {
+        onQuit: async () => {
+          const ret = ref.quit && ((await ref.quit()) as any);
+          if (ret) {
+            resolve(ret);
+          } else {
+            resolve(false as any);
+          }
+          destroy();
+        },
+        onSubmit: async () => {
+          const ret = ref.submit && ((await ref.submit()) as any);
+          if (ret) {
+            resolve(ret);
+          } else {
+            resolve(true as any);
+          }
+          destroy();
+        },
+      };
+
+      const layoutProps = reactive({
+        ...dprops,
+        ref: undefined,
+        show: true,
+        ...api,
+      });
+      const destroy = mountComponent(Dialog, layoutProps, {
+        default: () =>
+          createVNode(comp, {
+            ...props,
+            ref: (v: any) => {
+              for (const key in v) {
+                if (dialogPropsKeys.includes(key as any)) {
+                  (layoutProps as any)[key] = v[key];
+                }
+              }
+              ref = v;
+            },
+          }),
+      });
+
+      dRef &&
+        dRef({
+          submit: () => api.onSubmit(),
+          quit: () => api.onQuit(),
+        });
+    });
+  };

+ 3 - 0
src/main.ts

@@ -4,8 +4,11 @@ import "element-plus/dist/index.css";
 import { router } from "./router";
 import App from "./App.vue";
 import "./style.scss";
+import { setApp } from "./helper/mount";
 
 const app = createApp(App);
 app.use(ElementPlus);
 app.use(router);
 app.mount("#app");
+
+setApp(app);

+ 8 - 0
src/request/URL.ts

@@ -6,8 +6,16 @@ export const updateRelicsName = `/relics/relicsInfo/update`;
 export const getRelicsInfo = `/relics/relicsInfo/info/:relicsId`;
 export const delRelics = `/relics/relicsInfo/del`;
 export const addRelics = `/relics/relicsInfo/add`;
+
+export const getScenesPage = `/relics/scene/page`;
+export const delScene = `/relics/scene/del/:sceneId`;
+
 export const getRelicsScenes = `/relics/relics-scene/getAllList/:relicsId`;
 export const addRelicsScene = `/relics/relics-scene/add/:relicsId`;
 export const delRelicsScene = `/relics/relics-scene/del`;
 export const updateRelicsScenePosName = `/relics/relics-scene/editPosName`;
 export const getRelicsScenePosInfo = `/relics/relics-scene-pos/get/:posId`;
+
+export const getDevicePage = `/relics/camera/page`;
+export const delDevice = `/relics/camera/del/:deviceId`;
+export const addDevice = `/relics/camera/add`;

+ 80 - 15
src/request/index.ts

@@ -1,4 +1,4 @@
-import { gendUrl } from "@/util";
+import { dateFormat, gendUrl } from "@/util";
 import * as URL from "./URL";
 import {
   basePath,
@@ -10,13 +10,7 @@ import {
   Param,
 } from "./state";
 import { ElMessage } from "element-plus";
-import {
-  Relics,
-  RelicsScene,
-  RelicsScenePoint,
-  ResPage,
-  UserInfo,
-} from "./type";
+import { Relics, Scene, ScenePoint, ResPage, UserInfo, Device } from "./type";
 
 type Other = { params?: Param; paths?: Param };
 export const sendFetch = <T>(
@@ -83,11 +77,16 @@ export const loginFetch = (props: LoginProps) =>
 export const userInfoFetch = () =>
   sendFetch<UserInfo>(URL.getUserInfo, { method: "post" });
 
-export type RelicsPageProps = {
-  name: string;
+export type PageProps<T> = {
   pageNum: number;
   pageSize: number;
-};
+} & T;
+
+export type RelicsPageProps = PageProps<{
+  name?: string;
+  category?: number;
+  level?: number;
+}>;
 export const relicsPageFetch = (body: RelicsPageProps) =>
   sendFetch<ResPage<Relics>>(URL.getRelicsPage, {
     method: "post",
@@ -104,8 +103,12 @@ export const relicsInfoFetch = (relicsId: number) =>
     { paths: { relicsId } }
   );
 
-export const addRelicsFetch = (name: string) =>
-  sendFetch(URL.addRelics, { method: "post", body: JSON.stringify({ name }) });
+export const addRelicsFetch = (relics: Relics) => {
+  return sendFetch(URL.addRelics, {
+    method: "post",
+    body: JSON.stringify({ ...relics, relicsId: null }),
+  });
+};
 
 export const delRelicsFetch = (relicsId: number) =>
   sendFetch(URL.delRelics, {
@@ -120,7 +123,7 @@ export const updateRelicsFetch = (relics: Relics) =>
   });
 
 export const relicsScenesFetch = (relicsId: number) =>
-  sendFetch<RelicsScene[]>(
+  sendFetch<Scene[]>(
     URL.getRelicsScenes,
     { method: "post", body: JSON.stringify({}) },
     { paths: { relicsId } }
@@ -145,7 +148,7 @@ export const updateRelicsScenePosNameFetch = (posId: number, name: string) =>
   });
 
 export const relicsScenePosInfoFetch = (posId: number) =>
-  sendFetch<RelicsScenePoint>(
+  sendFetch<ScenePoint>(
     URL.getRelicsScenePosInfo,
     {
       method: "post",
@@ -153,3 +156,65 @@ export const relicsScenePosInfoFetch = (posId: number) =>
     },
     { paths: { posId } }
   );
+
+export type ScenePageProps = PageProps<{
+  sceneCode?: string;
+  sceneName?: string;
+  title?: string;
+  cameraType?: number;
+  algorithmTime?: [Date, Date];
+  endTime?: string;
+  sceneSource?: number;
+  snCode?: string;
+  startTime?: string;
+  userName?: string;
+}>;
+export const scenePageFetch = (props: ScenePageProps) =>
+  sendFetch<ResPage<Scene>>(URL.getScenesPage, {
+    method: "post",
+    body: JSON.stringify({
+      ...props,
+      algorithmTime: null,
+      startTime:
+        props.algorithmTime &&
+        dateFormat(props.algorithmTime[0], "yyyy-MM-dd 00:00:00"),
+      endTime:
+        props.algorithmTime &&
+        dateFormat(props.algorithmTime[1], "yyyy-MM-dd 23:59:59"),
+    }),
+  });
+
+export const delSceneFetch = (sceneId: number) =>
+  sendFetch(
+    URL.delScene,
+    {
+      method: "post",
+      body: JSON.stringify({}),
+    },
+    { paths: { sceneId } }
+  );
+
+export type DevicePageProps = PageProps<{
+  cameraSn?: string;
+  cameraType?: number;
+}>;
+export const devicePageFetch = (props: DevicePageProps) =>
+  sendFetch<ResPage<Device>>(URL.getDevicePage, {
+    method: "post",
+    body: JSON.stringify(props),
+  });
+
+export const delDeviceFetch = (deviceId: number) =>
+  sendFetch(
+    URL.delDevice,
+    {
+      method: "post",
+      body: JSON.stringify({}),
+    },
+    { paths: { deviceId } }
+  );
+export const addDeviceFetch = (sn: string) =>
+  sendFetch(URL.addDevice, {
+    method: "post",
+    body: JSON.stringify({ cameraSn: sn }),
+  });

+ 42 - 3
src/request/type.ts

@@ -1,3 +1,10 @@
+import { DeviceType } from "@/store/device";
+import {
+  relicsLevelDesc,
+  relicsTypeDesc,
+  creationMethodDesc,
+} from "@/store/relics";
+
 export type UserInfo = {
   head: string;
   nickName: string;
@@ -5,6 +12,15 @@ export type UserInfo = {
 };
 
 export type Relics = {
+  address?: string;
+  creationMethod?: keyof typeof creationMethodDesc;
+  category?: keyof typeof relicsTypeDesc;
+  createBy?: string;
+  createTime?: string;
+  level?: keyof typeof relicsLevelDesc;
+  tbStatus?: 0;
+  unicode?: string;
+  updateBy?: null;
   relicsId: number;
   name: string;
 };
@@ -14,7 +30,7 @@ export type ResPage<T> = {
   records: T[];
 };
 
-export type RelicsScenePoint = {
+export type ScenePoint = {
   tbStatus: number;
   createTime: string;
   updateTime: string;
@@ -25,10 +41,33 @@ export type RelicsScenePoint = {
   sceneCode: string;
 };
 
-export type RelicsScene = {
+export type Scene = {
   id: number;
+  sceneId: number;
   sceneCode: string;
   sceneName: string;
   title: string;
-  scenePos: RelicsScenePoint[];
+  cameraType: number;
+  scenePos: ScenePoint[];
+  algorithmTime: string;
+  endTime: string;
+  sceneSource: 0;
+  snCode: string;
+  startTime: string;
+  userName: string;
+};
+
+export type Device = {
+  cameraId: number;
+  cameraSn: string;
+  cameraType: DeviceType;
+  createBy: null;
+  createTime: string;
+  deptId: number;
+  fdCameraId: number;
+  tbStatus: number;
+  updateBy: string;
+  updateTime: string;
+  userId: 2;
+  userName: string;
 };

+ 35 - 0
src/router.ts

@@ -4,6 +4,11 @@ import { gHeaders } from "./request/state";
 const history = createWebHashHistory();
 const routes: RouteRecordRaw[] = [
   {
+    path: "",
+    name: "down-vision",
+    component: () => import("@/view/down-vision.vue"),
+  },
+  {
     path: "/login",
     name: "login",
     meta: { title: "登录" },
@@ -37,6 +42,18 @@ const routes: RouteRecordRaw[] = [
           },
         ],
       },
+      {
+        path: "scene",
+        name: "scene",
+        meta: { title: "场景管理" },
+        component: () => import("@/view/scene.vue"),
+      },
+      {
+        path: "device",
+        name: "device",
+        meta: { title: "设备管理" },
+        component: () => import("@/view/device.vue"),
+      },
     ],
   },
   {
@@ -65,6 +82,24 @@ const routes: RouteRecordRaw[] = [
   },
 ];
 
+export const findRoute = (
+  routeName: string,
+  fullPath = false,
+  routeAll = routes
+): RouteRecordRaw | null => {
+  for (const route of routeAll) {
+    if (route.name === routeName) {
+      return route;
+    } else if (route.children) {
+      const childRoute = findRoute(routeName, fullPath, route.children);
+      if (childRoute) {
+        return fullPath ? { ...route, children: [childRoute] } : childRoute;
+      }
+    }
+  }
+  return null;
+};
+
 export const router = createRouter({ history, routes });
 export const setDocTitle = (title: string) => {
   document.title = title + "-不移动动文物管理平台";

+ 10 - 0
src/store/device.ts

@@ -0,0 +1,10 @@
+export type { Device } from "@/request/type";
+export enum DeviceType {
+  VR = 9,
+  CLUNT = 11,
+}
+
+export const DeviceTypeDesc: { [key in DeviceType]: string } = {
+  [DeviceType.CLUNT]: "文保1号",
+  [DeviceType.VR]: "文保2号",
+};

+ 38 - 0
src/store/relics.ts

@@ -0,0 +1,38 @@
+import { relicsInfoFetch, updateRelicsFetch } from "@/request";
+import { ref } from "vue";
+import { Relics } from "@/request/type";
+import { refreshScenes } from "./scene";
+
+export type { Relics } from "@/request/type";
+export const relics = ref<Relics>();
+
+export const initRelics = async (relicsId: number) => {
+  relics.value = await relicsInfoFetch(relicsId);
+  refreshScenes();
+};
+export const updateRelicsName = async (name: string) => {
+  await updateRelicsFetch({ ...relics.value!, name });
+  relics.value!.name = name;
+};
+
+export const relicsLevelDesc = {
+  0: "全国重点文物保护单位",
+  1: "省级文物保护单位",
+  2: "市级和县级文物保护单位",
+  3: "尚未核定公布为文物保护单位的不可移动文物",
+  4: "未认定",
+};
+
+export const relicsTypeDesc = {
+  0: "古文化遗址",
+  1: "古墓葬",
+  2: "古建筑",
+  3: "石窟寺及石刻",
+  4: "近现代重要史迹及代表性建筑",
+  5: "其它",
+};
+
+export const creationMethodDesc = {
+  1: "手动",
+  2: "自动",
+};

+ 56 - 39
src/store/scene.ts

@@ -1,72 +1,64 @@
 import {
   addRelicsSceneFetch,
   delRelicsSceneFetch,
-  relicsInfoFetch,
   relicsScenesFetch,
-  updateRelicsFetch,
   updateRelicsScenePosNameFetch,
 } from "@/request";
 import { computed, ref } from "vue";
-import { Relics, RelicsScene, RelicsScenePoint } from "@/request/type";
+import { Scene, ScenePoint } from "@/request/type";
 import { gHeaders } from "@/request/state";
+import { relics } from "./relics";
+import { DeviceType as SceneType } from "./device";
 
-export type { RelicsScene, RelicsScenePoint };
-
-export const relics = ref<Relics>();
-export const scenes = ref<RelicsScene[]>([]);
+export type { Scene, ScenePoint };
 
+export const scenes = ref<Scene[]>([]);
 export const scenePoints = computed(() =>
   scenes.value.reduce((t, scene) => {
     t.push(...scene.scenePos);
     return t;
-  }, [] as RelicsScenePoint[])
+  }, [] as ScenePoint[])
 );
+export const relicsId = computed(() => relics.value!.relicsId);
 
-const fileNames = new Array(6).fill(0);
-export const getPointPano = (sceneCode: string, pid: number) =>
-  fileNames.map(
-    (_, i) =>
-      `https://4dkk.4dage.com/scene_view_data/${sceneCode}/images/tiles/4k/${pid}_skybox${i}.jpg`
-  );
+// https://4dkankan.oss-cn-shenzhen.aliyuncs.com/scene_view_data/KJ-t-OgSx9XIrvNQ/images/panoramas/22.jpg?x-oss-process=image/resize,m_fixed,w_6144&171342528615
+// const fileNames = new Array(6).fill(0);
+// export const getPointPano = (sceneCode: string, pid: number) =>
+//   fileNames.map(
+//     (_, i) =>
+//       `https://4dkk.4dage.com/scene_view_data/${sceneCode}/images/tiles/4k/${pid}_skybox${i}.jpg`
+//   );
 
-const refreshScenes = async (relicsId: number) => {
-  scenes.value = await relicsScenesFetch(relicsId);
+export const getPointPano = (sceneCode: string, pid: number) =>
+  `https://4dkankan.oss-cn-shenzhen.aliyuncs.com/scene_view_data/${sceneCode}/images/panoramas/${pid}.jpg`;
+// `https://4dkk.4dage.com/scene_view_data/${sceneCode}/images/pan/high/${pid}.jpg`;
+export const refreshScenes = async () => {
+  scenes.value = await relicsScenesFetch(relicsId.value);
 };
 
-export const initRelics = async (relicsId: number) => {
-  const data = await Promise.all([
-    relicsInfoFetch(relicsId),
-    refreshScenes(relicsId),
-  ]);
-  [relics.value] = data;
+export const addRelicsScene = async (sceneCode: string) => {
+  await addRelicsSceneFetch(relicsId.value, sceneCode);
+  await refreshScenes();
 };
-export const updateRelicsName = async (name: string) => {
-  await updateRelicsFetch({ ...relics.value!, name });
-  relics.value!.name = name;
+export const delRelicsScene = async (scene: Scene) => {
+  await delRelicsSceneFetch(relicsId.value, scene.id);
+  await refreshScenes();
 };
 
-export const addScene = async (sceneCode: string) => {
-  await addRelicsSceneFetch(relics.value!.relicsId, sceneCode);
-  await refreshScenes(relics.value!.relicsId);
-};
-export const delScene = async (scene: RelicsScene) => {
-  await delRelicsSceneFetch(relics.value!.relicsId, scene.id);
-  await refreshScenes(relics.value!.relicsId);
-};
 export const updateScenePointName = async (
-  point: RelicsScenePoint,
+  point: ScenePoint,
   newName: string
 ) => {
   await updateRelicsScenePosNameFetch(point.id, newName);
-  if (relics.value) {
-    await refreshScenes(relics.value.relicsId);
-  }
+  relicsId.value && (await refreshScenes());
 };
 
-export const gotoScene = (scene: RelicsScene) => {
+export const gotoScene = (scene: Scene, edit = false) => {
   const params = new URLSearchParams();
   params.set("m", scene.sceneCode);
-  params.set("token", gHeaders.token);
+  if (edit) {
+    params.set("token", gHeaders.token);
+  }
   params.set("lang", "zh");
   if (scene.sceneCode.startsWith("KJ")) {
     window.open(`https://test.4dkankan.com/spg.html?` + params.toString());
@@ -74,3 +66,28 @@ export const gotoScene = (scene: RelicsScene) => {
     window.open(`https://uat-laser.4dkankan.com/uat/?` + params.toString());
   }
 };
+
+// 普通场景状态
+export enum SceneStatus {
+  ERR = -1,
+  RUN = 0,
+  SUCCESS = 1,
+  // DEL = 2,
+  // ARCHIVE = 3,
+  // RERUN = 4,
+}
+
+export { SceneType };
+export const SceneTypeDesc: { [key in SceneType]: string } = {
+  [SceneType.VR]: "全景VR",
+  [SceneType.CLUNT]: "点云场景",
+};
+
+export const SceneStatusDesc: { [key in SceneStatus]: string } = {
+  // [SceneStatus.DEL]: "场景被删",
+  // [SceneStatus.ARCHIVE]: "封存",
+  // [SceneStatus.RERUN]: "重新计算中",
+  [SceneStatus.RUN]: "计算中",
+  [SceneStatus.ERR]: "计算失败",
+  [SceneStatus.SUCCESS]: "计算成功",
+};

+ 27 - 0
src/util/index.ts

@@ -226,3 +226,30 @@ export const gendUrl = (tempUrl: string, params: { [key: string]: any }) => {
   url += tempUrl.substr(preIndex);
   return url;
 };
+
+export const dateFormat = (date: Date, fmt: string) => {
+  var o: any = {
+    "M+": date.getMonth() + 1, //月份
+    "d+": date.getDate(), //日
+    "h+": date.getHours(), //小时
+    "m+": date.getMinutes(), //分
+    "s+": date.getSeconds(), //秒
+    "q+": Math.floor((date.getMonth() + 3) / 3), //季度
+    S: date.getMilliseconds(), //毫秒
+  };
+  if (/(y+)/.test(fmt)) {
+    fmt = fmt.replace(
+      RegExp.$1,
+      (date.getFullYear() + "").substr(4 - RegExp.$1.length)
+    );
+  }
+  for (var k in o) {
+    if (new RegExp("(" + k + ")").test(fmt)) {
+      fmt = fmt.replace(
+        RegExp.$1,
+        RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
+      );
+    }
+  }
+  return fmt;
+};

+ 4 - 1
src/util/pc4xlsl.ts

@@ -51,10 +51,13 @@ export const downloadPointsXLSL = async (
       ];
     }),
   ];
+  console.log(tabsArray);
   const names = [name, name + "本体边界坐标"];
 
-  return Promise.all([
+  await Promise.all([
     genXLSLByTemp(temps[0], tabsArray[0], names[0]),
     genXLSLByTemp(temps[1], tabsArray[1], names[1]),
   ]);
+
+  console.log(tabsArray);
 };

+ 28 - 0
src/view/device-edit.vue

@@ -0,0 +1,28 @@
+<template>
+  <el-form label-width="100px">
+    <el-form-item label="sn:" required>
+      <el-input v-model="data.sn" style="width: 250px" :maxlength="500" />
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { QuiskExpose } from "@/helper/mount";
+import { ElMessage } from "element-plus";
+import { ref } from "vue";
+
+const props = defineProps<{
+  submit: (sn: string) => Promise<any>;
+}>();
+const data = ref({ sn: "" });
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!data.value.sn) {
+      ElMessage.error("请输入SN!");
+      throw "请输入SN!";
+    }
+    await props.submit(data.value.sn);
+  },
+});
+</script>

+ 137 - 0
src/view/device.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="relics-layout">
+    <div class="relics-header">
+      <div class="search">
+        <el-form label-width="100px" inline>
+          <el-form-item label="sn码:">
+            <el-input v-model="pageProps.cameraSn" style="width: 250px" />
+          </el-form-item>
+          <el-form-item label="设备类型:">
+            <el-select style="width: 250px" v-model="pageProps.cameraType">
+              <el-option
+                :value="Number(key)"
+                :label="type"
+                v-for="(type, key) in DeviceTypeDesc"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button type="primary" @click="refresh">查询</el-button>
+            <el-button type="primary" plain @click="pageProps = initProps">
+              重置
+            </el-button>
+            <el-button type="primary" @click="addHandler"> 添加设备 </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+
+    <el-table :data="relicsArray" border>
+      <el-table-column label="SN码" prop="cameraSn"></el-table-column>
+
+      <el-table-column
+        label="设备类型"
+        prop="snCode"
+        v-slot:default="{ row }: { row: Device }"
+      >
+        {{ DeviceTypeDesc[row.cameraType] }}
+      </el-table-column>
+      <!-- <el-table-column label="所属单位" prop="deptId"></el-table-column> -->
+      <el-table-column label="绑定账号" prop="userName"> </el-table-column>
+      <el-table-column label="创建时间" prop="createTime" v-slot:default="{ row }">
+        {{ row.createTime && row.createTime.substr(0, 16) }}
+      </el-table-column>
+
+      <el-table-column label="操作" width="100px" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="danger" @click="delHandler(row.cameraId)" size="small">
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="pag-layout">
+      <el-pagination
+        background
+        layout="prev, pager, next"
+        :total="total"
+        @current-change="(data: number) => pageProps.pageNum = data"
+        :current-page="pageProps.pageNum"
+        :page-size="pageProps.pageSize"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onActivated, ref, watchEffect } from "vue";
+import {
+  devicePageFetch,
+  DevicePageProps,
+  delDeviceFetch,
+  addDeviceFetch,
+} from "@/request";
+import { Device } from "@/request/type";
+import { DeviceTypeDesc } from "@/store/device";
+import { ElMessageBox } from "element-plus";
+import { deviceEdit } from "./quisk";
+
+const initProps: DevicePageProps = {
+  pageNum: 1,
+  pageSize: 12,
+};
+const pageProps = ref({ ...initProps });
+const total = ref<number>(0);
+const relicsArray = ref<Device[]>([]);
+
+const refresh = async () => {
+  const data = await devicePageFetch(pageProps.value);
+  total.value = data.total;
+  relicsArray.value = data.records;
+};
+
+const delHandler = async (deviceId: number) => {
+  const ok = await ElMessageBox.confirm("确定要删除吗", {
+    type: "warning",
+  });
+  if (ok) {
+    await delDeviceFetch(deviceId);
+    await refresh();
+  }
+};
+
+const addHandler = async () => {
+  await deviceEdit({ submit: addDeviceFetch });
+  await refresh();
+};
+
+watchEffect(refresh);
+onActivated(refresh);
+</script>
+
+<style scoped lang="scss">
+.relics-layout {
+  height: 100%;
+  overflow-y: auto;
+  padding: 30px;
+}
+.pag-layout {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+.relics-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  .search {
+    flex: 1;
+  }
+  .relics-oper {
+    flex: 0 0 100px;
+    text-align: right;
+  }
+}
+</style>

+ 68 - 0
src/view/down-vision.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="down-layout">
+    <el-form label-width="auto" style="max-width: 600px">
+      <el-form-item label="场景码">
+        <el-input v-model="sceneCode" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="onSubmit">下载数据</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref } from "vue";
+import { openLoading, closeLoading } from "@/helper/loading";
+import { ElMessage } from "element-plus";
+import { downloadPointsXLSL } from "@/util/pc4xlsl";
+
+const sceneCode = ref("KJ-t-3OdMlfX1z2g");
+const onSubmit = async () => {
+  if (!sceneCode.value) {
+    ElMessage.error("请输入场景码");
+  }
+
+  openLoading();
+
+  try {
+    const data = await fetch(
+      `https://4dkk.4dage.com/scene_view_data/${sceneCode.value}/images/vision.txt`
+    ).then((res) => res.json());
+    const points: number[][] = [];
+    const desc: { title: string; desc: string }[] = [];
+    const name = sceneCode.value;
+
+    console.log(data);
+    data.sweepLocations.forEach((item: any) => {
+      const gga = item.ggaLocation;
+      console.log(gga);
+      if (gga.StatusIndicator != 4 || !(gga.lat && gga.lon)) {
+        return;
+      }
+      points.push([Number(gga.lat), Number(gga.lon), Number(gga.alt)]);
+      desc.push({ title: item.uuid, desc: item.uuid });
+    });
+
+    if (points.length === 0) {
+      ElMessage.error("当前场景没有有效点位数据");
+    } else {
+      await downloadPointsXLSL(points, desc, name);
+      ElMessage.success("文件下载成功");
+    }
+  } catch (e) {
+    ElMessage.error("该场景不存在");
+  }
+
+  closeLoading();
+};
+</script>
+
+<style scoped>
+.down-layout {
+  display: flex;
+  width: 100%;
+  height: 100%;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 41 - 5
src/view/layout/nav.vue

@@ -14,11 +14,14 @@
       </el-dropdown>
     </div>
     <div class="content">
-      <RouterView v-slot="{ Component }">
-        <KeepAlive>
-          <component :is="Component" />
-        </KeepAlive>
-      </RouterView>
+      <ly-slide class="slide" v-if="user && !['pano', 'map'].includes(name)" />
+      <div class="view">
+        <RouterView v-slot="{ Component }">
+          <KeepAlive>
+            <component :is="Component" />
+          </KeepAlive>
+        </RouterView>
+      </div>
     </div>
   </div>
 </template>
@@ -29,6 +32,7 @@ import { router } from "@/router";
 import { computed } from "vue";
 import { user, logout } from "@/store/user";
 import { errorHook } from "@/request/state";
+import lySlide from "./slide/index.vue";
 
 const name = computed(() => router.currentRoute.value.meta?.navClass as string);
 const logoutHandler = () => {
@@ -78,5 +82,37 @@ errorHook.push((code) => {
 .content {
   flex: 1;
   overflow: hidden;
+  display: flex;
+
+  .slide {
+    width: 200px;
+    flex: 0 0 auto;
+    border-right: 1px solid var(--el-border-color);
+  }
+
+  .view {
+    flex: 1;
+    overflow: hidden;
+    background-color: var(--bgColor);
+    flex-direction: column;
+    display: flex;
+
+    &.full {
+      width: 100%;
+    }
+
+    .player {
+      flex: 0 0 auto;
+      height: 32px;
+    }
+
+    .main {
+      margin: 0 16px 16px;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+      flex: 1;
+    }
+  }
 }
 </style>

+ 29 - 0
src/view/layout/slide/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="slide">
+    <el-menu
+      :default-active="(router.currentRoute.value.name as string)"
+      @select="(name: string) => router.push({ name })"
+    >
+      <sub-menu
+        v-for="route in routes"
+        :meta="route.meta"
+        :name="route.name"
+        :key="route.name"
+      />
+    </el-menu>
+  </div>
+</template>
+
+<script setup lang="ts">
+import subMenu from "./submenu.vue";
+import { router, findRoute } from "@/router";
+
+const names = ["relics", "scene", "device"];
+const routes = names.map((name) => findRoute(name));
+</script>
+
+<style lang="scss" scoped>
+.slide {
+  background: #ffffff;
+}
+</style>

+ 12 - 0
src/view/layout/slide/submenu.vue

@@ -0,0 +1,12 @@
+<template>
+  <el-menu-item :index="name" :key="name">
+    <i :class="'iconfont ' + meta.icon" v-if="meta.icon"></i>
+    <span>{{ meta.title }}</span>
+  </el-menu-item>
+</template>
+
+<script lang="ts" setup>
+import { RouteMeta } from "@/router";
+
+defineProps<{ meta: any; name: string }>();
+</script>

+ 63 - 54
src/view/map/map-right.vue

@@ -10,12 +10,7 @@
           </el-input>
         </el-form-item>
         <el-form-item>
-          <el-button
-            type="primary"
-            :icon="Plus"
-            style="width: 100%"
-            @click="addSceneMode = true"
-          >
+          <el-button type="primary" :icon="Plus" style="width: 100%" @click="addHandler">
             添加场景
           </el-button>
         </el-form-item>
@@ -58,7 +53,7 @@
               <span class="oper">
                 <template v-if="router.currentRoute.value.name === 'map'">
                   <el-icon color="#409efc" v-if="data.type === 'scene'">
-                    <Delete @click.stop="delScene(data.raw)" />
+                    <Delete @click.stop="delRelicsScene(data.raw)" />
                   </el-icon>
                   <el-icon v-else color="#409efc">
                     <Edit @click.stop="inputPoint = data.raw" />
@@ -80,9 +75,23 @@
       </div>
     </div>
 
-    <el-button type="primary" :icon="Document" style="width: 100%" @click="exportFile">
+    <el-button
+      type="primary"
+      :icon="Document"
+      style="width: 100%"
+      @click="exportFile(getSelectPoints())"
+    >
       导出四普数据
     </el-button>
+
+    <el-button
+      type="primary"
+      :icon="Document"
+      style="width: 100%; margin-top: 20px; margin-left: 0"
+      @click="exportImage(getSelectPoints())"
+    >
+      下载全景图
+    </el-button>
   </div>
 
   <SingleInput
@@ -92,13 +101,6 @@
     :update-value="updatePointName"
     title="修改点位名称"
   />
-  <SingleInput
-    :visible="!!addSceneMode"
-    @update:visible="addSceneMode = false"
-    :value="''"
-    :update-value="addSceneHandler"
-    title="添加场景"
-  />
 </template>
 
 <script setup lang="ts">
@@ -113,43 +115,34 @@ import {
 } from "@element-plus/icons-vue";
 import { computed, ref, watchEffect } from "vue";
 import {
-  RelicsScene,
+  Scene,
   scenes,
-  RelicsScenePoint,
-  addScene,
-  delScene,
+  ScenePoint,
+  delRelicsScene,
   updateScenePointName,
   gotoScene,
-  relics,
-  updateRelicsName,
+  relicsId,
+  refreshScenes,
 } from "@/store/scene";
+import { relics, updateRelicsName } from "@/store/relics";
 import SingleInput from "@/components/single-input.vue";
-import { downloadPointsXLSL } from "@/util/pc4xlsl";
 import { ElMessage } from "element-plus";
 import { router } from "@/router";
+import { selectScenes } from "../quisk";
+import { addRelicsSceneFetch, delRelicsSceneFetch } from "@/request";
+import { exportFile, exportImage } from "./pc4Helper";
 
 const emit = defineEmits<{
-  (e: "flyScene", data: RelicsScene): void;
-  (e: "flyPoint", data: RelicsScenePoint): void;
-  (e: "gotoPoint", data: RelicsScenePoint): void;
+  (e: "flyScene", data: Scene): void;
+  (e: "flyPoint", data: ScenePoint): void;
+  (e: "gotoPoint", data: ScenePoint): void;
 }>();
 
-const inputPoint = ref<RelicsScenePoint | null>(null);
+const inputPoint = ref<ScenePoint | null>(null);
 const updatePointName = async (title: string) => {
   await updateScenePointName(inputPoint.value!, title);
 };
 
-const addSceneMode = ref(false);
-const addSceneHandler = async (sceneCode: string) => {
-  const sceneTypes = ["SS", "KJ", "SG"];
-  if (sceneTypes.every((type) => !sceneCode.startsWith(type))) {
-    ElMessage.error("场景码不正确");
-    throw "场景码不正确";
-  } else {
-    await addScene(sceneCode);
-  }
-};
-
 const relicsName = ref("");
 watchEffect(() => (relicsName.value = relics.value?.name || ""));
 const updateRelics = async () => {
@@ -175,27 +168,43 @@ const treeNode = computed(() =>
   }))
 );
 
-const exportFile = async () => {
-  let points: RelicsScenePoint[] = treeRef
+const getSelectPoints = () =>
+  treeRef
     .value!.getCheckedNodes(false, false)
     .filter((option: any) => option.type === "point")
-    .map((option: any) => option.raw);
-  if (!points.length) {
-    ElMessage.error("请选择要导出的点位");
-    return;
-  }
-  points = points.filter((point) => !!point.pos);
+    .map((option: any) => option.raw) as ScenePoint[];
+
+const addHandler = async () => {
+  const sceneCodes = scenes.value.map((scene) => scene.sceneCode);
+  await selectScenes({
+    sceneCodes: sceneCodes,
+    submit: async (nSceneCodes) => {
+      const requests: Promise<any>[] = [];
+      for (let i = 0; i < sceneCodes.length; i++) {
+        if (!nSceneCodes.includes(sceneCodes[i])) {
+          requests.push(delRelicsSceneFetch(relicsId.value, scenes.value[i].id));
+        }
+      }
+      for (let i = 0; i < nSceneCodes.length; i++) {
+        if (!sceneCodes.includes(nSceneCodes[i])) {
+          requests.push(addSceneHandler(nSceneCodes[i]));
+        }
+      }
 
-  if (points.length === 0) {
-    ElMessage.error("当前选择点位没有gis信息");
-    return;
+      await Promise.all(requests);
+    },
+  });
+  await refreshScenes();
+};
+
+const addSceneHandler = async (sceneCode: string) => {
+  const sceneTypes = ["SS", "KJ", "SG"];
+  if (sceneTypes.every((type) => !sceneCode.startsWith(type))) {
+    ElMessage.error("场景码不正确");
+    throw "场景码不正确";
+  } else {
+    await addRelicsSceneFetch(relicsId.value!, sceneCode);
   }
-  await downloadPointsXLSL(
-    points.map((point) => point.pos),
-    points.map((point) => ({ title: point.name, desc: point.name })),
-    "test"
-  );
-  ElMessage.success("文件导出成功");
 };
 </script>
 

+ 8 - 14
src/view/map/map.vue

@@ -40,19 +40,13 @@
 import MapRight from "./map-right.vue";
 import { router, setDocTitle } from "@/router";
 import { TileType, createMap } from "./";
-import {
-  RelicsScenePoint,
-  RelicsScene,
-  scenePoints,
-  initRelics,
-  relics,
-  scenes,
-} from "@/store/scene";
+import { ScenePoint, Scene, scenePoints, scenes } from "@/store/scene";
+import { initRelics, relics } from "@/store/relics";
 import { computed, onMounted, ref, watchEffect, watch } from "vue";
 import { Manage } from "./manage";
 
 const center = [109.47293862712675, 30.26530938156551];
-const active = ref<RelicsScenePoint | null>();
+const active = ref<ScenePoint | null>();
 const activePixel = ref<number[] | null>();
 const triggerRef = ref({
   getBoundingClientRect() {
@@ -66,7 +60,7 @@ const triggerRef = ref({
 });
 
 const tileOptions: TileType[] = ["影像底图", "矢量底图"];
-const tileType = ref<TileType>(tileOptions[1]);
+const tileType = ref<TileType>(tileOptions[0]);
 
 const points = computed(() =>
   scenePoints.value
@@ -78,7 +72,7 @@ const points = computed(() =>
     }))
 );
 
-const gotoPoint = (point: RelicsScenePoint) => {
+const gotoPoint = (point: ScenePoint) => {
   router.push({
     name: router.currentRoute.value.name === "map" ? "pano" : "query-pano",
     params: { pid: point.id },
@@ -107,21 +101,21 @@ onMounted(() => {
   refreshTileType();
 });
 
-const activeScenePoint = (point: RelicsScenePoint) => {
+const activeScenePoint = (point: ScenePoint) => {
   activePixel.value = mapManage.map.getPixelFromCoordinate(point.pos);
   active.value = point;
 };
 
 const flyPos = (pos: number[]) => mapManage.map.getView().setCenter(pos);
 
-const flyScenePoint = (point: RelicsScenePoint) => {
+const flyScenePoint = (point: ScenePoint) => {
   flyPos(point.pos);
   setTimeout(() => {
     activeScenePoint(point);
   }, 16);
 };
 
-const flyScene = (scene: RelicsScene) => {
+const flyScene = (scene: Scene) => {
   const totalPos = [0, 0];
   let numCalc = 0;
   for (let i = 0; i < scene.scenePos.length; i++) {

+ 91 - 0
src/view/map/pc4Helper.ts

@@ -0,0 +1,91 @@
+import { ScenePoint, getPointPano } from "@/store/scene";
+import JSZip from "jszip";
+import saveAs from "@/util/file-serve";
+import { openLoading, closeLoading } from "@/helper/loading";
+import { dateFormat } from "@/util";
+import { ElMessage } from "element-plus";
+import { downloadPointsXLSL } from "@/util/pc4xlsl";
+
+export const exportFile = async (points: ScenePoint[]) => {
+  if (!points.length) {
+    ElMessage.error("请选择要导出的点位");
+    return;
+  }
+  points = points.filter((point) => !!point.pos);
+
+  if (points.length === 0) {
+    ElMessage.error("当前选择点位没有gis信息");
+    return;
+  }
+  await downloadPointsXLSL(
+    points.map((point) => point.pos),
+    points.map((point) => ({ title: point.name, desc: point.name })),
+    "test"
+  );
+  ElMessage.success("文件导出成功");
+};
+
+export const exportImage = async (points: ScenePoint[]) => {
+  openLoading();
+
+  if (!points.length) {
+    ElMessage.error("请选择要导出的点位");
+    return;
+  }
+
+  const zip = new JSZip();
+  const imgFolder = zip.folder("images")!;
+
+  const downloadImages = Promise.all(
+    points.map((point) => {
+      const url = getPointPano(point.sceneCode, point.uuid);
+      return fetch(url)
+        .then((res) => res.blob())
+        .then((blob) => {
+          imgFolder.file(`${point.sceneCode}-${point.uuid}.jpg`, blob);
+        })
+        .catch((e) => {
+          ElMessage.error(url + "图片下载失败!");
+          throw e;
+        });
+    })
+  );
+  await downloadImages;
+
+  const content = await zip.generateAsync({ type: "blob" });
+  await saveAs(content, "images.zip");
+  closeLoading();
+};
+
+export const exposeExsl = async (pointsGroup: ScenePoint[][]) => {
+  const timeStr = dateFormat(new Date(), "yyyy-MM-ddThh:mm:ssZ");
+  const rets: string[] = pointsGroup.map((points, i) => {
+    const name = `Rt${(i + 1).toString().padStart(4, "0")}`;
+    const rtepts = points.map((point, j) => {
+      return `
+          <rtept lat="${point.pos[1]}" lon="${point.pos[0]}">
+              <ele>${point.pos[2]}</ele>
+              <time>${timeStr}</time>
+              <name>${name}_pt${(j + 1).toString().padStart(4, "0")}</name>
+          </rtept>
+        `;
+    });
+    return `
+    <rte>
+          <name>${name}</name>
+          ${rtepts.join("")}
+    </rte>
+    `;
+  });
+
+  for (let i = 0; i < rets.length; i++) {
+    const name = `Rt${(i + 1).toString().padStart(4, "0")}`;
+    const text = `\
+<?xml version="1.0" encoding="UTF-8" ?>
+<gpx version="1.1" creator="BHCNAV" xmlns="http://www.topografix.com/GPX/1/1">
+  ${rets[i]}
+</gpx>
+  `;
+    await saveAs(new Blob([text], { type: "text/plain" }), `${name}.gpx`);
+  }
+};

+ 17 - 6
src/view/pano/env.ts

@@ -7,7 +7,7 @@ import { setUniforms } from "./setUniform";
 
 const generatePreset = (gl: WebGL2RenderingContext) => {
   const skyCubeTex = gl.createTexture();
-  const updateSky = (images: HTMLImageElement[]) => {
+  const updateSky1 = (images: HTMLImageElement[]) => {
     gl.bindTexture(gl.TEXTURE_CUBE_MAP, skyCubeTex);
     const mapper = [2, 4, 0, 5, 1, 3];
     for (let i = 0; i < 6; i++) {
@@ -28,12 +28,23 @@ const generatePreset = (gl: WebGL2RenderingContext) => {
     gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
     gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
   };
+  const updateSky = (image: HTMLImageElement) => {
+    gl.bindTexture(gl.TEXTURE_2D, skyCubeTex);
+    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+    gl.bindTexture(gl.TEXTURE_2D, null);
+  };
 
   return {
     skyCubeTex,
-    async preset(urls: string[]) {
-      const images = await Promise.all(urls.map(loadImage));
-      updateSky(images);
+    // async preset(urls: string[]) {
+    //   const images = await Promise.all(urls.map(loadImage));
+    //   updateSky(images);
+    // },
+    async preset(url: string) {
+      const image = await loadImage(url);
+      updateSky(image);
     },
   };
 };
@@ -87,7 +98,7 @@ export const init = (canvas: HTMLCanvasElement) => {
     gl.bindVertexArray(varing.vao);
     setUniforms(gl, program, {
       invProjectionViewMat,
-      envTex: useTex(gl, varing.skyCubeTex!, gl.TEXTURE_CUBE_MAP),
+      envTex: useTex(gl, varing.skyCubeTex!),
     });
     gl.drawArrays(gl.TRIANGLES, 0, varing.numArrays);
   };
@@ -106,7 +117,7 @@ export const init = (canvas: HTMLCanvasElement) => {
   );
   return {
     redraw,
-    changeUrls(urls: string[]) {
+    changeUrls(urls: string) {
       fps.recovery();
       return varing.preset(urls).then(redraw);
     },

+ 3 - 3
src/view/pano/pano.vue

@@ -49,7 +49,7 @@ import {
   scenePoints,
   updateScenePointName,
   getPointPano,
-  RelicsScenePoint,
+  ScenePoint,
 } from "@/store/scene";
 import { copyText, toDegrees } from "@/util";
 import { ElMessage } from "element-plus";
@@ -60,7 +60,7 @@ type Params = { pid?: string } | null;
 const params = computed(() => router.currentRoute.value.params as Params);
 const panoDomRef = ref<HTMLCanvasElement>();
 const destroyFns: (() => void)[] = [];
-const point = ref<RelicsScenePoint>();
+const point = ref<ScenePoint>();
 watchEffect(() => {
   if (params.value?.pid) {
     const pid = Number(params.value!.pid);
@@ -82,7 +82,7 @@ const loading = ref(false);
 const copyGis = async () => {
   const pos = point.value!.pos;
   await copyText(
-    `经度:${toDegrees(pos[1])}, 纬度: ${toDegrees(pos[0])}, 高程: ${pos[2]}`
+    `经度:${toDegrees(pos[0])}, 纬度: ${toDegrees(pos[1])}, 高程: ${pos[2]}`
   );
   ElMessage.success("经纬度高程复制成功");
 };

+ 18 - 7
src/view/pano/shader-env.frag

@@ -1,14 +1,25 @@
 #version 300 es
-precision highp float;
+precision mediump float;
 
-uniform samplerCube envTex;
+#define PI 3.14159265359
+
+uniform sampler2D envTex;
 
 in vec4 vLocalPosition;
-out vec4 oFragColor;
 
+const vec2 invAtan = vec2(1.0 / (PI * 2.0), -1.0 / PI);
+vec2 getSphereUV(vec3 v) {
+    vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
+    uv *= invAtan; 
+    uv += 0.5;
+    uv = mod(uv, 1.0);
+    return uv;
+}
+
+out vec4 vFragColor;
 void main(){
-  vec3 nor = normalize(vLocalPosition.xyz / vLocalPosition.w);
-  nor.x *= -1.0;
-  vec3 color = texture(envTex, nor).rgb;
-  oFragColor = vec4(color, 1);
+  vec2 uv = getSphereUV(normalize(vLocalPosition.xyz / vLocalPosition.w));
+  vec3 color = texture(envTex, uv).rgb;
+  // color /= color + 1.0;
+  vFragColor = vec4(color, 1);
 }

+ 18 - 0
src/view/quisk.ts

@@ -0,0 +1,18 @@
+import { quiskMountFactory } from "@/helper/mount";
+import RelicsEdit from "./relics-edit.vue";
+import DeviceEdit from "./device-edit.vue";
+import SceneSelect from "./scene-select.vue";
+
+export const relicsEdit = quiskMountFactory(RelicsEdit, {
+  title: "添加文物保护单位",
+  width: 486,
+});
+export const deviceEdit = quiskMountFactory(DeviceEdit, {
+  title: "添加设备",
+  width: 486,
+});
+
+export const selectScenes = quiskMountFactory(SceneSelect, {
+  title: "选择场景",
+  width: 900,
+});

+ 55 - 0
src/view/relics-edit.vue

@@ -0,0 +1,55 @@
+<template>
+  <el-form label-width="100px">
+    <el-form-item label="文物名称:" required>
+      <el-input v-model="data.name" style="width: 250px" :maxlength="500" />
+    </el-form-item>
+    <el-form-item label="文物编号:">
+      <el-input v-model="data.unicode" style="width: 250px" :maxlength="500" />
+    </el-form-item>
+    <el-form-item label="文物级别:">
+      <el-select style="width: 250px" v-model="data.level">
+        <el-option
+          :value="Number(key)"
+          :label="type"
+          v-for="(type, key) in relicsLevelDesc"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="类别:">
+      <el-select style="width: 250px" v-model="data.category">
+        <el-option
+          :value="Number(key)"
+          :label="type"
+          v-for="(type, key) in relicsTypeDesc"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="文物地址:">
+      <el-input v-model="data.address" style="width: 250px" :maxlength="500" />
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { QuiskExpose } from "@/helper/mount";
+import { Relics, relicsLevelDesc, relicsTypeDesc } from "@/store/relics";
+import { ElMessage } from "element-plus";
+import { computed, ref } from "vue";
+
+const props = defineProps<{
+  relics?: Relics;
+  submit: (relics: Relics) => Promise<void>;
+}>();
+const data = ref<Relics>(props.relics ? { ...props.relics } : { name: "", relicsId: -1 });
+
+defineExpose<QuiskExpose>({
+  title: computed(() => `${props.relics ? "修改" : "添加"}文物保护单位`),
+  async submit() {
+    if (!data.value.name) {
+      ElMessage.error("请输入名称!");
+      throw "请输入名称!";
+    }
+    props.submit(data.value);
+  },
+});
+</script>

+ 76 - 31
src/view/relics.vue

@@ -2,37 +2,71 @@
   <div class="relics-layout">
     <div class="relics-header">
       <div class="search">
-        <el-form label-width="60px" inline>
+        <el-form label-width="100px" inline>
           <el-form-item label="名称:">
-            <el-input v-model="pageProps.name" />
+            <el-input v-model="pageProps.name" style="width: 250px" />
+          </el-form-item>
+          <el-form-item label="文物级别:">
+            <el-select style="width: 250px" v-model="pageProps.level">
+              <el-option
+                :value="Number(key)"
+                :label="type"
+                v-for="(type, key) in relicsLevelDesc"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="类别:">
+            <el-select style="width: 250px" v-model="pageProps.category">
+              <el-option
+                :value="Number(key)"
+                :label="type"
+                v-for="(type, key) in relicsTypeDesc"
+              />
+            </el-select>
           </el-form-item>
           <el-form-item class="searh-btns">
             <el-button type="primary" @click="refresh">查询</el-button>
-            <el-button type="primary" plain @click="pageProps = initProps">
+            <el-button type="primary" plain @click="pageProps = { ...initProps }">
               重置
             </el-button>
+            <el-button type="primary" @click="addHandler">新增</el-button>
           </el-form-item>
         </el-form>
       </div>
-      <div class="relics-oper">
+      <!-- <div class="relics-oper">
         <el-button type="primary" @click="inputMode = true">新增</el-button>
-      </div>
+      </div> -->
     </div>
 
     <el-table :data="relicsArray" border>
       <el-table-column prop="name" label="文物保护单位名称" />
-      <el-table-column label="操作" width="200">
+      <el-table-column prop="unicode" label="文物编号" />
+      <el-table-column prop="name" label="类别" v-slot:default="{ row }: { row: Relics }">
+        {{ relicsLevelDesc[row.level!] }}
+      </el-table-column>
+      <el-table-column prop="name" label="类别" v-slot:default="{ row }: { row: Relics }">
+        {{ relicsTypeDesc[row.category!] }}
+      </el-table-column>
+      <el-table-column prop="address" label="文物地址" />
+      <el-table-column label="创建时间" prop="createTime" v-slot:default="{ row }">
+        {{ row.createTime && row.createTime.substr(0, 16) }}
+      </el-table-column>
+      <el-table-column prop="userName" label="创建人" />
+      <el-table-column
+        prop="creationMethod"
+        label="创建方式"
+        v-slot:default="{ row }: { row: Relics }"
+      >
+        {{ creationMethodDesc[row.creationMethod!] }}
+        {{}}
+      </el-table-column>
+      <el-table-column label="操作" width="240" fixed="right">
         <template #default="{ row }">
           <el-button link type="primary" size="small" @click="shareHandler(row)">
-            分享
+            文物链接
           </el-button>
-          <el-button
-            link
-            type="primary"
-            size="small"
-            @click="router.push(getQueryRouteLocation(row))"
-          >
-            查看
+          <el-button link type="primary" size="small" @click="editHandler(row)">
+            编辑
           </el-button>
           <el-button
             link
@@ -40,7 +74,7 @@
             size="small"
             @click="router.push({ name: 'map', params: { relicsId: row.relicsId } })"
           >
-            编辑
+            数据提取
           </el-button>
           <el-button link type="danger" @click="delHandler(row.relicsId)" size="small">
             删除
@@ -59,32 +93,29 @@
       />
     </div>
   </div>
-
-  <SingleInput
-    :visible="inputMode"
-    @update:visible="inputMode = false"
-    :value="''"
-    :update-value="addRelicsHandler"
-    title="添加文物保护单位"
-  />
 </template>
 
 <script lang="ts" setup>
 import { onActivated, ref, watchEffect } from "vue";
-import SingleInput from "@/components/single-input.vue";
 import {
   relicsPageFetch,
   RelicsPageProps,
   addRelicsFetch,
   delRelicsFetch,
+  updateRelicsFetch,
 } from "@/request";
-import { Relics } from "@/request/type";
+import {
+  Relics,
+  relicsLevelDesc,
+  relicsTypeDesc,
+  creationMethodDesc,
+} from "@/store/relics";
 import { router } from "@/router";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { copyText } from "@/util";
+import { relicsEdit } from "./quisk";
 
 const initProps: RelicsPageProps = {
-  name: "",
   pageNum: 1,
   pageSize: 12,
 };
@@ -97,10 +128,6 @@ const refresh = async () => {
   total.value = data.total;
   relicsArray.value = data.records;
 };
-const addRelicsHandler = async (name: string) => {
-  await addRelicsFetch(name);
-  await refresh();
-};
 
 const delHandler = async (relicsId: number) => {
   const ok = await ElMessageBox.confirm("确定要删除吗", {
@@ -120,8 +147,25 @@ const shareHandler = async (row: Relics) => {
   await ElMessage.success("链接复制成功");
 };
 
-const inputMode = ref(false);
+const addHandler = async () => {
+  const a = await relicsEdit({
+    submit: async (data) => {
+      await addRelicsFetch(data);
+      await refresh();
+    },
+  });
+  console.log(a);
+};
 
+const editHandler = async (relics: Relics) => {
+  await relicsEdit({
+    relics,
+    submit: async (data) => {
+      await updateRelicsFetch(data);
+      await refresh();
+    },
+  });
+};
 watchEffect(refresh);
 onActivated(refresh);
 </script>
@@ -141,6 +185,7 @@ onActivated(refresh);
 .relics-header {
   display: flex;
   align-items: center;
+  margin-bottom: 20px;
   .search {
     flex: 1;
   }

+ 66 - 0
src/view/scene-select.vue

@@ -0,0 +1,66 @@
+<template>
+  <SceneTable :tableProps="tableProps" simple>
+    <template v-slot:table>
+      <el-table-column type="selection" width="55" />
+    </template>
+  </SceneTable>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from "vue";
+import SceneTable from "./scene.vue";
+import { ElTable } from "element-plus";
+import { QuiskExpose } from "@/helper/mount";
+import { Scene } from "@/store/scene";
+
+const props = defineProps<{
+  submit: (sceneCodes: string[]) => Promise<any>;
+  sceneCodes: string[];
+}>();
+const sceneCodes = ref([...props.sceneCodes]);
+const originScenes = ref<Scene[]>([]);
+const scenes = computed(() =>
+  originScenes.value.filter((scene) => sceneCodes.value.includes(scene.sceneCode))
+);
+
+const loaded = ref(false);
+const tableProps = {
+  selectionChange(val: Scene[]) {
+    if (!loaded.value) return;
+    const originSceneCodes = originScenes.value.map((scene) => scene.sceneCode);
+    sceneCodes.value = sceneCodes.value.filter(
+      (code) => !originSceneCodes.includes(code)
+    );
+    sceneCodes.value.push(...val.map((scene) => scene.sceneCode));
+  },
+  tableDataChange(val: Scene[]) {
+    originScenes.value = val;
+    setTimeout(checkedTable);
+  },
+  tableRef: ref<InstanceType<typeof ElTable>>(),
+};
+
+let time: NodeJS.Timeout;
+const checkedTable = () => {
+  if (tableProps.tableRef.value) {
+    tableProps.tableRef.value!.clearSelection();
+    scenes.value.forEach((item) => {
+      tableProps.tableRef.value!.toggleRowSelection(item, true);
+    });
+    clearTimeout(time);
+    time = setTimeout(() => {
+      loaded.value = true;
+    }, 1000);
+  }
+};
+
+watch(tableProps.tableRef, checkedTable);
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (scenes.value) {
+      await props.submit(scenes.value.map((scene) => scene.sceneCode));
+    }
+  },
+});
+</script>

+ 174 - 0
src/view/scene.vue

@@ -0,0 +1,174 @@
+<template>
+  <div class="relics-layout">
+    <div class="relics-header">
+      <div class="search">
+        <el-form label-width="100px" inline>
+          <el-form-item label="名称:">
+            <el-input v-model="pageProps.sceneName" style="width: 250px" />
+          </el-form-item>
+          <el-form-item label="场景码:">
+            <el-input v-model="pageProps.sceneCode" style="width: 250px" />
+          </el-form-item>
+          <template v-if="!simple">
+            <el-form-item label="sn码:">
+              <el-input v-model="pageProps.snCode" style="width: 250px" />
+            </el-form-item>
+            <el-form-item label="设备类型:">
+              <el-select style="width: 250px" v-model="pageProps.cameraType">
+                <el-option
+                  :value="Number(key)"
+                  :label="type"
+                  v-for="(type, key) in DeviceTypeDesc"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="拍摄时间:">
+              <el-date-picker
+                type="daterange"
+                v-model="pageProps.algorithmTime"
+                range-separator="-"
+                placeholder="请选择"
+                style="width: 250px"
+              />
+            </el-form-item>
+            <el-form-item label="绑定账号:">
+              <el-input v-model="pageProps.userName" style="width: 250px" />
+            </el-form-item>
+            <el-form-item v-if="!simple">
+              <el-button type="primary" @click="refresh">查询</el-button>
+              <el-button type="primary" plain @click="pageProps = { ...initProps }">
+                重置
+              </el-button>
+            </el-form-item>
+          </template>
+        </el-form>
+      </div>
+    </div>
+
+    <el-table
+      :data="sceneArray"
+      border
+      @selection-change="(val) => tableProps && tableProps.selectionChange(val)"
+      :ref="(table) => tableProps && (tableProps.tableRef.value = table)"
+    >
+      <slot name="table"></slot>
+      <el-table-column label="场景标题" prop="sceneName"></el-table-column>
+      <el-table-column label="场景类型" prop="snCode" v-slot:default="{ row }">
+        {{ SceneTypeDesc[row.cameraType as SceneType] }}
+      </el-table-column>
+      <el-table-column label="场景码" prop="sceneCode"></el-table-column>
+      <el-table-column label="sn" prop="snCode"></el-table-column>
+      <el-table-column label="设备类型" prop="snCode" v-slot:default="{ row }">
+        {{ DeviceTypeDesc[row.cameraType as DeviceType] }}
+      </el-table-column>
+      <el-table-column label="拍摄时间" prop="algorithmTime" v-slot:default="{ row }">
+        {{ row.algorithmTime && row.algorithmTime.substr(0, 16) }}
+      </el-table-column>
+      <el-table-column label="计算完成时间" prop="createTime" v-slot:default="{ row }">
+        {{ row.createTime && row.createTime.substr(0, 16) }}
+      </el-table-column>
+      <el-table-column label="点位数量" prop="shootCount"></el-table-column>
+      <el-table-column label="拍摄位置" prop="gpsInfo"></el-table-column>
+      <el-table-column label="绑定账号" prop="userName"></el-table-column>
+      <el-table-column label="状态" v-slot:default="{ row }">
+        {{ SceneStatusDesc[(row.calcStatus as SceneStatus)] }}
+      </el-table-column>
+      <el-table-column label="操作" width="150" fixed="right" v-if="!simple">
+        <template #default="{ row }">
+          <el-button link type="primary" size="small" @click="gotoScene(row, false)">
+            查看
+          </el-button>
+          <el-button link type="primary" size="small" @click="gotoScene(row, true)">
+            编辑
+          </el-button>
+          <el-button link type="danger" @click="delHandler(row.sceneId)" size="small">
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="pag-layout">
+      <el-pagination
+        background
+        layout="prev, pager, next"
+        :total="total"
+        @current-change="(data: number) => pageProps.pageNum = data"
+        :current-page="pageProps.pageNum"
+        :page-size="pageProps.pageSize"
+      />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onActivated, ref, watchEffect } from "vue";
+import { scenePageFetch, ScenePageProps, delSceneFetch } from "@/request";
+import {
+  SceneStatusDesc,
+  SceneStatus,
+  SceneType,
+  SceneTypeDesc,
+  Scene,
+} from "@/store/scene";
+import { DeviceTypeDesc, DeviceType } from "@/store/device";
+import { ElMessageBox } from "element-plus";
+import { gotoScene } from "@/store/scene";
+
+const props = defineProps<{ tableProps?: { [key in string]: any }; simple?: boolean }>();
+
+const initProps: ScenePageProps = {
+  pageNum: 1,
+  pageSize: 12,
+};
+const pageProps = ref({ ...initProps });
+const total = ref<number>(0);
+const sceneArray = ref<Scene[]>([]);
+
+const refresh = async () => {
+  const data = await scenePageFetch(pageProps.value);
+  total.value = data.total;
+  sceneArray.value = data.records;
+  if (props.tableProps) {
+    props.tableProps.tableDataChange(sceneArray.value);
+  }
+};
+
+const delHandler = async (relicsId: number) => {
+  const ok = await ElMessageBox.confirm("确定要删除吗", {
+    type: "warning",
+  });
+  if (ok) {
+    await delSceneFetch(relicsId);
+    await refresh();
+  }
+};
+
+watchEffect(refresh);
+onActivated(refresh);
+</script>
+
+<style scoped lang="scss">
+.relics-layout {
+  height: 100%;
+  overflow-y: auto;
+  padding: 30px;
+}
+.pag-layout {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+.relics-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  .search {
+    flex: 1;
+  }
+  .relics-oper {
+    flex: 0 0 100px;
+    text-align: right;
+  }
+}
+</style>

+ 7 - 7
vite.config.ts

@@ -19,16 +19,16 @@ export default defineConfig({
     port: 5173,
     open: true,
     proxy: {
-      // "/relics": {
-      //   target: "http://192.168.0.11:8324",
-      //   changeOrigin: true,
-      //   rewrite: (path) => path.replace(/^\/relics/, "/relics"),
-      // },
       "/api": {
-        target: `https://test-sp.4dkankan.com/`,
+        target: "http://192.168.0.11:8324",
         changeOrigin: true,
-        rewrite: (path) => path.replace(/^\/api/, "/api"),
+        rewrite: (path) => path.replace(/^\/api/, ""),
       },
+      // "/api": {
+      //   target: `https://test-sp.4dkankan.com/`,
+      //   changeOrigin: true,
+      //   rewrite: (path) => path.replace(/^\/api/, "/api"),
+      // },
     },
   },
 });