Jelajahi Sumber

feat: 对接热点

bill 3 tahun lalu
induk
melakukan
79f769a9ea

+ 4 - 1
src/api/constant.ts

@@ -32,4 +32,7 @@ export const TAGGING_STYLE_LIST = ''
 export const GUIDE_LIST = ''
 export const INSERT_GUIDE = ''
 export const UPDATE_GUIDE = ''
-export const DELETE_GUIDE = ''
+export const DELETE_GUIDE = ''
+
+// 文件上传
+export const UPLOAD_FILE = ''

+ 2 - 1
src/api/index.ts

@@ -7,4 +7,5 @@ export * from './instance'
 export * from './model'
 export * from './tagging'
 export * from './tagging-style'
-export * from './guide'
+export * from './guide'
+export * from './sys'

+ 12 - 0
src/api/sys.ts

@@ -0,0 +1,12 @@
+import { UPLOAD_FILE } from './constant'
+import { axios } from './instance'
+
+type UploadFile = LocalFile | string
+
+export const uploadFile = async (file: UploadFile) => {
+  if (typeof file === 'string') {
+    return file
+  } else {
+    return file.url
+  }
+}

+ 16 - 5
src/api/tagging.ts

@@ -6,16 +6,21 @@ import {
   UPDATE_TAGGING
 } from './constant'
 
+import type { Model } from './model'
+
 export interface Tagging {
   id: string
-  styleId: string,
+  styleId: string
   title: string,
   desc: string
   part: string
   method: string
   principal: string
   images: string[],
-  positions: ScenePos[]
+  positions: {
+    modelId: Model['id']
+    localPos: SceneLocalPos
+  }[]
 }
 
 export type Taggings = Tagging[]
@@ -28,6 +33,7 @@ export const fetchTaggings = async () => {
       title: 'aaaa',
       styleId: '1231',
       desc: '123123',
+      modelId: '123',
       part: '123asd',
       method: '123123a',
       principal: 'asdasd',
@@ -36,7 +42,10 @@ export const fetchTaggings = async () => {
         'https://gw.alicdn.com/tps/TB1W_X6OXXXXXcZXVXXXXXXXXXX-400-400.png'
       ],
       positions: [
-        { x: 1, y: 1, z: 1 }
+        { 
+          modelId: '123',
+          localPos: { x: 1, y: 1, z: 1 }
+        }
       ]
     },
 
@@ -52,8 +61,10 @@ export const fetchTaggings = async () => {
         'https://gw.alicdn.com/tps/TB1W_X6OXXXXXcZXVXXXXXXXXXX-400-400.png',
         'https://gw.alicdn.com/tps/TB1W_X6OXXXXXcZXVXXXXXXXXXX-400-400.png'
       ],
-      positions: [
-        { x: 1, y: 1, z: 1 }
+      positions: [{ 
+        modelId: '123',
+        localPos: { x: 1, y: 1, z: 1 }
+      }
       ]
     }
   ]

+ 4 - 2
src/components/tagging/sign.vue

@@ -30,7 +30,7 @@
       <Preview 
         @close="pullIndex = -1"
         :type="MediaType.img" 
-        :url="tagging.images[pullIndex]" 
+        :url="getFileUrl(tagging.images[pullIndex])" 
         v-if="!!~pullIndex" 
       />
     </div>
@@ -43,6 +43,8 @@ import UIBubble from 'bill/components/bubble/index.vue'
 import Images from '@/views/tagging/images.vue'
 import Preview, { MediaType } from '../static-preview/index.vue'
 import { Tagging, getTaggingStyle } from '@/store';
+import { getFileUrl } from '@/utils'
+import { sdk } from '@/sdk'
 
 export type SignProps = { tagging: Tagging, scenePos: Tagging['positions'][number], show?: boolean }
 
