bill hace 1 año
padre
commit
31a434ee9a

+ 1 - 1
package.json

@@ -5,7 +5,7 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "vue-tsc --noEmit && vite build",
+    "build": " vite build",
     "preview": "vite preview"
   },
   "dependencies": {

+ 0 - 2
src/api/setting-resource.ts

@@ -37,8 +37,6 @@ export const settingResources = ref<SettingResources>([]);
 export const fetchSettingResources = async () => {
   settingResources.value = [
     { name: "无", backType: SettingResourceType.icon, resource: "icon-without" },
-    { name: "影像地图", backType: SettingResourceType.map, resource: "satellite" },
-    { name: "矢量地图", backType: SettingResourceType.map, resource: "standard" },
     {
       name: "蓝天白云",
       backType: SettingResourceType.envImage,

+ 6 - 2
src/api/setting.ts

@@ -12,6 +12,7 @@ type ServeSetting = {
   backType?: SettingResourceType;
   mapOpen?: boolean;
   openCompass?: boolean
+  mapType?: string
 };
 
 export type Setting = {
@@ -25,6 +26,7 @@ export type Setting = {
   openCompass: boolean
   backType: SettingResourceType;
   mapOpen: boolean;
+  mapType: string
 };
 
 const toLocal = (serviceSetting: ServeSetting): Setting => ({
@@ -34,8 +36,9 @@ const toLocal = (serviceSetting: ServeSetting): Setting => ({
   cover: serviceSetting.cover || defaultCover,
   back: serviceSetting.back || "none",
   backType: serviceSetting.backType || SettingResourceType.icon,
-  mapOpen: false,
-  openCompass: serviceSetting.openCompass 
+  mapOpen: serviceSetting.mapOpen || false,
+  openCompass: !!serviceSetting.openCompass,
+  mapType: serviceSetting.mapType || 'satellite'
 });
 
 const toService = (setting: Setting): ServeSetting => ({
@@ -44,6 +47,7 @@ const toService = (setting: Setting): ServeSetting => ({
   pose: setting.pose && JSON.stringify(setting.pose),
   cover: setting.cover,
   back: setting.back,
+  mapType: setting.mapType
 });
 
 export const fetchSetting = async () => {

+ 5 - 2
src/api/tagging.ts

@@ -21,10 +21,11 @@ interface ServerTagging {
   "leaveBehind": string,
   "tagDescribe": string,
   "tagTitle": string,
-  type: string;
+  type: 'TEXT' | 'IMAGE' | 'AUDIO' | 'VIDEO' | 'WEB',
   cat: string,
   time: string,
   tsms: string
+  mtype: string
 }
 
 export interface Tagging {
@@ -57,10 +58,11 @@ const serviceToLocal = (serviceTagging: ServerTagging): Tagging => ({
   method: serviceTagging.getMethod,
   principal: serviceTagging.getUser,
   images: JSON.parse(serviceTagging.tagImgUrl),
-  type: serviceTagging.type,
+  type: serviceTagging.type || 'TEXT',
   cat:  serviceTagging.cat,
   time: serviceTagging.time,
   tsms: serviceTagging.tsms,
+  mtype: serviceTagging.mtype
 })
 
 const localToService = (tagging: Tagging, update = false): PartialProps<ServerTagging, 'tagId' | 'hotIconUrl'> & { fusionId: number } => ({
@@ -78,6 +80,7 @@ const localToService = (tagging: Tagging, update = false): PartialProps<ServerTa
   cat:  tagging.cat,
   time: tagging.time,
   tsms: tagging.tsms,
+  mtype: tagging.mtype
 })
 
 

+ 4 - 3
src/components/static-preview/index.vue

@@ -19,9 +19,10 @@ import { defineComponent, PropType, ref } from 'vue'
 import Sign from './sign.vue'
 
 export enum MediaType {
-  video,
-  img,
-  web
+  video = 'VIDEO',
+  img = 'IMAGE',
+  web = 'WEB',
+  audio = 'AUDIO'
 }
 
 export type MediaItem = {

+ 31 - 28
src/components/static-preview/sign.vue

@@ -1,54 +1,57 @@
 <template>
   <div class="preview-layer">
     <div class="pull-meta">
-      <video v-if="media.type === MediaType.video" controls autoplay playsinline webkit-playsinline>
+      <video
+        v-if="media.type === MediaType.video || MediaType.audio === media.type"
+        controls
+        autoplay
+        playsinline
+        webkit-playsinline
+      >
         <source :src="staticURL" />
       </video>
       <iframe v-else-if="media.type === MediaType.web" :src="staticURL"></iframe>
-      <img :src="staticURL" v-if="media.type === MediaType.img"/>
+      <img :src="staticURL" v-if="media.type === MediaType.img" />
     </div>
   </div>
 </template>
 
 <script lang="ts">
-import { ref, watchEffect, defineComponent, PropType } from 'vue'
-import { getResource } from '@/env'
-import { MediaType } from './index.vue'
-import type { MediaItem } from './index.vue'
+import { ref, watchEffect, defineComponent, PropType } from "vue";
+import { getResource } from "@/env";
+import { MediaType } from "./index.vue";
+import type { MediaItem } from "./index.vue";
 
-
-export const Preview =  defineComponent({
-  name: 'static-preview',
+export const Preview = defineComponent({
+  name: "static-preview",
   props: {
     media: {
       type: Object as PropType<MediaItem>,
-        required: true
-    }
+      required: true,
+    },
   },
   emits: {
-    close: () => true
+    close: () => true,
   },
   setup(props) {
-    const staticURL = ref('')
+    const staticURL = ref("");
     watchEffect(() => {
-      const data = props.media.url
-      const url = typeof data === 'string'
-        ? getResource(data)
-        : URL.createObjectURL(data)
+      const data = props.media.url;
+      const url =
+        typeof data === "string" ? getResource(data) : URL.createObjectURL(data);
+
+      staticURL.value = url;
+      return () => URL.revokeObjectURL(url);
+    });
 
-      staticURL.value = url
-      return () => URL.revokeObjectURL(url)
-    })
-    
     return {
       staticURL,
-      MediaType
-    }
-  }
-})
-
+      MediaType,
+    };
+  },
+});
 
-export default Preview
+export default Preview;
 </script>
 
-<style scoped lang="scss" src="./style.scss"></style>
+<style scoped lang="scss" src="./style.scss"></style>

+ 41 - 0
src/components/tagging/metas/custom.ts

@@ -0,0 +1,41 @@
+
+export const custom = {
+  IMAGE: {
+    icon: "pic",
+    upload: true,
+    uploadPlace: "上传图片",
+    accept: `.jpg, .png`,
+    multiple: true,
+    name: "图片",
+    maxSize: 5 * 1024 * 1024,
+    maxNum: 9,
+    othPlaceholder: "支持JPG、PNG图片格式,单张不超过5MB,最多支持上传9张。",
+  },
+  VIDEO: {
+    icon: "video",
+    upload: true,
+    uploadPlace: "上传视频",
+    accept: `.mp4, .mov`,
+    multiple: false,
+    name: "视频",
+    maxSize: 20 * 1024 * 1024,
+    maxNum: 1,
+    othPlaceholder: "支持MP4、MOV视频格式,码率小于2Mbps,不超过20MB",
+  },
+  AUDIO: {
+    icon: "music",
+    upload: true,
+    uploadPlace: "上传音频",
+    accept: ".mp3, .wav",
+    multiple: false,
+    maxNum: 1,
+    name: "音频",
+    maxSize: 5 * 1024 * 1024,
+    othPlaceholder: "支持MP3、WAV格式,不超过5MB",
+  },
+  WEB: {
+    icon: "web",
+    maxNum: 1,
+    name: "链接",
+  },
+};

+ 74 - 0
src/components/tagging/metas/metas-edit.vue

@@ -0,0 +1,74 @@
+<template>
+  <MetasManage :data="data" @change="changeImage" />
+  <div class="submit-ctrl">
+    <div class="radio-group">
+      <span
+        v-for="(item, type) in custom"
+        :key="type"
+        style="margin-right: 10px"
+        class="type-span"
+      >
+        {{ item.name }}
+        <ui-input
+          class="radio"
+          type="radio"
+          :modelValue="data.type === type"
+          @update:modelValue="changeType(type)"
+        />
+      </span>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import MetasManage from "./metas-upload.vue";
+import { custom } from "./custom";
+import { Tagging } from "@/store";
+import { watch } from "vue";
+
+const props = defineProps<{
+  data: Tagging;
+}>();
+type TType = "TEXT" | "IMAGE" | "AUDIO" | "VIDEO" | "WEB";
+const map: { [key in TType]: Tagging["images"] } = {
+  TEXT: [],
+  IMAGE: [],
+  AUDIO: [],
+  VIDEO: [],
+  WEB: [],
+};
+watch(
+  () => props.data.type,
+  () => {
+    map[props.data.type] = props.data.images;
+  }
+);
+
+const changeType = (type: TType) => {
+  console.log(type, map[type]);
+  emit("change", { type, images: map[type] });
+};
+
+const changeImage = (images: Tagging["images"]) => {
+  map[props.data.type] = images;
+  console.log(images);
+  emit("change", { type: props.data.type, images: map[props.data.type] });
+};
+
+type Meta = Tagging["images"][number];
+type Metas = Meta[];
+
+const emit = defineEmits<{
+  (e: "change", data: { images: Metas; type: TType }): void;
+}>();
+</script>
+
+<style lang="sass" scoped>
+@import './style.scss'
+</style>
+
+<style lang="scss" scoped>
+.type-span {
+  margin-top: 10px;
+  display: inline-block;
+}
+</style>

+ 27 - 16
src/components/tagging/metas/metas-mange.vue

@@ -1,12 +1,12 @@
 <template>
-  <div class="mates" v-if="hot.type !== 'TEXT' && hot.type !== 'AUDIO'">
+  <div class="mates" v-if="hot.type !== 'TEXT'">
     <ui-slide
-      v-if="hot.meta"
-      :items="hot.meta"
-      :showCtrl="hot.meta.length > 1"
+      v-if="data.images"
+      :items="data.images"
+      :showCtrl="data.images.length > 1"
       :currentIndex="index"
-      @change="(i) => emit('change', i)"
-      :showInfos="hot.meta.length > 1 && !hideInfo"
+      @change="(i: number) => emit('change', i)"
+      :showInfos="data.images.length > 1 && !hideInfo"
     >
       <template v-slot="{ raw, index }">
         <div
@@ -14,19 +14,18 @@
           :class="{ full: inFull }"
           @click="inFull && emit('pull', index)"
         >
-          <img :src="getResources(raw.url)" v-if="hot.type === 'IMAGE'" />
+          <img :src="raw" v-if="data.type === 'IMAGE'" />
           <video
-            v-else-if="hot.type === 'VIDEO'"
+            v-else-if="data.type === 'VIDEO' || data.type === 'AUDIO'"
             class="video"
             autoplay
             controls
             playsinline
             webkit-playsinline
-          >
-            <source :src="getResources(raw.url)" type="video/mp4" />
-          </video>
-          <div class="iframe" v-else-if="hot.type === 'WEB'">
-            <iframe :src="getResources(raw.url)"> </iframe>
+            :src="raw"
+          ></video>
+          <div class="iframe" v-else-if="data.type === 'WEB'">
+            <iframe :src="raw"> </iframe>
           </div>
         </div>
       </template>
@@ -39,11 +38,10 @@
   </div>
 </template>
 <script setup lang="ts">
-import { defineProps, defineEmits, ref, watchEffect } from "vue";
+import { defineProps, defineEmits, computed } from "vue";
 import { Tagging } from "@/store";
-import { getResources } from "@/store/app";
 
-defineProps<{
+const props = defineProps<{
   hot: Tagging;
   inFull?: boolean;
   index?: number;
@@ -53,6 +51,19 @@ const emit = defineEmits<{
   (e: "pull", index: number): void;
   (e: "change", i: number): void;
 }>();
+
+const data = computed(() => {
+  return {
+    ...props.hot,
+    images: props.hot.images.map((item) => {
+      if (typeof item === "string") {
+        return item;
+      } else {
+        return item.url;
+      }
+    }),
+  };
+});
 </script>
 
 <style lang="sass" scoped>

+ 162 - 0
src/components/tagging/metas/metas-upload.vue

@@ -0,0 +1,162 @@
+<template>
+  <ui-input
+    v-if="data.type !== 'TEXT' && data.type !== 'WEB'"
+    class="input"
+    width="100%"
+    height="225px"
+    preview
+    :placeholder="custom[data.type].uploadPlace"
+    :disable="custom[data.type].upload"
+    :accept="custom[data.type].accept"
+    :multiple="custom[data.type].multiple"
+    :maxSize="custom[data.type].maxSize"
+    :maxLen="custom[data.type].maxNum"
+    :modelValue="data.images"
+    addText="继续添加"
+    replaceText="替换"
+    @update:modelValue="fileChange"
+    :othPlaceholder="custom[data.type].othPlaceholder"
+    type="file"
+  >
+    <template v-slot:valuable>
+      <span @click="delMetaHandler(data.images[index])" class="del-file">
+        <ui-icon type="del" ctrl />
+      </span>
+      <MetasMange
+        :hot="data"
+        @delete="delMetaHandler"
+        :index="index"
+        @change="(i: number) => (index = i)"
+      />
+    </template>
+  </ui-input>
+  <div v-else-if="data.type === 'WEB'" class="webview">
+    <MetasMange
+      :hot="data"
+      @delete="delMetaHandler"
+      v-if="data.images && data.images.length"
+    />
+    <p v-else>网页展示区</p>
+    <ui-input
+      placeholder="https://"
+      type="text"
+      v-model="link"
+      width="100%"
+      class="link-input"
+    >
+      <template v-slot:icon>
+        <span class="link-enter fun-ctrl" @click="enterLink">
+          <ui-icon type="checkbox" color="rgba(0, 0, 0, 0.7)" />
+        </span>
+      </template>
+    </ui-input>
+  </div>
+</template>
+
+<script setup lang="ts">
+import MetasMange from "./metas-mange.vue";
+import { defineProps, ref, defineEmits, watchEffect, watch, reactive } from "vue";
+import { Tagging } from "@/store";
+import { normalizeLink } from "@/utils";
+import { Dialog } from "bill/expose-common";
+import { custom } from "./custom";
+
+type Meta = Tagging["images"][number];
+type Metas = Meta[];
+
+const props = defineProps<{
+  data: Tagging;
+}>();
+
+const meta: Metas = reactive([]);
+watch(
+  () => props.data.type,
+  () => {
+    meta.length = 0;
+    meta.push(...props.data.images);
+  }
+);
+
+const link = ref<string>("");
+const emit = defineEmits<{
+  (e: "change", data: Metas): void;
+}>();
+
+watchEffect(() => {
+  if (props.data.type === "WEB") {
+    link.value = meta[0] ? (meta[0] as string) : "";
+  }
+});
+
+const enterLink = () => {
+  link.value = normalizeLink(link.value);
+  emit("change", [link.value]);
+};
+
+const delMetaHandler = async (file: Tagging["images"][number]) => {
+  const index = props.data.images.indexOf(file);
+  if (~index && (await Dialog.confirm(`确定要删除此数据吗?`))) {
+    const meta = [...props.data.images];
+    meta.splice(index, 1);
+    emit("change", meta);
+  }
+};
+
+type LocalImageFile = { file: File; preview: string } | Tagging["images"][number];
+const fileChange = (file: LocalImageFile | LocalImageFile[]) => {
+  const files = Array.isArray(file) ? file : [file];
+  const data = files.map((atom) => {
+    if (typeof atom === "string" || "blob" in atom) {
+      return atom;
+    } else {
+      return {
+        blob: atom.file,
+        url: atom.preview,
+      };
+    }
+  });
+  emit("change", data);
+};
+
+const index = ref(0);
+watch(
+  () => {
+    return {
+      type: props.data.type,
+      meta: meta,
+    };
+  },
+  (newv, oldv) => {
+    if (newv.type !== oldv.type) {
+      index.value = 0;
+    } else if (newv.meta.length > oldv.meta.length || index.value >= newv.meta.length) {
+      index.value = newv.meta.length - 1;
+    }
+  }
+);
+</script>
+
+<style lang="sass" scoped>
+@import './style.scss'
+</style>
+
+<style>
+.link-input input {
+  border: none !important;
+  background: linear-gradient(
+    180deg,
+    rgba(0, 0, 0, 0.25) 0%,
+    rgba(0, 0, 0, 0.5) 100%
+  ) !important;
+  border-radius: 0 !important;
+  color: #ffffff !important;
+}
+
+.link-input input::placeholder {
+  color: rgba(255, 255, 255, 0.7) !important;
+}
+
+.link-input .input {
+  border-radius: 0 !important;
+}
+</style>

+ 413 - 0
src/components/tagging/metas/style.scss

@@ -0,0 +1,413 @@
+.mobile-bubble {
+  position: absolute;
+  z-index: 299;
+  width: 100vw;
+  max-width: 300px;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  padding: 20px;
+  background: rgba(27, 27, 28, 0.8);
+  // -webkit-backdrop-filter: blur(4px);
+  // backdrop-filter: blur(4px);
+  border-radius: 6px;
+}
+
+.hot-full {
+  padding: 60px 20px 20px;
+  background: rgba(27, 27, 28, 0.8);
+  backdrop-filter: blur(4px);
+  position: absolute;
+  z-index: 9998;
+  width: 100vw;
+  height: 100vh;
+  overflow-y: auto;
+  left: 0;
+  top: 0;
+
+  .audio {
+    position: absolute;
+    top: -40px;
+    left: 10px;
+    z-index: 1;
+    display: inline-block;
+  }
+
+  .close-trl {
+    position: absolute;
+    width: 50px;
+    height: 50px;
+    right: 0;
+    top: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+
+.hot-content-layout {
+  position: relative;
+
+  .audio {
+    --colors-primary-base: #fff;
+    height: 16px;
+    overflow: hidden;
+    display: inline-block;
+  }
+  h2 {
+    font-size: 20px;
+    margin-bottom: 20px;
+    line-height: 1.5em;
+    word-wrap: break-word;
+    color: #ffffff;
+    position: relative;
+
+  }
+
+  .content {
+    font-size: 14px;
+    font-family: MicrosoftYaHei;
+    color: #999999;
+    line-height: 1.5em;
+    margin-top: 10px;
+    word-break: break-all;
+    overflow:hidden;
+  }
+
+  .meta {
+    max-width: 100%;
+    border-radius: 4px;
+    margin-top: 20px;
+  }
+
+  .deteil {
+    text-align: right;
+    margin-top: 20px;
+    font-size: 13px;
+  }
+
+  &.sam {
+    overflow: initial;
+    h2, .content {
+      text-overflow:ellipsis; 
+      display:-webkit-box;
+      -webkit-box-orient:vertical; 
+      overflow: hidden;
+    }
+    h2 {
+      -webkit-line-clamp:2;
+    }
+    .content {
+      font-size: 14px;
+      -webkit-line-clamp:3;
+    }
+    .close-trl {
+      position: absolute;
+      bottom: 0;
+      margin-bottom: -100px;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 40px;
+      height: 40px;
+      border-radius: 50%;
+      text-align: center;
+      line-height: 40px;
+      background: rgba(0,0,0,0.8);
+    }
+  
+  }
+}
+
+.hot-item {
+  position: absolute;
+  cursor: pointer;
+
+  > img {
+    width: 32px;
+    height: 32px;
+  }
+
+  .hot-bubble {
+    cursor: initial;
+
+    &.pc {
+      width: 400px;
+    }
+    &:not(.pc) {
+      width: 80vw;
+      --bottom-left: 40vw;
+    }
+  }
+
+  &.active,
+  &:hover {
+    z-index: 3;
+  }
+}
+
+.mates {
+  width: 100%;
+  height: 100%;
+  max-height: 100%;
+  overflow-y: auto;
+
+  .meta-item {
+    width: 100%;
+    height: 100%;
+
+    &.full {
+      cursor: zoom-in;
+    }
+  }
+
+  .iframe {
+    width: 100%;
+    height: 100%;
+    position: relative;
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 0;
+      top: 0;
+      right: 0;
+      left: 0;
+      z-index: 2;
+    }
+  }
+
+  iframe,
+  video,
+  img {
+    width: 100%;
+    height: 203px;
+    object-fit: cover;
+  }
+  video,
+  img {
+    object-fit: cover;
+  }
+  iframe {
+    border: none;
+  }
+
+  .file-mange {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    span {
+      display: block;
+      width: 24px;
+      height: 24px;
+      background-color: rgba(0, 0, 0, 0.3);
+      font-size: 14px;
+      color: rgba(255, 255, 255, 0.7);
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+
+      &:not(:last-child) {
+        margin-bottom: 10px;
+      }
+    }
+  }
+}
+
+.close {
+  right: 0;
+  top: 0;
+  height: 25px;
+  position: absolute;
+  font-size: 24px;
+  color: #fff;
+  cursor: pointer;
+  width: 50px;
+  height: 50px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 99999;
+}
+
+.pull-hot {
+  position: absolute;
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  background-color: rgba(0, 0, 0, 0.1);
+  backdrop-filter: blur(1px);
+
+  &:not(.pc) {
+    .hot-layer {
+      padding-top: 40px;
+    }
+  }
+
+  &.pc {
+    .hot-layer {
+      padding: 40px 20px 20px;
+    }
+  }
+
+  .hot-layer {
+    flex: 1;
+    background-color: rgba(0, 0, 0, 0.7);
+    color: #fff;
+    height: 100%;
+    position: relative;
+    display: flex;
+    flex-direction: column;
+
+    h3 {
+      font-size: 20px;
+      font-weight: 700;
+      letter-spacing: 1px;
+      margin-bottom: 10px;
+      word-break: break-all;
+    }
+
+    .pull-meta {
+      height: 100%;
+      width: 100%;
+      overflow-y: auto;
+      flex: 1;
+
+      .content {
+        margin-bottom: 10px;
+        font-size: 16px;
+        font-weight: 400;
+        line-height: 26px;
+        color: #ccc;
+        word-break: break-all;
+        letter-spacing: 1px;
+      }
+
+      iframe,
+      video,
+      img {
+        width: 100%;
+        height: 100%;
+        display: block;
+      }
+
+      video,
+      img {
+        object-fit: contain;
+      }
+
+      iframe {
+        border: none;
+        height: 100%;
+      }
+    }
+  }
+}
+
+.edit-hot {
+  margin-top: 20px;
+  text-align: right;
+
+  span {
+    font-size: 14px;
+    color: rgba(255, 255, 255, 0.7);
+    cursor: pointer;
+  }
+}
+
+.full-img {
+  height: 100%;
+  width: 100%;
+  position: relative;
+}
+
+.full-img img {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  left: 0;
+  top: 0;
+}
+
+
+.radio-group {
+  display: flex;
+  align-items: center;
+
+  span {
+    display: inline-flex;
+    align-items: center;
+  }
+}
+
+.del-file {
+  position: absolute;
+  right: 10px;
+  top: 10px;
+  display: block;
+  width: 24px;
+  height: 24px;
+  background-color: rgba(0, 0, 0, 0.3);
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.7);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 222;
+  cursor: pointer;
+
+  &:not(:last-child) {
+    margin-bottom: 10px;
+  }
+}
+
+
+
+.webview {
+  height: 225px;
+  background: rgba(255, 255, 255, 0.1);
+  border-radius: 4px;
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  position: relative;
+  display: flex;
+  align-items: center;
+  margin-bottom: 30px;
+  justify-content: center;
+  overflow: hidden;
+
+  p {
+    color: rgba(255, 255, 255, 0.3);
+    font-size: 16px;
+    font-weight: bold;
+  }
+
+  .link-input {
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    input {
+      border: none;
+    }
+  }
+
+  .link-enter {
+    width: 16px;
+    height: 16px;
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.7);
+    color: rgba(0, 0, 0, 0.7);
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+  }
+}

+ 8 - 9
src/components/tagging/sign.vue

@@ -19,7 +19,7 @@
       <UIBubble class="hot-bubble pc" :show="showContent" type="left" level="center">
         <h2>{{ tagging.title }}</h2>
         <div class="content">
-          <p><span>特征描述:</span>{{ tagging.desc }}</p>
+          <p><span>描述:</span>{{ tagging.desc }}</p>
           <p><span>遗留部位:</span>{{ tagging.part }}</p>
           <p><span>提取方法:</span>{{ tagging.method }}</p>
           <p><span>类别:</span>{{ tagging.mtype }}</p>
@@ -28,11 +28,9 @@
           <p><span>提取时间:</span>{{ tagging.time }}</p>
           <p><span>提取人:</span>{{ tagging.principal }}</p>
         </div>
-        <Images
-          :tagging="tagging"
-          :in-full="true"
-          @pull="(index) => (pullIndex = index)"
-        />
+
+        <Metas :hot="tagging" @pull="(index) => (pullIndex = index)" :in-full="true" />
+
         <div
           class="edit-hot"
           v-if="router.currentRoute.value.name === RoutesName.tagging"
@@ -47,7 +45,7 @@
       <Preview
         @close="pullIndex = -1"
         :current="pullIndex"
-        :items="queryItems"
+        :items="queryItems as any"
         v-if="!!~pullIndex"
       />
     </div>
@@ -59,6 +57,7 @@ import { computed, ref, watchEffect, watch, onUnmounted } from "vue";
 import { router, RoutesName } from "@/router";
 import UIBubble from "bill/components/bubble/index.vue";
 import Images from "@/views/tagging/images.vue";
+import Metas from "./metas/metas-mange.vue";
 import Preview, { MediaType } from "../static-preview/index.vue";
 import { getTaggingStyle, getFuseModel } from "@/store";
 import { getFileUrl } from "@/utils";
@@ -113,8 +112,8 @@ const pullIndex = ref(-1);
 const isHover = ref(false);
 const queryItems = computed(() =>
   props.tagging.images.map((image) => ({
-    type: MediaType.img,
-    url: getResource(getFileUrl(image)),
+    type: props.tagging.type,
+    url: image,
   }))
 );
 

+ 1 - 1
src/model/app.vue

@@ -191,8 +191,8 @@ export default Model;
   right: calc(var(--editor-menu-right) + var(--editor-toolbox-width)) !important;
   bottom: 0;
   width: 320px;
+  display: none;
   height: 200px;
-  background: red;
   z-index: 99;
 }
 </style>

+ 27 - 0
src/sdk/association.ts

@@ -436,19 +436,46 @@ export const setupAssociation = (mountEl: HTMLDivElement) => {
 
 
 export const setBackdrop = (back: string, type: SettingResourceType) => {
+  console.error('????')
+  ;(document.querySelector('#scene-map') as HTMLDivElement)!.style.display =  'none';
   if (type === SettingResourceType.map) {
     if (!caseProject.value!.tmProject?.latlng) {
       return;
     }
     
     const latlng = caseProject.value!.tmProject?.latlng.split(',').map(i => Number(i))
+    ;(document.querySelector('#scene-map') as HTMLDivElement)!.style.display =  'block';
     sdk.enableMap && sdk.enableMap(document.querySelector('#scene-map') as HTMLDivElement, latlng)
     sdk.switchMapType && sdk.switchMapType(back)
     // 'satellite' | 'standard'
 
+    setMap(setting.value!.mapOpen, setting.value!.mapType)
   } else if (type!== SettingResourceType.icon) {
     setting.value?.back && sdk.setBackdrop(back, type)
   } else {
     sdk.setBackdrop('none', type)
   }
+}
+
+let opened = false
+export const setMap = (open: boolean, type:string = 'satellite') => {
+  if (!caseProject.value!.tmProject?.latlng) {
+    ;(document.querySelector('#scene-map') as HTMLDivElement)!.style.display =  'none';
+    return;
+  }
+
+  if (open) {
+    ;(document.querySelector('#scene-map') as HTMLDivElement)!.style.display =  'block';  
+    console.error('????', open, caseProject.value!.tmProject?.latlng)
+  } else {
+    ;(document.querySelector('#scene-map') as HTMLDivElement)!.style.display =  'none';
+  }
+
+  if (!opened) {
+    const latlng = caseProject.value!.tmProject?.latlng.split(',').map(i => Number(i))
+    sdk.enableMap && sdk.enableMap(document.querySelector('#scene-map') as HTMLDivElement, latlng)
+    opened = true
+  }
+
+  sdk.switchMapType && sdk.switchMapType(type)
 }

+ 5 - 0
src/store/tagging.ts

@@ -55,7 +55,12 @@ export const createTagging = (tagging: Partial<Tagging> = {}): Tagging => {
     part: '',
     method: '',
     principal: '',
+    time: '',
     images: [],
+    mtype: '',
+    cat: '',
+    tsms: '',
+    type: 'IMAGE',
     ...tagging
   }
 }

+ 7 - 0
src/utils/index.ts

@@ -66,6 +66,13 @@ export const round = (num: number, index: number = 2) => {
   const s = Math.pow(10, index)
   return Math.round(num * s) / s
 }
+export const normalizeLink = (link: string): string => {
+  if (!link.includes('//')) {
+    return location.protocol + '//' + link
+  } else {
+    return link
+  }
+}
 
 export * from './store-help'
 export * from "./stack";

+ 41 - 11
src/views/setting/index.vue

@@ -14,11 +14,33 @@
         <ui-icon
           ctrl
           :type="setting?.openCompass ? 'eye-s' : 'eye-n'"
-          @click="changeBack(setting!.back, setting!.backType, !setting!.openCompass)"
+          @click="changeBack(setting!.back, setting!.backType, !setting!.openCompass, setting!.mapOpen, setting!.mapType)"
         />
       </template>
     </ui-group>
 
+    <ui-group title="地图" v-if="caseProject!.tmProject?.latlng">
+      <template #icon>
+        <ui-icon
+          ctrl
+          :type="setting?.mapOpen ? 'eye-s' : 'eye-n'"
+          @click="changeBack(setting!.back, setting!.backType, setting!.openCompass, !setting!.mapOpen, setting!.mapType)"
+        />
+      </template>
+      <ui-group-option v-if="setting?.mapOpen">
+        <ui-input
+          type="select"
+          width="100%"
+          :options="[
+            { label: '卫星地图', value: 'satellite' },
+            { label: '矢量地图', value: 'standard' },
+          ]"
+          :modelValue="setting!.mapType"
+          @update:modelValue="(e: string )=> changeBack(setting!.back, setting!.backType, setting!.openCompass, setting!.mapOpen, e)"
+        />
+      </ui-group-option>
+    </ui-group>
+
     <ui-group title="设置背景">
       <ui-group-option>
         <div class="back-layout">
@@ -27,7 +49,7 @@
             :key="back.resource"
             class="back-item"
             :class="{ [back.backType]: true, active: setting!.back === back.resource }"
-            @click="setting!.back !== back.resource && changeBack(back.resource, back.backType, setting!.openCompass)"
+            @click="setting!.back !== back.resource && changeBack(back.resource, back.backType, setting!.openCompass, setting!.mapOpen, setting!.mapType)"
           >
             <img :src="back.resource" v-if="back.backType === 'img'" />
             <i
@@ -118,7 +140,7 @@ import {
 import { ref } from "vue";
 import { togetherCallback, getFileUrl, loadPack } from "@/utils";
 import { showRightPanoStack, showRightCtrlPanoStack } from "@/env";
-import { sdk, setBackdrop } from "@/sdk";
+import { sdk, setBackdrop, setMap } from "@/sdk";
 import {
   fetchSettingResources,
   settingResources,
@@ -154,8 +176,16 @@ const enterSetPic = () => {
 const initBack = setting.value!.back;
 const initType = setting.value!.backType;
 const initOpenCompass = setting.value!.openCompass;
+const initopenMap = setting.value!.mapOpen;
+const initmapType = setting.value!.mapType;
 let isFirst = true;
-const changeBack = (back: string, type: SettingResourceType, openCompass: boolean) => {
+const changeBack = (
+  back: string,
+  type: SettingResourceType,
+  openCompass: boolean,
+  openMap: boolean,
+  mapType: string
+) => {
   if (type === SettingResourceType.map && !caseProject.value!.tmProject?.latlng) {
     Dialog.alert("当前案件没绑定经纬度,无法开启地图功能");
     return;
@@ -164,8 +194,12 @@ const changeBack = (back: string, type: SettingResourceType, openCompass: boolea
   setting.value!.back = back;
   setting.value!.backType = type;
   setting.value!.openCompass = openCompass;
+  setting.value!.mapOpen = openMap;
+  setting.value!.mapType = mapType;
 
   setBackdrop(back, type);
+  setMap(openMap, mapType);
+
   (document.querySelector("#direction") as HTMLDivElement)!.style.display = openCompass
     ? "block"
     : "none";
@@ -178,18 +212,14 @@ const changeBack = (back: string, type: SettingResourceType, openCompass: boolea
         setting.value!.back = initBack;
         setting.value!.backType = initType;
         setting.value!.openCompass = initOpenCompass;
+        setting.value!.mapOpen = initopenMap;
+        setting.value!.mapType = initmapType;
 
         setBackdrop(initBack, initType);
+        setMap(initopenMap, initmapType);
         (document.querySelector(
           "#direction"
         ) as HTMLDivElement)!.style.display = initOpenCompass ? "block" : "none";
-
-        if (setting.value?.backType !== SettingResourceType.icon) {
-          setting.value?.back &&
-            sdk.setBackdrop(setting.value.back, setting.value.backType);
-        } else {
-          sdk.setBackdrop("none", setting.value.backType);
-        }
       }
       isFirst = true;
     });

+ 12 - 2
src/views/tagging/edit.vue

@@ -100,7 +100,13 @@
       >
         <template #preIcon><span>提取人:</span></template>
       </ui-input>
-      <ui-input
+
+      <MediaManage :data="tagging" @change="data => {
+        tagging.type = data.type
+        tagging.images = data.images
+      }" />
+      
+      <!-- <ui-input
         class="input"
         type="file"
         width="100%"
@@ -126,7 +132,9 @@
             </template>
           </Images>
         </template>
-      </ui-input>
+      </ui-input> -->
+
+
       <div class="edit-hot">
         <a @click="submitHandler">
           <ui-icon type="nav-edit" />
@@ -140,8 +148,10 @@
 <script lang="ts" setup>
 import StylesManage from "./styles.vue";
 import Images from "./images.vue";
+import MediaManage from '@/components/tagging/metas/metas-edit.vue'
 import { computed, ref, watchEffect } from "vue";
 import { Dialog, Message } from "bill/index";
+
 import {
   taggingStyles,
   Tagging,

+ 70 - 70
src/views/tagging/sign.vue

@@ -1,33 +1,33 @@
 <template>
-  <ui-group-option 
-    class="sign-tagging" 
-    :class="{active: selected, edit}" 
+  <ui-group-option
+    class="sign-tagging"
+    :class="{ active: selected, edit }"
     @click="edit && getTaggingIsShow(tagging) && emit('select', true)"
   >
     <div class="info">
-      <img 
-        :src="getResource(getFileUrl(tagging.images[0]))" 
-        v-if="tagging.images.length"
-      >
+      <img
+        :src="getResource(getFileUrl(tagging.images[0]))"
+        v-if="tagging.images.length && tagging.type === 'IMAGE'"
+      />
       <div>
         <p>{{ tagging.title }}</p>
         <span>放置:{{ positions.length }}</span>
       </div>
     </div>
     <div class="actions" @click.stop>
-      <ui-icon 
-         v-if="!edit"
-        type="pin" 
-        ctrl 
-        @click.stop="$emit('select', true)" 
+      <ui-icon
+        v-if="!edit"
+        type="pin"
+        ctrl
+        @click.stop="$emit('select', true)"
         :class="{ disabled: !getTaggingIsShow(tagging) }"
       />
       <template v-else>
         <ui-icon type="pin1" ctrl @click.stop="$emit('fixed')" tip="放置" />
-        <ui-more 
-          :options="menus" 
-          style="margin-left: 20px" 
-          @click="(action: keyof typeof actions) => actions[action]()" 
+        <ui-more
+          :options="menus"
+          style="margin-left: 20px"
+          @click="(action: keyof typeof actions) => actions[action]()"
         />
       </template>
     </div>
@@ -35,88 +35,88 @@
 </template>
 
 <script setup lang="ts">
-import { getFileUrl } from '@/utils'
-import { computed, ref, watchEffect, nextTick } from 'vue';
-import { getResource, showTaggingPositionsStack } from '@/env'
-import { sdk } from '@/sdk'
-import { 
-  getTaggingStyle, 
-  getTaggingPositions, 
+import { getFileUrl } from "@/utils";
+import { computed, ref, watchEffect, nextTick } from "vue";
+import { getResource, showTaggingPositionsStack } from "@/env";
+import { sdk } from "@/sdk";
+import {
+  getTaggingStyle,
+  getTaggingPositions,
   getFuseModel,
   getFuseModelShowVariable,
-  getTaggingIsShow
-} from '@/store'
+  getTaggingIsShow,
+} from "@/store";
 
-import type { Tagging } from '@/store'
+import type { Tagging } from "@/store";
 
 const props = withDefaults(
-  defineProps<{ tagging: Tagging, selected?: boolean, edit?: boolean }>(),
+  defineProps<{ tagging: Tagging; selected?: boolean; edit?: boolean }>(),
   { edit: true }
-)
-const style = computed(() => getTaggingStyle(props.tagging.styleId))
-const positions = computed(() => getTaggingPositions(props.tagging))
+);
+const style = computed(() => getTaggingStyle(props.tagging.styleId));
+const positions = computed(() => getTaggingPositions(props.tagging));
 
-const emit = defineEmits<{ 
-  (e: 'delete'): void 
-  (e: 'edit'): void
-  (e: 'select', selected: boolean): void
-  (e: 'fixed'): void
-}>()
+const emit = defineEmits<{
+  (e: "delete"): void;
+  (e: "edit"): void;
+  (e: "select", selected: boolean): void;
+  (e: "fixed"): void;
+}>();
 
 const menus = [
-  { label: '编辑', value: 'edit' },
-  { label: '删除', value: 'delete' },
-]
+  { label: "编辑", value: "edit" },
+  { label: "删除", value: "delete" },
+];
 const actions = {
-  edit: () => emit('edit'),
-  delete: () => emit('delete')
-}
+  edit: () => emit("edit"),
+  delete: () => emit("delete"),
+};
 
 const flyTaggingPositions = (tagging: Tagging, callback?: () => void) => {
-  const positions = getTaggingPositions(tagging)
+  const positions = getTaggingPositions(tagging);
 
-  let isStop = false
+  let isStop = false;
   const flyIndex = (i: number) => {
     if (isStop || i >= positions.length) {
-      callback && nextTick(callback)
+      callback && nextTick(callback);
       return;
     }
-    const position = positions[i]
-    const model = getFuseModel(position.modelId)
+    const position = positions[i];
+    const model = getFuseModel(position.modelId);
     if (!model || !getFuseModelShowVariable(model).value) {
-      flyIndex(i + 1)
+      flyIndex(i + 1);
       return;
     }
 
-    const pop = showTaggingPositionsStack.push(ref(new WeakSet([position])))
-    sdk.comeTo({ 
-      position: position.localPos, 
+    const pop = showTaggingPositionsStack.push(ref(new WeakSet([position])));
+    sdk.comeTo({
+      position: position.localPos,
       modelId: position.modelId,
       dur: 300,
-      distance: 3
-    })
-    
+      distance: 3,
+    });
+
     setTimeout(() => {
-      pop()
-      flyIndex(i + 1)
-    }, 2000)
-  }
-  flyIndex(0)
-  return () => isStop = true
-}
+      pop();
+      flyIndex(i + 1);
+    }, 2000);
+  };
+  flyIndex(0);
+  return () => (isStop = true);
+};
 watchEffect((onCleanup) => {
   if (props.selected) {
-    const success = () => emit('select', false)
-    const stop = flyTaggingPositions(props.tagging, success)
-    const keyupHandler = (ev: KeyboardEvent) => ev.code === 'Escape' && success()
+    const success = () => emit("select", false);
+    const stop = flyTaggingPositions(props.tagging, success);
+    const keyupHandler = (ev: KeyboardEvent) => ev.code === "Escape" && success();
 
-    document.documentElement.addEventListener('keyup', keyupHandler, false)
+    document.documentElement.addEventListener("keyup", keyupHandler, false);
     onCleanup(() => {
-      stop()
-      document.documentElement.removeEventListener('keyup', keyupHandler, false)
-    })
+      stop();
+      document.documentElement.removeEventListener("keyup", keyupHandler, false);
+    });
   }
-})
+});
 </script>
 
-<style lang="scss" scoped src="./style.scss"></style>
+<style lang="scss" scoped src="./style.scss"></style>