bill преди 3 години
родител
ревизия
d1540b897d
променени са 33 файла, в които са добавени 1252 реда и са изтрити 46 реда
  1. 7 1
      src/api/constant.ts
  2. 3 1
      src/api/index.ts
  3. 14 0
      src/api/tagging-style.ts
  4. 19 0
      src/api/tagging.ts
  5. 1 0
      src/components/bill-ui/assets/scss/_components.scss
  6. 47 0
      src/components/bill-ui/assets/scss/components/_more.scss
  7. 2 1
      src/components/bill-ui/components/floating/index.vue
  8. 118 3
      src/components/bill-ui/components/icon/iconfont/demo_index.html
  9. 23 3
      src/components/bill-ui/components/icon/iconfont/iconfont.css
  10. 1 1
      src/components/bill-ui/components/icon/iconfont/iconfont.js
  11. 35 0
      src/components/bill-ui/components/icon/iconfont/iconfont.json
  12. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.ttf
  13. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.woff
  14. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.woff2
  15. 6 2
      src/components/bill-ui/components/icon/index.vue
  16. 0 1
      src/components/bill-ui/components/input/richtext.vue
  17. 73 0
      src/components/bill-ui/components/more/index.vue
  18. 4 2
      src/components/bill-ui/expose-common.js
  19. 37 0
      src/components/bill-ui/hook/useFocus.js
  20. 7 2
      src/layout/main.vue
  21. 9 2
      src/store/index.ts
  22. 56 0
      src/store/tagging.ts
  23. 24 0
      src/store/taging-style.ts
  24. 0 1
      src/style.scss
  25. 10 10
      src/views/merge/index.vue
  26. 0 14
      src/views/query/index.vue
  27. 247 0
      src/views/tagging/edit.vue
  28. 119 0
      src/views/tagging/images.vue
  29. 33 1
      src/views/tagging/index.vue
  30. 42 0
      src/views/tagging/sign.vue
  31. 38 0
      src/views/tagging/style.scss
  32. 274 0
      src/views/tagging/styles.vue
  33. 3 1
      src/vite-env.d.ts

+ 7 - 1
src/api/constant.ts