@@ -54,7 +56,7 @@ const posStyle = computed(() => {
     x: 700,
     y: 400
   } 
-  // sdk.getPositionByScreen(props.scenePos)
+  console.log(sdk.getScreenByPosition(props.scenePos.localPos, props.scenePos.modelId))
   return {
     left: screenPos.x + 'px',
     top: screenPos.y + 'px',

+ 5 - 1
src/env/index.ts

@@ -1,6 +1,8 @@
 import { stackFactory, flatStacksValue } from '@/utils'
 import { ref } from 'vue'
 
+import type { Model } from '@/store'
+
 export const viewModeStack = stackFactory(ref<'full' | 'auto'>('auto'))
 export const showToolbarStack = stackFactory(ref<boolean>(false))
 export const showRightPanoStack = stackFactory(ref<boolean>(true))
@@ -8,6 +10,7 @@ export const showLeftPanoStack = stackFactory(ref<boolean>(false))
 export const showLeftCtrlPanoStack = stackFactory(ref<boolean>(true))
 export const showRightCtrlPanoStack = stackFactory(ref<boolean>(true))
 export const showTaggingsStack = stackFactory(ref<boolean>(true))
+export const currentModelStack = stackFactory(ref<Model | null>(null))
 
 export const custom = flatStacksValue({
   viewMode: viewModeStack,
@@ -16,6 +19,7 @@ export const custom = flatStacksValue({
   showLeftPano: showLeftPanoStack,
   showLeftCtrlPano: showLeftCtrlPanoStack,
   shwoRightCtrlPano: showRightCtrlPanoStack,
-  showTaggings: showTaggingsStack
+  showTaggings: showTaggingsStack,
+  currentModel: currentModelStack
 })
 

+ 8 - 7
src/layout/model-list/index.vue

@@ -19,7 +19,8 @@
 <script lang="ts" setup>
 import { computed, ref, toRaw } from 'vue'
 import { LeftPano } from '@/layout'
-import { models, currentModel } from '@/store'
+import { models } from '@/store'
+import { custom } from '@/env'
 import { getSceneModel } from '@/sdk'
 import List from '@/components/list/index.vue'
 import ModelSign from './sign.vue'
@@ -29,19 +30,19 @@ import type { Model } from '@/store'
 const modelList = computed(() => 
   models.value.map(model => ({
     raw: model,
-    select: currentModel.value === model
+    select: custom.currentModel === model
   }))
 )
 
 const modelChangeSelect = (model: Model) => {
-  if (currentModel.value) {
-    getSceneModel(currentModel.value)?.changeSelect(false)
+  if (custom.currentModel) {
+    getSceneModel(custom.currentModel)?.changeSelect(false)
   }
-  if (toRaw(currentModel.value) !== toRaw(model)) {
+  if (toRaw(custom.currentModel) !== toRaw(model)) {
     getSceneModel(model)?.changeSelect(true)
-    currentModel.value = model
+    custom.currentModel = model
   } else {
-    currentModel.value = null
+    custom.currentModel = null
   }
 }
 

+ 2 - 1
src/sdk/association.ts

@@ -11,7 +11,7 @@ import {
 import TaggingComponent from '@/components/tagging/list.vue'
 
 import type { SDK, SceneModel, SceneGuidePath } from '.'
-import { Model, Tagging } from '@/api'
+import { Model, Tagging } from '@/store'
 
 const sceneModelMap = new WeakMap<Model, SceneModel>()
 export const getSceneModel = (model: Model | null) => model && sceneModelMap.get(toRaw(model))
@@ -51,6 +51,7 @@ const associationTaggings = (el: HTMLDivElement) => {
 
   shallowWatchArray(getTaggings, (taggings, oldTaggings) => {
     const { added, deleted } = diffArrayChange(taggings, oldTaggings)
+    console.log(added, deleted)
     for (const item of added) {
       taggingVMs.set(toRaw(item), mount(el, TaggingComponent, { tagging: item }))
     }

+ 8 - 6
src/sdk/index.ts

@@ -2,7 +2,7 @@ import cover from './cover'
 import { setup } from './association'
 import { loadLib } from '@/utils'
 
-import type { ModelAttrs, Model, GuidePath, GuidePaths } from '@/api'
+import type { ModelAttrs, Model, GuidePath, GuidePaths } from '@/store'
 import type { Emitter } from 'mitt'
 
 
@@ -18,7 +18,7 @@ export type SceneModel = ToChangeAPI<Omit<SceneModelAttrs, 'position' | 'rotatio
   }
 
 
-export type AddModelProps = Pick<Model, 'type' | 'url'> & ModelAttrs
+export type AddModelProps = Pick<Model, 'type' | 'url' | 'id'> & ModelAttrs
 
 export type SceneGuidePath = Pick<GuidePath, 'position' | 'target' | 'speed' | 'time'>
 export interface SceneGuide {
@@ -28,15 +28,17 @@ export interface SceneGuide {
   clear: () => void
 }
 
+export type ScenePos = { localPos: SceneLocalPos, modelId: Model['id'] }
+export type ScreenPos = { screenPos: ScreenLocalPos, modelId: Model['id'] }
 
 export interface SDK {
   layout: HTMLDivElement,
   addModel: (props: AddModelProps) => SceneModel
-  getPositionByScreen: (screenPos: ScreenPos) => ScenePos
-  getScreenByPosition: (scenePos: ScenePos) => ScreenPos
+  getPositionByScreen: (screenPos: ScreenPos['screenPos'], modelId?: Model['id']) => ScenePos | null
+  getScreenByPosition: (localPos: ScenePos['localPos'], modelId?: Model['id']) => ScreenLocalPos | null
   screenshot: (width: number, height: number) => Promise<string>
-  getPose: () => { position: ScenePos, target: ScenePos }
-  comeTo: (pos: { position: ScenePos; target: ScenePos; dur?: number }) => void
+  getPose: () => { position: SceneLocalPos, target: SceneLocalPos }
+  comeTo: (pos: { position: SceneLocalPos; target: SceneLocalPos; dur?: number }) => void
   enterSceneGuide: (data: SceneGuidePath[]) => SceneGuide
 }
   

+ 32 - 6
src/store/guide.ts

@@ -5,7 +5,9 @@ import {
   fetchGuides, 
   postAddGuide, 
   postDeleteGuide,
-  postUpdateGuide
+  postUpdateGuide,
+  uploadFile,
+  
 } from '@/api'
 import { 
   deleteStoreItem, 
@@ -16,7 +18,13 @@ import {
   recoverStoreItems
 } from '@/utils'
 
-import type { GuidePath, Guide, Guides } from '@/api'
+import type { GuidePath as SGuidePath, Guide as SGuide } from '@/api'
+
+export type GuidePath = LocalMode<SGuidePath, 'cover'>
+export type GuidePaths = GuidePath[]
+export type Guide = Omit<LocalMode<SGuide, 'cover'>, 'paths'> & { paths: GuidePaths }
+export type Guides = Guide[]
+
 
 export const guides = ref<Guides>([])
 
@@ -48,9 +56,29 @@ export const backupGuides = () => {
   }))
 }
 
+export const transformGuide = async (guide: Guide): Promise<SGuide> => {
+  let guideCover: string = ''
+  const pathsCover: string[] = []
+
+  const uploadGuideCover = uploadFile(guide.cover)
+    .then(cover => guideCover = cover)
+  const uploadPathsCver = guide.paths.map((path, index) => 
+    uploadFile(path.cover)
+      .then(cover => pathsCover[index] = cover)
+  )
+
+  await Promise.all([uploadGuideCover, ...uploadPathsCver])
+
+  return {
+    ...guide,
+    paths: guide.paths.map((path, i) => ({...path, cover: pathsCover[i]})),
+    cover: guideCover
+  }
+}
+
 export const recoverGuides = recoverStoreItems(guides, getBackupGuides)
-export const addGuide = addStoreItem(guides, postAddGuide)
-export const updateGuide = updateStoreItem(guides, postUpdateGuide)
+export const addGuide = addStoreItem(guides, postAddGuide, transformGuide)
+export const updateGuide = updateStoreItem(guides, postUpdateGuide, transformGuide)
 export const deleteGuide = deleteStoreItem(guides, guide => postDeleteGuide(guide.id))
 export const initialGuides = fetchStoreItems(guides, fetchGuides, backupGuides)
 export const saveGuides = saveStoreItems(
@@ -67,5 +95,3 @@ export const autoSaveGuides = autoSetModeCallback(guides, {
   recovery: recoverGuides,
   save: saveGuides,
 })
-
-export type { Guide, Guides, GuidePath, GuidePaths } from '@/api'

+ 4 - 5
src/store/model.ts

@@ -1,4 +1,4 @@
-import { ref, watchEffect } from 'vue'
+import { ref } from 'vue'
 import { autoSetModeCallback } from './sys'
 import { 
   fetchModels, 
@@ -15,9 +15,8 @@ import {
   recoverStoreItems
 } from '@/utils'
 
-import type { Models, Model } from '@/api'
+import type { Models } from '@/api'
 
-export const currentModel = ref<Model | null>(null)
 export const models = ref<Models>([])
 
 let bcModels: Models = []
@@ -30,7 +29,7 @@ export const backupModels = () => {
   }))
 }
 
-export const recoverModels = () => recoverStoreItems(models, getBackupModels)
+export const recoverModels = recoverStoreItems(models, getBackupModels)
 export const addModel = addStoreItem(models, postAddModel)
 export const updateModel = updateStoreItem(models, postUpdateModels)
 export const deleteModel = deleteStoreItem(models, model => postDeleteModel(model.id))
@@ -51,4 +50,4 @@ export const autoSaveModels = autoSetModeCallback(models, {
 })
 
 export { ModelType, ModelTypeDesc } from '@/api'
-export type { Model, Models } from '@/api'
+export type { Model, Models, ModelAttrs } from '@/api'

+ 4 - 2
src/store/sys.ts

@@ -23,12 +23,14 @@ export const TemploraryID = '-1'
 export const sysBus = asyncBusFactory<{ save: void; leave: void }>()
 
 // 进入编辑界面
-export const enterEdit = () => {
+export const enterEdit = (cb?: () => void) => {
   mode.value |= Flags.EDIT
+  cb && sysBus.on('leave', cb)
 }
 
-export const enterOld = () => {
+export const enterOld = (cb?: () => void) => {
   mode.value &= ~Flags.NOW
+  cb && sysBus.on('save', cb)
 }
 
 // 放弃保存内容

+ 18 - 9
src/store/tagging.ts

@@ -4,7 +4,8 @@ import {
   fetchTaggings, 
   postAddTagging,
   postDeleteTagging,
-  postUpdateTagging 
+  postUpdateTagging, 
+  uploadFile
 } from '@/api'
 import { 
   deleteStoreItem, 
@@ -16,7 +17,10 @@ import {
 } from '@/utils'
 
 
-import type { Taggings, Tagging } from '@/api'
+import type { Tagging as STagging } from '@/api'
+
+export type Tagging = LocalMode<STagging, 'images'>
+export type Taggings = Tagging[]
 
 export const taggings = ref<Taggings>([])
 
@@ -44,12 +48,19 @@ export const backupTaggings = () => {
   }))
 }
 
+export const transformTagging = async (tagging: Tagging): Promise<STagging> => {
+  const images: string[] = []
+  const uploadImages = tagging.images.map((file, index) => 
+    uploadFile(file).then(url => images[index] = url)
+  )
+
+  await Promise.all(uploadImages)
+  return { ...tagging, images }
+}
+
 export const recoverTaggings = recoverStoreItems(taggings, () => bcTaggings)
-export const addTagging = addStoreItem(taggings, postAddTagging)
-export const updateTagging = updateStoreItem(taggings, (newTagging, oldTagging) => {
-  console.log(newTagging, oldTagging)
-  return postUpdateTagging(newTagging)
-})
+export const addTagging = addStoreItem(taggings, postAddTagging, transformTagging)
+export const updateTagging = updateStoreItem(taggings, postUpdateTagging, transformTagging)
 export const deleteTagging = deleteStoreItem(taggings, tagging => postDeleteTagging(tagging.id))
 export const initialTaggings = fetchStoreItems(taggings, fetchTaggings, backupTaggings)
 export const saveTaggings = saveStoreItems(
@@ -66,5 +77,3 @@ export const autoSaveTaggings = autoSetModeCallback(taggings, {
   recovery: recoverTaggings,
   save: saveTaggings,
 })
-
-export type { Taggings, Tagging } from '@/api'

+ 5 - 0
src/utils/index.ts

@@ -37,6 +37,11 @@ export const together = (cbs: (() => void)[]) => {
   cbs.forEach(cb => cb())
 }
 
+export const getFileUrl = (file: LocalFile | string) => 
+  typeof file === 'string'
+    ? file
+    : file.url
+
 export * from './store-help'
 export * from "./stack";
 export * from "./loading";

+ 65 - 7
src/utils/store-help.ts

@@ -20,17 +20,55 @@ export const storeSecurityDelete = <T extends any>(items: T[], pushItem: T) => {
   }
 } 
 
-export const addStoreItem = <T extends {id: any}>(items: Ref<T[]>, addAction: (item: T) => Promise<T>) => {
+export function addStoreItem <T extends {id: any}>(
+  items: Ref<T[]>, 
+  addAction: (item: T) => Promise<T>,
+): (item: T) => Promise<void>
+export function addStoreItem <T extends {id: any}, K extends {id: any} = T>(
+  items: Ref<T[]>, 
+  addAction: (item: K) => Promise<K>,
+  transform: (item: T) => Promise<K> | K
+): (item: T) => Promise<void>
+export function addStoreItem <T extends {id: any}, K extends {id: any} = T>(
+  items: Ref<T[]>, 
+  addAction: (item: K) => Promise<K>,
+  transform?: (item: T) => Promise<K> | K
+) {
   return async (item: T) => {
-    const newItem = await addAction(item)
+    let actionData: K
+    if (transform) {
+      actionData = await transform(item)
+    } else {
+      actionData = item as unknown as K
+    }
+    const newItem = await addAction(actionData)
     item.id = newItem.id
     storeSecurityPush(items.value, item)
   }
 }
 
-export const updateStoreItem = <T extends {id: any}>(items: Ref<T[]>, updateAction: (item: T, oldItem: T) => Promise<any>) => {
+export function updateStoreItem <T extends {id: any}>(
+  items: Ref<T[]>, 
+  updateAction: (item: T, oldItem: T) => Promise<any>,
+): (item: T, oldItem: T) => Promise<void>
+export function updateStoreItem <T extends {id: any}, K extends {id: any} = T>(
+  items: Ref<T[]>, 
+  updateAction: (item: K, oldItem: T) => Promise<any>,
+  transform: (item: T) => Promise<K> | K
+): (item: T, oldItem: T) => Promise<void>
+export function updateStoreItem <T extends {id: any}, K extends {id: any} = T>(
+  items: Ref<T[]>, 
+  updateAction: (item: K, oldItem: T) => Promise<any>,
+  transform?: (item: T) => Promise<K> | K
+) {
   return async (item: T, oldItem: T) => {
-    await updateAction(item, oldItem)
+    let actionData: K
+    if (transform) {
+      actionData = await transform(item)
+    } else {
+      actionData = item as unknown as K
+    }
+    await updateAction(actionData, oldItem)
     const storeItem = items.value.find(atom => atom.id === item.id)
     if (storeItem) {
       Object.assign(storeItem, item)
@@ -38,10 +76,28 @@ export const updateStoreItem = <T extends {id: any}>(items: Ref<T[]>, updateActi
   }
 }
 
-
-export const deleteStoreItem = <T extends {id: any}>(items: Ref<T[]>, deleteAction: (item: T) => Promise<any>) => {
+export function deleteStoreItem <T extends {id: any}>(
+  items: Ref<T[]>, 
+  deleteAction: (item: T) => Promise<any>,
+): (item: T) => Promise<void>
+export function deleteStoreItem <T extends {id: any}, K extends {id: any} = T>(
+  items: Ref<T[]>, 
+  deleteAction: (item: K) => Promise<any>,
+  transform: (item: T) => Promise<K> | K
+): (item: T) => Promise<void>
+export function deleteStoreItem <T extends {id: any}, K extends {id: any} = T>(
+  items: Ref<T[]>, 
+  deleteAction: (item: K) => Promise<any>,
+  transform?: (item: T) => Promise<K> | K
+) {
   return async (item: T) => {
-    await deleteAction(item)
+    let actionData: K
+    if (transform) {
+      actionData = await transform(item)
+    } else {
+      actionData = item as unknown as K
+    }
+    await deleteAction(actionData)
     storeSecurityDelete(items.value, item)
   }
 }
@@ -113,9 +169,11 @@ export const diffStoreItemsChange = <T extends Array<{ id: any }>>(newItems: T,
 }
 
 export const recoverStoreItems = <T extends Array<{ id: any }>>(items: Ref<T>, getBackupItems: () => T) => () => {
+  console.log('?????')
   const backupItems = getBackupItems()
   items.value = backupItems.map(oldItem => {
     const model = items.value.find(item => item.id === oldItem.id)
+    console.log(model, oldItem)
     return model ? Object.assign(model, oldItem) : oldItem
   }) as T
 }

+ 8 - 24
src/views/guide/edit-paths.vue

@@ -45,7 +45,7 @@
             @click.stop="deletePath(path)" 
             :class="{ disabled: isScenePlayIng }" 
           />
-          <img :src="path.cover" />
+          <img :src="getFileUrl(path.cover)" />
         </div>
       </div>
     </div>
@@ -54,7 +54,7 @@
 </template>
 
 <script setup lang="ts">
-import { loadPack, togetherCallback } from '@/utils'
+import { loadPack, togetherCallback, getFileUrl } from '@/utils'
 import { sdk, playSceneGuide, pauseSceneGuide, isScenePlayIng } from '@/sdk'
 import { createGuidePath, TemploraryID, useAutoSetMode, guides, enterOld } from '@/store'
 import { Dialog } from 'bill/index'
@@ -64,12 +64,9 @@ import { showRightPanoStack, showLeftCtrlPanoStack, showLeftPanoStack, showRight
 
 import type { Guide, GuidePaths, GuidePath } from '@/store'
 
-type LocalPath = GuidePath & { blob?: Blob }
-type LocalPaths = LocalPath[]
-
 const props = defineProps< { data: Guide }>()
-const paths = ref<LocalPaths>([...props.data.paths])
-const current = ref<LocalPath>(paths.value[0])
+const paths = ref<GuidePaths>([...props.data.paths])
+const current = ref<GuidePath>(paths.value[0])
 
 useViewStack(() => 
   togetherCallback([
@@ -89,19 +86,6 @@ useAutoSetMode(paths, {
   }
 })
 
-setTimeout(() => {
-  paths.value = [{
-          id: '123a',
-          position: {x: 1, y: 1, z: 1},
-          target: {x: 1, y: 1, z: 1},
-          cover: 'https://gw.alicdn.com/tps/TB1W_X6OXXXXXcZXVXXXXXXXXXX-400-400.png',
-          speed: 1,
-          time: 1
-        }]
-}, 1000)
-
-
-
 const addPath = () => {
   loadPack(async () => {
     const dataURL = await sdk.screenshot(260, 160)
@@ -110,10 +94,10 @@ const addPath = () => {
 
     const pose = sdk.getPose()
     const index = paths.value.indexOf(current.value) + 1
-    const path: LocalPath = Object.assign(
-      { blob }, 
-      createGuidePath({ ...pose, cover: dataURL })
-    )
+    const path: GuidePath = createGuidePath({ 
+      ...pose, 
+      cover: { url: dataURL, blob } 
+    })
     paths.value.splice(index, 0, path)
     current.value = path
   })

+ 2 - 1
src/views/guide/sign.vue

@@ -2,7 +2,7 @@
   <ui-group-option class="sign-guide">
     <div class="info">
       <div class="guide-cover">
-        <img :src="guide.cover" />
+        <img :src="getFileUrl(guide.cover)" />
         <ui-icon type="preview" class="icon" ctrl />
       </div>
       <div>
@@ -21,6 +21,7 @@
 
 <script setup lang="ts">
 import { Guide } from '@/store'
+import { getFileUrl } from '@/utils'
 
 
 defineProps<{ guide: Guide }>()

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

@@ -1,5 +1,5 @@
 <template>
-  <RightPano v-if="currentModel">
+  <RightPano v-if="custom.currentModel">
     <ui-group>
       <template #header>
         <Actions class="edit-header" :items="actionItems" />
@@ -8,13 +8,13 @@
         <template #icon>
           <a href="">设置比例</a>
         </template>
-        <ui-input type="range" v-model="currentModel.scale" v-bind="scaleOption" width="100%" />
+        <ui-input type="range" v-model="custom.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="custom.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%" />
+        <ui-input type="range" v-model="custom.currentModel.opacity" v-bind="opacityOption" width="100%" />
       </ui-group-option>
       <ui-group-option>
         <ui-button>配准</ui-button>
@@ -28,11 +28,11 @@
 
 <script lang="ts" setup>
 import { RightPano } from '@/layout'
-import { currentModel, autoSaveModels } from '@/store'
+import { autoSaveModels } from '@/store'
 import Actions from '@/components/actions/index.vue'
 import { getSceneModel } from '@/sdk'
 import { useViewStack } from '@/hook'
-import { showLeftCtrlPanoStack, showLeftPanoStack } from '@/env'
+import { showLeftCtrlPanoStack, showLeftPanoStack, custom } from '@/env'
 import { ref } from 'vue'
 
 import type { ActionsProps } from '@/components/actions/index.vue'
@@ -45,16 +45,16 @@ const actionItems: ActionsProps['items'] = [
     icon: 'move',
     text: '移动',
     action: () => {
-      getSceneModel(currentModel.value)?.enterMoveMode()
-      return () => getSceneModel(currentModel.value)?.leaveMoveMode()
+      getSceneModel(custom.currentModel)?.enterMoveMode()
+      return () => getSceneModel(custom.currentModel)?.leaveMoveMode()
     }
   },
   {
     icon: 'flip',
     text: '旋转',
     action: () => {
-      getSceneModel(currentModel.value)?.enterRotateMode()
-      return () => getSceneModel(currentModel.value)?.leaveRotateMode()
+      getSceneModel(custom.currentModel)?.enterRotateMode()
+      return () => getSceneModel(custom.currentModel)?.leaveRotateMode()
     }
   },
 ]

+ 8 - 11
src/views/tagging/edit.vue

@@ -102,6 +102,7 @@ import { Dialog } from 'bill/index';
 import { 
   // taggingStyles,
   Tagging, 
+  Taggings,
   // TemploraryID, 
   // getTaggingStyle, 
   // TaggingStyle,
@@ -113,12 +114,10 @@ export type EditProps = {
   // 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, (e: 'save', data: LocalTagging): void }>()
-const tagging = ref<LocalTagging>({...props.data, images: [...props.data.images]})
+const emit = defineEmits<{ (e: 'quit'): void, (e: 'save', data: Tagging): void }>()
+const tagging = ref<Tagging>({...props.data, images: [...props.data.images]})
 
 // const styles = computed(() => 
 //   [...taggingStyles.value].sort((a, b) => 
@@ -166,23 +165,21 @@ const tagging = ref<LocalTagging>({...props.data, images: [...props.data.images]
 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') {
+    if (typeof atom === 'string' || 'blob' in atom) {
       return atom
     } else {
       console.log(atom)
       return {
-        file: atom.file,
-        preview: {
-          url: atom.preview,
-          name: atom.file.name,
-        },
+        blob: atom.file,
+        url: atom.preview
       }
     }
   })
 }
 
-const delImageHandler = async (file: ImageFile) => {
+const delImageHandler = async (file: Tagging['images'][number]) => {
   const index = tagging.value.images.indexOf(file)
   if (~index && (await Dialog.confirm(`确定要删除此数据吗?`))) {
     tagging.value.images.splice(index, 1)

+ 4 - 3
src/views/tagging/images.vue

@@ -14,7 +14,7 @@
           :class="{ full: inFull }" 
           @click="inFull && $emit('pull', index)"
         >
-          <img :src="typeof raw === 'string' ? raw : raw.preview.url" />
+          <img :src="getFileUrl(raw)" />
         </div>
       </template>
       <template v-slot:attach="{ active }">
@@ -28,10 +28,11 @@
 
 <script lang="ts" setup>
 import { ref } from 'vue'
+import { getFileUrl } from '@/utils'
+import { Tagging } from '@/store'
 
-import type { LocalTagging } from './edit.vue'
 export type ImagesProps = {
-  tagging: LocalTagging
+  tagging: Tagging
   inFull?: boolean
   hideInfo?: boolean
 }

+ 40 - 15
src/views/tagging/index.vue

@@ -45,22 +45,29 @@
 
 <script lang="ts" setup>
 import Edit from './edit.vue'
+import TagingSign from './sign.vue'
 import { Message } from 'bill/index'
 import { RightFillPano } from '@/layout'
+import { togetherCallback } from '@/utils'
 import { useViewStack } from '@/hook'
-import { taggings, TemploraryID, Tagging, autoSaveTaggings, createTagging } from '@/store'
-import TagingSign from './sign.vue'
-import { ref, watchEffect } from 'vue';
-import { custom } from '@/env'
+import { taggings, TemploraryID, Tagging, autoSaveTaggings, createTagging, models, enterEdit } from '@/store'
+import { ref, watch } from 'vue';
 import { sdk } from '@/sdk'
+import { 
+  custom, 
+  showLeftCtrlPanoStack, 
+  showLeftPanoStack,
+  currentModelStack,
+  showRightCtrlPanoStack,
+  showRightPanoStack
+} from '@/env'
 
-import type { LocalTagging } from './edit.vue'
 
 const editTagging = ref<Tagging | null>(null)
-const saveHandler = (tagging: LocalTagging) => {
+const saveHandler = (tagging: Tagging) => {
   if (!editTagging.value) return;
   if (editTagging.value.id === TemploraryID) {
-    // taggings.value.push(tagging)
+    taggings.value.push(tagging)
   } else {
     Object.assign(editTagging.value, tagging)
   }
@@ -74,22 +81,40 @@ const deleteTagging = (tagging: Tagging) => {
 
 
 const selectTagging = ref<Tagging | null>(null)
-watchEffect(() => {
+watch(selectTagging, (a, b, onCleanup) => {
   if (selectTagging.value) {
-    const handler = (ev: MouseEvent) => {
+    const pop = togetherCallback([
+      showLeftCtrlPanoStack.push(ref(true)), 
+      showLeftPanoStack.push(ref(true)),
+      currentModelStack.push(ref(custom.currentModel || models.value[0])),
+      showRightCtrlPanoStack.push(ref(false)),
+      showRightPanoStack.push(ref(false))
+    ])
+    const leave = () => selectTagging.value = null
+    const clickHandler = (ev: MouseEvent) => {
       const position = sdk.getPositionByScreen({
         x: ev.clientX,
         y: ev.clientY
-      })
-      
+      }, custom.currentModel?.id)
+
       if (!position) {
         Message.error('当前位置无法添加')
-      } else {
-        selectTagging.value?.positions.push(position)
+      } else if (selectTagging.value) {
+        selectTagging.value.positions.push(position)
+        leave()
       }
     }
-    sdk.layout.addEventListener('click', handler, false)
-    return () => sdk.layout.removeEventListener('click', handler, false)
+    const keyupHandler = (ev: KeyboardEvent) => ev.code === 'Escape' && leave()
+
+    document.documentElement.addEventListener('keyup', keyupHandler, false);
+    sdk.layout.addEventListener('click', clickHandler, false)
+
+    enterEdit(leave)
+    onCleanup(() => {
+      document.documentElement.removeEventListener('keyup', keyupHandler, false);
+      sdk.layout.removeEventListener('click', clickHandler, false);
+      pop()
+    })
   }
 })
 

+ 2 - 1
src/views/tagging/sign.vue

@@ -1,7 +1,7 @@
 <template>
   <ui-group-option class="sign-tagging" :class="{active: selected}" @click="emit('select')">
     <div class="info">
-      <img :src="tagging.images[0]">
+      <img :src="getFileUrl(tagging.images[0])">
       <div>
         <p>{{ tagging.title }}</p>
         <a>放置:{{ tagging.positions.length }}</a>
@@ -20,6 +20,7 @@
 
 <script setup lang="ts">
 import { Tagging } from '@/store'
+import { getFileUrl } from '@/utils'
 
 
 defineProps<{ tagging: Tagging, selected?: boolean }>()

+ 21 - 2
src/vite-env.d.ts

@@ -10,5 +10,24 @@ 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 }
-type ScreenPos = { x: number, y: number }
+type SceneLocalPos = { x: number, y: number, z: number }
+type ScreenLocalPos = { x: number, y: number }
+
+type LocalFile = { url: string, blob: Blob }
+type LocalMode<T, K> = T extends any[]
+  ? LocalMode<T[number], K>[]
+  : T extends {}
+    ? K extends keyof T
+      ? T[K] extends string
+        ? Omit<T, K> & { [key in K]: string | LocalFile }
+        : T[K] extends string[]
+          ? Omit<T, K> & { [key in K]: (string | LocalFile)[] }
+          : T
+      : T
+    : T
+
+type PartialProps<T, U extends keyof T = keyof T> = {
+    [P in keyof Omit<T, U>]: T[P];
+} & {
+  [P in U]?: T[P];
+}