@@ -14,4 +14,10 @@ export const UPLOAD_HEADS = {
 
 
 // 模型列表
-export const MODEL_LIST = ''
+export const MODEL_LIST = ''
+
+// 标注列表
+export const TAGGING_LIST = ''
+
+// 标注样式类型列表
+export const TAGGING_STYLE_LIST = ''

+ 3 - 1
src/api/index.ts

@@ -4,4 +4,6 @@ export { ResCode } from './constant'
 export type { ResData } from './setup'
 
 export * from './instance'
-export * from './model'
+export * from './model'
+export * from './tagging'
+export * from './tagging-style'

+ 14 - 0
src/api/tagging-style.ts

@@ -0,0 +1,14 @@
+import axios from './instance'
+import { TAGGING_STYLE_LIST } from './constant'
+
+export interface TaggingStyle {
+  id: string
+  icon: string
+  name: string
+  default: boolean
+}
+
+export type TaggingStyles = TaggingStyle[]
+
+export const getTaggingStyles = () => 
+  axios.post<TaggingStyles>(TAGGING_STYLE_LIST)

+ 19 - 0
src/api/tagging.ts

@@ -0,0 +1,19 @@
+import axios from './instance'
+import { TAGGING_LIST } from './constant'
+
+export interface Tagging {
+  id: string
+  styleId: string,
+  title: string,
+  desc: string
+  part: string
+  method: string
+  principal: string
+  images: string[],
+  positions: ScenePos[]
+}
+
+export type Taggings = Tagging[]
+
+export const getTaggings = () => 
+  axios.post<Taggings>(TAGGING_LIST)

+ 1 - 0
src/components/bill-ui/assets/scss/_components.scss

@@ -17,4 +17,5 @@
 @import "components/bubble";
 @import "components/guide";
 @import "components/tip";
+@import "components/more";
 

+ 47 - 0
src/components/bill-ui/assets/scss/components/_more.scss

@@ -0,0 +1,47 @@
+.ui-more {
+  display: inline-block;
+  cursor: pointer;
+}
+
+.more-float {
+  transition: transform .3s ease,
+    opacity .3s ease;
+  margin-top: 9px;
+  box-shadow: inset 0 0 1px rgb(255 255 255 / 90%);
+  background: rgba(27, 27, 28, 0.8);
+  border-radius: 4px;
+  border: 1px solid #000000;
+  backdrop-filter: blur(4px);
+  padding: 9px 0; 
+
+  .option {
+    padding: 5px 16px; 
+    color: #fff; 
+    font-size: 14px;
+    white-space: nowrap;
+
+
+    &.active {
+      background: var(--colors-normal-back);
+      color: var(--colors-primary-base);;        
+    }
+
+    &:not(.active):hover {
+      cursor: pointer;
+      background-color: var(--colors-primary-base);
+    }
+  }
+
+
+  transform-origin: center top;
+
+  &:not(.show) {
+    transform: translateY(0) translateX(-100%) scale(1, 0);
+    opacity: 0;
+  }
+
+  &.show {
+    transform: translateY(0) translateX(-100%) scale(1, 1);
+    opacity: 1;
+  }
+}

+ 2 - 1
src/components/bill-ui/components/floating/index.vue

@@ -7,7 +7,7 @@
 </template>
 
 <script setup>
-import { defineProps, defineExpose, onUnmounted, reactive, watch, computed, onUpdated, onActivated, ref } from 'vue'
+import { defineProps, defineExpose, onUnmounted, reactive, watch, computed, onUpdated, onActivated, ref, watchEffect } from 'vue'
 import { getPostionByTarget, getScrollParents, getZIndex } from '../../utils'
 
 const Horizontal = {
@@ -172,6 +172,7 @@ onActivated(() => {
 })
 
 defineExpose({
+    vmRef,
     updateLocation,
 })
 </script>

+ 118 - 3
src/components/bill-ui/components/icon/iconfont/demo_index.html

@@ -55,6 +55,36 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe63b;</span>
+                <div class="name">video</div>
+                <div class="code-name">&amp;#xe63b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe600;</span>
+                <div class="name">more read</div>
+                <div class="code-name">&amp;#xe600;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe63a;</span>
+                <div class="name">preview</div>
+                <div class="code-name">&amp;#xe63a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6dc;</span>
+                <div class="name">nav-record</div>
+                <div class="code-name">&amp;#xe6dc;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6dd;</span>
+                <div class="name">order</div>
+                <div class="code-name">&amp;#xe6dd;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe6d9;</span>
                 <div class="name">point-s</div>
                 <div class="code-name">&amp;#xe6d9;</div>
@@ -786,9 +816,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1658474856187') format('woff2'),
-       url('iconfont.woff?t=1658474856187') format('woff'),
-       url('iconfont.ttf?t=1658474856187') format('truetype');
+  src: url('iconfont.woff2?t=1660124061824') format('woff2'),
+       url('iconfont.woff?t=1660124061824') format('woff'),
+       url('iconfont.ttf?t=1660124061824') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -815,6 +845,51 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-video1"></span>
+            <div class="name">
+              video
+            </div>
+            <div class="code-name">.icon-video1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-more"></span>
+            <div class="name">
+              more read
+            </div>
+            <div class="code-name">.icon-more
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-preview"></span>
+            <div class="name">
+              preview
+            </div>
+            <div class="code-name">.icon-preview
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-nav-record"></span>
+            <div class="name">
+              nav-record
+            </div>
+            <div class="code-name">.icon-nav-record
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-order"></span>
+            <div class="name">
+              order
+            </div>
+            <div class="code-name">.icon-order
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-point-s"></span>
             <div class="name">
               point-s
@@ -1914,6 +1989,46 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-video1"></use>
+                </svg>
+                <div class="name">video</div>
+                <div class="code-name">#icon-video1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-more"></use>
+                </svg>
+                <div class="name">more read</div>
+                <div class="code-name">#icon-more</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-preview"></use>
+                </svg>
+                <div class="name">preview</div>
+                <div class="code-name">#icon-preview</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-nav-record"></use>
+                </svg>
+                <div class="name">nav-record</div>
+                <div class="code-name">#icon-nav-record</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-order"></use>
+                </svg>
+                <div class="name">order</div>
+                <div class="code-name">#icon-order</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-point-s"></use>
                 </svg>
                 <div class="name">point-s</div>

+ 23 - 3
src/components/bill-ui/components/icon/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 2930899 */
-  src: url('iconfont.woff2?t=1658474856187') format('woff2'),
-       url('iconfont.woff?t=1658474856187') format('woff'),
-       url('iconfont.ttf?t=1658474856187') format('truetype');
+  src: url('iconfont.woff2?t=1660124061824') format('woff2'),
+       url('iconfont.woff?t=1660124061824') format('woff'),
+       url('iconfont.ttf?t=1660124061824') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,26 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-video1:before {
+  content: "\e63b";
+}
+
+.icon-more:before {
+  content: "\e600";
+}
+
+.icon-preview:before {
+  content: "\e63a";
+}
+
+.icon-nav-record:before {
+  content: "\e6dc";
+}
+
+.icon-order:before {
+  content: "\e6dd";
+}
+
 .icon-point-s:before {
   content: "\e6d9";
 }

Файловите разлики са ограничени, защото са твърде много
+ 1 - 1
src/components/bill-ui/components/icon/iconfont/iconfont.js


+ 35 - 0
src/components/bill-ui/components/icon/iconfont/iconfont.json

@@ -6,6 +6,41 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "23781429",
+      "name": "video",
+      "font_class": "video1",
+      "unicode": "e63b",
+      "unicode_decimal": 58939
+    },
+    {
+      "icon_id": "11304931",
+      "name": "more read",
+      "font_class": "more",
+      "unicode": "e600",
+      "unicode_decimal": 58880
+    },
+    {
+      "icon_id": "23773344",
+      "name": "preview",
+      "font_class": "preview",
+      "unicode": "e63a",
+      "unicode_decimal": 58938
+    },
+    {
+      "icon_id": "30948139",
+      "name": "nav-record",
+      "font_class": "nav-record",
+      "unicode": "e6dc",
+      "unicode_decimal": 59100
+    },
+    {
+      "icon_id": "30948192",
+      "name": "order",
+      "font_class": "order",
+      "unicode": "e6dd",
+      "unicode_decimal": 59101
+    },
+    {
       "icon_id": "30787879",
       "name": "point-s",
       "font_class": "point-s",

BIN
src/components/bill-ui/components/icon/iconfont/iconfont.ttf


BIN
src/components/bill-ui/components/icon/iconfont/iconfont.woff


BIN
src/components/bill-ui/components/icon/iconfont/iconfont.woff2


+ 6 - 2
src/components/bill-ui/components/icon/index.vue

@@ -3,14 +3,14 @@
         <slot></slot>
         <p class="tip" v-if="tip && os.isPc && !os.isTablet">{{ tip }}</p>
     </Icon>
-    <i class="iconfont ui-kankan-icon icon" :class="className" :style="style" @click="ev => emit('click', ev)" v-else>
+    <i class="iconfont ui-kankan-icon icon" :class="className" :style="style" @click="ev => emit('click', ev)" ref="vm" v-else>
         <slot></slot>
         <p class="tip" v-if="tip && os.isPc && !os.isTablet">{{ tip }}</p>
     </i>
 </template>
 
 <script setup>
-import { defineProps, computed, defineEmits } from 'vue'
+import { defineProps, computed, defineEmits, ref, reactive, defineExpose } from 'vue'
 import { normalizeUnitToStyle, os } from '../../utils'
 import Icon from './icon/index.vue'
 import Tip from '../tip'
@@ -65,6 +65,10 @@ const className = computed(() => {
 })
 
 const emit = defineEmits(['click'])
+const vm = ref()
+defineExpose(reactive({
+    vm
+}))
 </script>
 
 <style>

+ 0 - 1
src/components/bill-ui/components/input/richtext.vue

@@ -103,7 +103,6 @@ let interval
 const focusHandler = ev => {
     clearInterval(interval)
     interval = setInterval(() => {
-        console.log(getCursortPosition())
         emit('updatePos', getCursortPosition())
     }, 100)
     emit('focus')

+ 73 - 0
src/components/bill-ui/components/more/index.vue

@@ -0,0 +1,73 @@
+<template>
+  <Icon class="ui-more" ref="vm" v-bind="attrs" type="more" ctrl />
+  <Floating
+    class="more-float"
+    :dire="dire"
+    :class="{show: showOption}"
+    :isTransform="isTransform"
+    :mount="mountel"
+    :refer="referVM"
+    ref="floatVM"
+  >
+    <div class="option" v-for="option in options" @click="clickHandler(option)">
+      {{ option.label }}
+    </div>
+  </Floating>
+</template>
+
+<script>
+import { defineComponent, computed, ref, watchEffect } from 'vue'
+import Floating from '../floating/index.vue'
+import useFocus from '../../hook/useFocus'
+import Icon from '../icon'
+
+export default defineComponent({
+  name: 'ui-more',
+  emits: {
+    'click': () => true
+  },
+  inheritAttrs: false,
+  props: {
+    isTransform: {
+      type: Boolean,
+      default: false
+    },
+    options: {
+      type: Array,
+      default: () => []
+    },
+    dire: {
+      type: String,
+      default: 'right-bottom'
+    }
+  },
+  setup(props, { attrs, emit }) {
+    const vm = ref()
+    const floatVM = ref()
+    const referVM = computed(() => vm.value?.vm)
+    const showOption = useFocus(
+      referVM,
+      computed(() => floatVM.value?.vmRef)
+    )
+    const clickHandler = (option) => {
+      showOption.value = false
+      emit('click', option.value)
+    }
+
+    return {
+      showOption,
+      floatVM,
+      vm,
+      attrs,
+      referVM,
+      mountel: document.body,
+      clickHandler
+    }
+  },
+  components: {
+    Floating,
+    Icon
+  }
+})
+
+</script>

+ 4 - 2
src/components/bill-ui/expose-common.js

@@ -23,6 +23,7 @@ import Cropper from './components/cropper'
 import Bubble from './components/bubble'
 import Guide from './components/guide'
 import Tip from './components/tip'
+import More from './components/more'
 
 const components = setup(
     DialogContent,
@@ -47,10 +48,11 @@ const components = setup(
     Audio,
     Bubble,
     Guide,
-    Tip
+    Tip,
+    More
 )
 
-export { DialogContent, Cropper, Message, Loading, Dialog, Tree, Button, Group, GroupOption, Input, Icon, MenuItem, Floating, Gate, GateContent, Slide, Audio, Bubble, Guide, Tip }
+export { More, DialogContent, Cropper, Message, Loading, Dialog, Tree, Button, Group, GroupOption, Input, Icon, MenuItem, Floating, Gate, GateContent, Slide, Audio, Bubble, Guide, Tip }
 
 export default function install(app) {
     components.forEach(component => component.install(app))

+ 37 - 0
src/components/bill-ui/hook/useFocus.js

@@ -0,0 +1,37 @@
+import { ref, watchEffect, onUnmounted } from 'vue'
+
+export const useFocus = (selfRef, modalRef = selfRef) => {
+  const focus = ref(false)
+  const checkDOM = document.body
+  
+  const clickChangeFocus = (ev) => {
+    modalRef.value.contains(ev.target) 
+      || (focus.value = false)
+  }
+  const selfClickHandler = () => focus.value = true
+
+  let cahceSelf = selfRef.value
+  watchEffect(() => {
+    
+    cahceSelf && cahceSelf.removeEventListener('click', selfClickHandler)
+    selfRef.value && selfRef.value.addEventListener('click', selfClickHandler)
+    cahceSelf = selfRef.value
+  })
+
+  watchEffect(() => {
+    if (modalRef.value && focus.value) {
+      checkDOM.addEventListener('mousedown', clickChangeFocus, { capture: true })
+    } else {
+      checkDOM.removeEventListener('mousedown', clickChangeFocus, { capture: true })
+    }
+  })
+
+  onUnmounted(() => {
+    cahceSelf && cahceSelf.removeEventListener('click', selfClickHandler)
+    checkDOM.removeEventListener('mousedown', clickChangeFocus)
+  })
+
+  return focus
+}
+
+export default useFocus

+ 7 - 2
src/layout/main.vue

@@ -49,6 +49,7 @@ const stopSdkInstalWatch = watchEffect(() => {
 <style scoped lang="scss">
 .editor-layout {
   --editor-menu-bottom: 0px;
+  --left-pano-left: calc(var(--editor-menu-left) + var(--editor-menu-width));
 }
 
 .sys-view-full {
@@ -63,7 +64,7 @@ const stopSdkInstalWatch = watchEffect(() => {
 }
 
 .hide-left-box-mode {
-  --left-pano-left: calc(var(--editor-menu-width) - var(--left-pano-width)) !important;
+  --left-pano-left: calc(var(--editor-menu-left) + var(--editor-menu-width) - var(--left-pano-width)) !important;
 }
 
 .sys-view-auto {
@@ -75,6 +76,10 @@ const stopSdkInstalWatch = watchEffect(() => {
   --editor-menu-bottom: 60px;
 }
 
+.edit-mode {
+  --editor-menu-left: calc(-1 * var(--editor-menu-width));
+}
+
 .laser-layer {
   position: absolute;
   z-index: 1;
@@ -83,7 +88,7 @@ const stopSdkInstalWatch = watchEffect(() => {
   .scene {
     width: 100%;
     height: 100%;
-    background-color: black;
+    background-color: #ccc;
   }
 }
 </style>

+ 9 - 2
src/store/index.ts

@@ -1,12 +1,17 @@
 import { initialModels } from './model'
+import { initialTaggings } from './tagging'
 import { ref } from 'vue'
+import { initialTaggingStyles } from './taging-style'
 
 export const loaded = ref(false)
 export const error = ref(false)
+export const TemploraryID = '-1'
 
 export const initialStore = async () => {
   const init = Promise.all([
-    initialModels()
+    initialModels(),
+    initialTaggingStyles(),
+    initialTaggings()
   ])
 
   try {
@@ -19,4 +24,6 @@ export const initialStore = async () => {
 
 
 export * from './sys'
-export * from './model'
+export * from './model'
+export * from './tagging'
+export * from './taging-style'

+ 56 - 0
src/store/tagging.ts

@@ -0,0 +1,56 @@
+import { ref } from 'vue'
+import { getTaggings } from '@/api'
+
+import type { Taggings } from '@/api'
+
+export const taggings = ref<Taggings>([])
+
+export const initialTaggings = async () => {
+  // taggings.value = await getTaggings()
+  taggings.value = [
+    {
+      id: '1231',
+      title: 'aaaa',
+      styleId: '1231',
+      desc: '123123',
+      part: '123asd',
+      method: '123123a',
+      principal: 'asdasd',
+      images: ['https://gw.alicdn.com/tps/TB1W_X6OXXXXXcZXVXXXXXXXXXX-400-400.png'],
+      positions: [
+        { x: 1, y: 1, z: 1 }
+      ]
+    },
+    {
+      id: '12321',
+      title: 'aaaa',
+      desc: '123123',
+      styleId: '1231',
+      part: '123asd',
+      method: '123123a',
+      principal: 'asdasd',
+      images: ['https://gw.alicdn.com/tps/TB1W_X6OXXXXXcZXVXXXXXXXXXX-400-400.png'],
+      positions: [
+        { x: 1, y: 1, z: 1 }
+      ]
+    },
+    {
+      id: '1234',
+      title: 'aaaa',
+      desc: '123123',
+      part: '123asd',
+      styleId: '1231',
+      method: '123123a',
+      principal: 'asdasd',
+      images: ['https://gw.alicdn.com/tps/TB1W_X6OXXXXXcZXVXXXXXXXXXX-400-400.png'],
+      positions: [
+        { x: 1, y: 1, z: 1 }
+      ]
+    }
+  ]
+}
+
+
+
+
+export type { Taggings, Tagging } from '@/api'

+ 24 - 0
src/store/taging-style.ts

@@ -0,0 +1,24 @@
+import { ref } from 'vue'
+import { getTaggingStyles } from '@/api'
+
+import type { TaggingStyles, TaggingStyle } from '@/api'
+
+export const taggingStyles = ref<TaggingStyles>([])
+
+export const getTaggingStyle = (id: TaggingStyle['id']) => 
+  taggingStyles.value.find(style => style.id === id)
+
+export const initialTaggingStyles = async () => {
+  // taggings.value = await getTaggingStyles()
+  taggingStyles.value = [
+    {
+      id: '1231',
+      name: 'aaa',
+      icon: 'https://gw.alicdn.com/tps/TB1W_X6OXXXXXcZXVXXXXXXXXXX-400-400.png',
+      default: true
+    }
+  ]
+}
+
+
+export type { TaggingStyle, TaggingStyles } from '@/api'

+ 0 - 1
src/style.scss

@@ -25,7 +25,6 @@ body {
 
   --body-right-margin: 20px;
 
-  --left-pano-left: calc(var(--editor-menu-left) + var(--editor-menu-width));
   --left-pano-width: 340px;
 }
 

+ 10 - 10
src/views/merge/index.vue

@@ -2,7 +2,7 @@
   <RightPano v-if="currentModel">
     <ui-group>
       <template #header>
-          <Actions class="edit-header" :items="actionItems" />
+        <Actions class="edit-header" :items="actionItems" />
       </template>
       <ui-group-option label="等比缩放">
         <template #icon>
@@ -11,7 +11,7 @@
         <ui-input type="range" v-model="currentModel.scale" v-bind="scaleOption" width="100%" />
       </ui-group-option>
       <ui-group-option label="离地高度">
-        <ui-input type="range" v-model="currentModel.bottom" v-bind="bottomOption" width="100%"/>
+        <ui-input type="range" v-model="currentModel.bottom" v-bind="bottomOption" width="100%" />
       </ui-group-option>
       <ui-group-option label="模型不透明度">
         <ui-input type="range" v-model="currentModel.opacity" v-bind="opacityOption" width="100%" />
@@ -41,21 +41,21 @@ const opacityOption = { min: 0.01, max: 1, step: 0.01, }
 const bottomOption = { min: 1, max: 100, step: 1, }
 const scaleOption = { min: 0.01, max: 1, step: 0.01, }
 const actionItems: ActionsProps['items'] = [
-  { 
-    icon: 'move', 
-    text: '移动', 
+  {
+    icon: 'move',
+    text: '移动',
     action: () => {
       getSceneModel(currentModel.value)?.enterMoveMode()
       return () => getSceneModel(currentModel.value)?.leaveMoveMode()
-    } 
+    }
   },
-  { 
-    icon: 'flip', 
-    text: '旋转', 
+  {
+    icon: 'flip',
+    text: '旋转',
     action: () => {
       getSceneModel(currentModel.value)?.enterRotateMode()
       return () => getSceneModel(currentModel.value)?.leaveRotateMode()
-    } 
+    }
   },
 ]
 

+ 0 - 14
src/views/query/index.vue

@@ -1,14 +0,0 @@
-<template>
-  <LeftPano>
-    123123
-  </LeftPano>
-
-  <RightPano>
-    123123
-  </RightPano>
-</template>
-
-<script lang="ts" setup>
-import { LeftPano, RightPano } from '@/layout'
-
-</script>

+ 247 - 0
src/views/tagging/edit.vue

@@ -0,0 +1,247 @@
+<template>
+  <div class="edit-hot-layer">
+    <div class="edit-hot-item">
+      <h3 class="edit-title">
+        热点
+        <ui-icon type="close" ctrl @click.stop="$emit('quit')" class="edit-close" />
+      </h3>
+      <!-- <StylesManage 
+        :styles="styles" 
+        :active="(getTaggingStyle(tagging.styleId) as TaggingStyle)" 
+        @change="style => tagging.styleId = style.id" 
+        @delete="deleteStyle"
+        @uploadStyles="uploadStyles" 
+      /> -->
+      <ui-input 
+        require 
+        class="input" 
+        width="100%" 
+        placeholder="请输入热点标题" 
+        type="text" 
+        v-model="tagging.title"
+        maxlength="15" 
+      />
+      <ui-input
+        class="input"
+        width="100%"
+        height="158px"
+        placeholder="特征描述:"
+        type="richtext"
+        v-model="tagging.desc"
+        :maxlength="200"
+      />
+      <ui-input 
+        class="input preplace" 
+        width="100%" 
+        placeholder="" 
+        type="text" 
+        v-model="tagging.part"
+      >
+        <template #preIcon><span>遗留部位:</span></template>
+      </ui-input>
+      <ui-input 
+        class="input preplace" 
+        width="100%" 
+        placeholder="" 
+        type="text" 
+        v-model="tagging.method"
+      >
+        <template #preIcon><span>提取方法:</span></template>
+      </ui-input>
+      <ui-input 
+        class="input preplace" 
+        width="100%" 
+        type="text" 
+        placeholder=""
+        v-model="tagging.principal"
+      >
+        <template #preIcon><span>提取人:</span></template>
+      </ui-input>
+      <ui-input
+          class="input "
+          type="file"
+          width="100%"
+          height="225px"
+          preview
+          placeholder="上传图片"
+          othPlaceholder="支持JPG、PNG图片格式,单张不超过5MB,最多支持上传9张。"
+          accept=".jpg, .png"
+          :disable="true"
+          :multiple="true"
+          :maxSize="5 * 1024 * 1024"
+          :maxLen="9"
+          :modelValue="tagging.images"
+          @update:modelValue="fileChange"
+      >
+          <template v-slot:valuable>
+              <Images :tagging="tagging" :hideInfo="true">
+                <template v-slot:icons="{ active }">
+                  <span @click="delImageHandler(active)" class="del-file">
+                    <ui-icon type="del" ctrl />
+                  </span>
+                </template>
+              </Images>
+          </template>
+      </ui-input>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+// import StylesManage from './styles.vue'
+import Images from './images.vue'
+// import { LocalTaggingStyle } from './styles.vue'
+import { computed, ref } from 'vue';
+import { Dialog } from 'bill/index';
+import { 
+  // taggingStyles,
+  Tagging, 
+  // TemploraryID, 
+  // getTaggingStyle, 
+  // TaggingStyle,
+  // taggings
+} from '@/store'
+
+export type EditProps = {
+  // taggingFiles: WeakMap<Tagging, Array<File>>
+  // styleFile: WeakMap<TaggingStyle, File>
+  data: Tagging
+}
+export type ImageFile = { file: File; preview: { url: string, name: string } } | Tagging['images'][number]
+export type LocalTagging = Omit<Tagging, 'images'> & {images: ImageFile[]}
+
+const props = defineProps<EditProps>()
+const emit = defineEmits<{ (e: 'quit'): void }>()
+const tagging = ref<LocalTagging>({...props.data, images: [...props.data.images]})
+
+// const styles = computed(() => 
+//   [...taggingStyles.value].sort((a, b) => 
+//     a.default ? -1 : b.default ? 1 : a.id === TemploraryID ? -1 : b.id === TemploraryID ? 1 : 0
+//   )
+// )
+
+// const deleteStyle = (style: TaggingStyle) => {
+//     const index = taggingStyles.value.indexOf(style)
+//     if (~index) {
+//       taggingStyles.value.splice(index, 1)
+//       for (const item of taggings.value) {
+//         if (item.styleId === style.id) {
+//           const defaultIcon = taggingStyles.value.find(({ default: isDefault }) => isDefault)?.id
+//           if (defaultIcon) {
+//             item.styleId = defaultIcon
+//           }
+//         }
+//       }
+//     }
+// }
+
+// const normalizeIcon = (icon: LocalTaggingStyle['icon'] | string): string => {
+//   if (typeof icon === 'string') {
+//     return icon
+//   } else {
+//     return icon.preview
+//   }
+// }
+
+// const uploadStyles = (unStyles: Array<LocalTaggingStyle>) => {
+//   const addStyles = unStyles.map(item => {
+//     const style = {
+//       ...item,
+//       icon: normalizeIcon(item.icon),
+//     }
+
+//     props.styleFile.set(style, item.icon.file)
+//     return style
+//   })
+//   styles.value.push(...addStyles)
+//   tagging.value.styleId = addStyles[0].id
+// }
+
+type LocalImageFile = { file: File; preview: string } | Tagging['images'][number]
+const fileChange = (file: LocalImageFile | LocalImageFile[]) => {
+  const files = Array.isArray(file) ? file : [file]
+  tagging.value.images = files.map(atom => {
+    if (typeof atom === 'string') {
+      return atom
+    } else {
+      console.log(atom)
+      return {
+        file: atom.file,
+        preview: {
+          url: atom.preview,
+          name: atom.file.name,
+        },
+      }
+    }
+  })
+}
+
+const delImageHandler = async (file: ImageFile) => {
+  const index = tagging.value.images.indexOf(file)
+  if (~index && (await Dialog.confirm(`确定要删除此数据吗?`))) {
+    tagging.value.images.splice(index, 1)
+  }
+}
+
+</script>
+
+<style lang="scss" scoped>
+.edit-hot-layer {
+  position: fixed;
+  inset: 0;
+  background: rgba(0,0,0,0.3000);
+  backdrop-filter: blur(4px);
+  z-index: 2000;
+}
+.edit-hot-item {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 400px;
+  position: relative;
+  padding: 20px;
+  background: rgba(27, 27, 28, 0.8);
+  box-shadow: 0px 0px 10px 0px rgba(0,0,0, 0.3);
+  border-radius: 4px;
+
+  .input {
+    margin-bottom: 10px;
+  }
+
+}
+.edit-close {
+  position: absolute;
+  cursor: pointer;
+  top: calc((100% - 18px) / 2);
+  right: 0;
+  transform: translateY(-50%);
+}
+
+.edit-title {
+  padding-bottom: 18px;
+  margin-bottom: 18px;
+  position: relative;
+
+  &::after {
+    content: '';
+    position: absolute;
+    left: -20px;
+    right: -20px;
+    height: 1px;
+    bottom: 0;
+    background-color: rgba(255, 255, 255, 0.16);;
+  }
+}
+</style>
+<style>
+.edit-hot-item .preplace input{
+  padding-left: 76px !important;
+}
+
+.edit-hot-item .preplace .pre-icon {
+  color: rgba(255,255,255,0.6000);
+  width: 70px;
+  text-align: right;
+}
+</style>

+ 119 - 0
src/views/tagging/images.vue

@@ -0,0 +1,119 @@
+<template>
+  <div class="mates">
+    <ui-slide 
+      v-if="tagging" 
+      :items="tagging.images" 
+      :showCtrl="tagging.images.length > 1" 
+      :currentIndex="index"
+      @change="(i: number) => index = i" 
+      :showInfos="tagging.images.length > 1 && !hideInfo"
+    >
+      <template v-slot="{ raw, index }">
+        <div 
+          class="meta-item" 
+          :class="{ full: inFull }" 
+          @click="inFull && $emit('pull', index)"
+        >
+          <img :src="typeof raw === 'string' ? raw : raw.preview.url" />
+        </div>
+      </template>
+      <template v-slot:attach="{ active }">
+        <div class="file-mange">
+          <slot name="icons" :active="active" />
+        </div>
+      </template>
+    </ui-slide>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+
+import type { LocalTagging } from './edit.vue'
+export type ImagesProps = {
+  tagging: LocalTagging
+  inFull?: boolean
+  hideInfo?: boolean
+}
+
+defineProps<ImagesProps>()
+defineEmits<{
+    (e: 'pull', index: number): void
+    (e: 'change', i: number): void
+}>()
+const index = ref(0)
+</script>
+
+<style lang="scss" scoped>
+
+.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.6);
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+
+      &:not(:last-child) {
+        margin-bottom: 10px;
+      }
+    }
+  }
+}
+
+</style>

+ 33 - 1
src/views/tagging/index.vue

@@ -1,10 +1,42 @@
 <template>
   <RightFillPano>
-    123123
+    <ui-group borderBottom>
+      <template #header>
+        <ui-button>
+          <ui-icon type="add" />
+          新增
+        </ui-button>
+      </template>
+    </ui-group>
+    <ui-group title="标注">
+      <template #icon>
+        <ui-icon type="eye-s" />
+      </template>
+      <ui-group-option>
+        <ui-input type="text" width="100%" placeholder="搜索">
+          <template #preIcon>
+            <ui-icon type="search" />
+          </template>
+        </ui-input>
+      </ui-group-option>
+      <TagingSign 
+        v-for="tagging in taggings" 
+        :key="tagging.id" 
+        :tagging="tagging" 
+        @edit="currentTagging = tagging"/>
+    </ui-group>
   </RightFillPano>
+
+  <Edit v-if="currentTagging" :data="currentTagging" @quit="currentTagging = void 0" />
 </template>
 
 <script lang="ts" setup>
+import Edit from './edit.vue'
 import { RightFillPano } from '@/layout'
+import { Tagging, taggings } from '@/store'
+import TagingSign from './sign.vue'
+import { ref } from 'vue';
+
+const currentTagging = ref<Tagging>()
 
 </script>

+ 42 - 0
src/views/tagging/sign.vue

@@ -0,0 +1,42 @@
+<template>
+  <ui-group-option class="sign-tagging">
+    <div class="info">
+      <img :src="tagging.images[0]">
+      <div>
+        <p>{{ tagging.title }}</p>
+        <a>放置:{{ tagging.positions.length }}</a>
+      </div>
+    </div>
+    <div class="actions">
+      <ui-icon type="pin" ctrl />
+      <ui-more 
+        :options="menus" 
+        style="margin-left: 20px" 
+        @click="(action: keyof typeof actions) => actions[action]()" 
+      />
+    </div>
+  </ui-group-option>
+</template>
+
+<script setup lang="ts">
+import { Tagging } from '@/store'
+
+
+defineProps<{ tagging: Tagging }>()
+const emit = defineEmits<{ 
+  (e: 'delete'): void 
+  (e: 'edit'): void 
+}>()
+
+const menus = [
+  { label: '编辑', value: 'edit' },
+  { label: '删除', value: 'delete' },
+]
+const actions = {
+  edit: () => emit('edit'),
+  delete: () => emit('delete')
+}
+
+</script>
+
+<style lang="scss" scoped src="./style.scss"></style>

+ 38 - 0
src/views/tagging/style.scss

@@ -0,0 +1,38 @@
+.sign-tagging {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px 0;
+  border-bottom: 1px solid var(--colors-border-color);
+
+  .info {
+    flex: 1;
+
+    display: flex;
+    align-items: center;
+
+    img {
+      width: 48px;
+      height: 48px;
+      object-fit: cover;
+      border-radius: 4px;
+      overflow: hidden;
+      background-color: rgba(255,255,255,.6);
+    }
+
+    div {
+      margin-left: 10px;
+
+      p {
+        color: #fff;
+        font-size: 14px;
+        margin-bottom: 6px;
+      }
+    }
+  }
+  
+  .actions {
+    flex: none;
+  }  
+}
+

+ 274 - 0
src/views/tagging/styles.vue

@@ -0,0 +1,274 @@
+<template>
+  <div class="hot-styles">
+    <div class="add item" v-if="!props.all">
+      <span class="fun-ctrl">
+        <ui-input 
+          class="input" 
+          preview 
+          accept=".jpg, .jpeg, .png" 
+          @update:modelValue="iconUpload" 
+          type="file"
+        >
+          <template v-slot:replace>
+            <ui-icon type="add" class="icon" />
+          </template>
+        </ui-input>
+      </span>
+    </div>
+    <div 
+      v-for="hotStyle in styleAll" 
+      class="item" 
+      :class="{ active: active === hotStyle }"
+      @click="clickHandler(hotStyle)"
+    >
+      <span>
+        <img :src="hotStyle.icon" />
+        <ui-icon 
+          v-if="!hotStyle.default"
+          class="delete" 
+          type="close" 
+          @click.stop="emit('delete', hotStyle)" 
+        />
+      </span>
+    </div>
+    <div 
+      v-if="!props.all && props.styles.length > 5"
+      class="add item style-more" 
+      @click="showAll = !showAll"
+    >
+      <span class="fun-ctrl">
+        <ui-icon :type="showAll ? 'pull-up' : 'pull-down'" class="icon" />
+        <ui-bubble 
+          class="more-content" 
+          :show="showAll" 
+          @click.stop 
+          type="bottom">
+          
+          <styles 
+            :styles="styles.filter(style => !styleAll.includes(style))" 
+            :active="active" 
+            all
+            @quitMore="showAll = false" 
+            @uploadStyles="addStyles" @change="clickHandler"
+            @delete="style => emit('delete', style)"
+          />
+        </ui-bubble>
+      </span>
+    </div>
+  </div>
+  <!-- <div class="buttons" v-if="props.all">
+    <ui-button type="submit" class="button" @click="emit('quitMore')">取消</ui-button>
+    <ui-button type="primary" class="button" @click="enterHandler">保存</ui-button>
+  </div> -->
+</template>
+
+<script setup lang="ts">
+import { TaggingStyle, TaggingStyles } from '@/store'
+import { TemploraryID } from '@/store'
+import { ref, computed, defineEmits } from 'vue'
+import { Cropper } from 'bill/index'
+
+export type LocalTaggingStyle = Omit<TaggingStyle, 'icon'> & {
+  icon: { file: File; preview: string }
+}
+
+const props = defineProps<{
+  styles: TaggingStyles
+  active: TaggingStyle
+  all?: boolean
+}>()
+
+const emit = defineEmits<{
+  (e: 'change', style: TaggingStyle): void
+  (e: 'delete', style: TaggingStyle): void
+  (e: 'uploadStyles', styles: Array<LocalTaggingStyle>): void
+  (e: 'quitMore'): void
+}>()
+
+const showAll = ref(false)
+const styleAll = computed(() => {
+  if (props.all) {
+    return props.styles
+  } else {
+    const styles = props.styles.slice(0, props.styles.length > 5 ? 4 : 5)
+    if (!styles.includes(props.active)) {
+      styles[3] = props.active
+    }
+    return styles
+  }
+})
+
+const iconUpload = async ({ file, preview }: LocalTaggingStyle['icon']) => {
+  const data = await Cropper.open(preview)
+  if (data) {
+    const item = {
+      id: TemploraryID,
+      icon: { file: data[0], preview: data[1] },
+      name: file.name,
+      default: false,
+    }
+    emit('uploadStyles', [item])
+  }
+}
+
+const clickHandler = (hotStyle: TaggingStyle) => {
+  if (!props.all) {
+    showAll.value = false
+  }
+  emit('change', hotStyle)
+}
+
+const addStyles = (newStyles: Array<LocalTaggingStyle>) => {
+  emit('uploadStyles', newStyles)
+}
+</script>
+
+<style lang="scss" scoped>
+.hot-styles {
+  --size: 40px;
+  --icon-size: calc(var(--size) * 0.85);
+  margin: 24px 0;
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(var(--size), 1fr));
+  gap: calc(var(--size) / 4);
+  align-items: start;
+  justify-content: center;
+
+  .item {
+    --un-active-color: rgba(var(--colors-primary-base-fill), 0);
+    --active-transition: .3s ease;
+    cursor: pointer;
+
+    &.disable {
+      opacity: .3;
+      pointer-events: none;
+      cursor: inherit;
+    }
+
+    span {
+      width: var(--size);
+      height: var(--size);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border: 1px solid var(--un-active-color);
+      position: relative;
+      transition: border var(--active-transition);
+      border-radius: 4px;
+
+      .input {
+        margin: 0;
+      }
+
+      img {
+        width: var(--icon-size);
+        height: var(--icon-size);
+        // outline: 1px dashed var(--un-active-color);
+        transition: outline-color var(--active-transition);
+        border-radius: 4px;
+      }
+
+      .delete {
+        --round-size: calc(var(--size) * 0.45);
+        position: absolute;
+        width: var(--round-size);
+        height: var(--round-size);
+        border-radius: 50%;
+        background-color: rgba(250, 63, 72, 1);
+        right: calc(var(--round-size) * -1 / 2);
+        top: calc(var(--round-size) * -1 / 2);
+        transition: background-color var(--active-transition);
+        font-size: 12px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #fff;
+        opacity: 0;
+        transition: opacity .3s ease;
+
+      }
+    }
+
+    p {
+      transition: color var(--active-transition);
+      margin-top: calc(var(--size) / 4);
+      text-align: center;
+      color: rgb(var(--colors-primary-fill));
+      font-size: var(--small-size);
+    }
+
+    &.active {
+      color: rgba(var(--colors-primary-base-fill), 1);
+      --un-active-color: rgba(var(--colors-primary-base-fill), 1);
+
+      span img {
+        outline-color: rgb(var(--colors-primary-fill));
+      }
+      p {
+        color: currentColor;
+      }
+    }
+
+    &:not(.style-more):hover {
+      .delete {
+        opacity: 0.5;
+
+        &:hover {
+          opacity: 1;
+        }
+      }
+    }
+  }
+
+  .add {
+    height: 100%;
+    align-items: center;
+    display: flex;
+    flex: none;
+
+    span {
+      font-size: calc(var(--icon-size) * 0.4);
+      border: none;
+
+      &::before {
+        content: '';
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        width: var(--icon-size);
+        height: var(--icon-size);
+        border-radius: 2px;
+        border: 1px solid var(--colors-border-color);
+        transition: border-color .3s ease;
+      }
+
+      &:hover::before {
+        border-color: rgba(255,255,255,1);
+      }
+      &:active::before {
+        border-color: var(--colors-primary-base) !important;
+      }
+    }
+  }
+
+  .style-more {
+    .fun-ctrl {
+      position: relative;
+    }
+
+    .more-content {
+      width: 360px;
+      z-index: 9;
+      --arrow-width: 20px;
+      --bottom-left: 310px;
+      --back-color: rgba(0, 0, 0, 0.7);
+
+      .hot-styles {
+        margin: 0;
+      }
+    }
+  }
+}
+
+</style>

+ 3 - 1
src/vite-env.d.ts

@@ -8,4 +8,6 @@ declare module '*.vue' {
 
 type ToChangeAPI<T extends Record<string, any>> = {
   [key in keyof T as `change${Capitalize<key & string>}`]: (prop: T[key]) => void
-}
+}
+
+type ScenePos = { x: number, y: number, z: number }