edit-paths.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <template>
  2. <div class="video">
  3. <div class="overflow">
  4. <ui-icon
  5. ctrl
  6. :type="isScenePlayIng ? 'pause' : 'preview'"
  7. :disabled="!paths.length"
  8. @click="play"
  9. />
  10. <ui-button
  11. type="primary"
  12. @click="addPath"
  13. width="200px"
  14. :class="{ disabled: isScenePlayIng }"
  15. >
  16. 添加视角
  17. </ui-button>
  18. </div>
  19. <div class="info" v-if="paths.length">
  20. <div class="meta">
  21. <div class="length">
  22. <span>视频时长</span>{{paths.reduce((t, c) => t + c.time, 0).toFixed(1)}}s
  23. </div>
  24. <div
  25. class="fun-ctrl clear"
  26. @click="deleteAll"
  27. :class="{ disabled: isScenePlayIng }"
  28. >
  29. <ui-icon type="del" />
  30. <span>清空画面</span>
  31. </div>
  32. </div>
  33. <div class="photo-list" ref="listVm">
  34. <template v-for="(path, i) in paths" :key="path.id">
  35. <div
  36. class="photo"
  37. :class="{ active: current === path, disabled: isScenePlayIng }"
  38. @click="changeCurrent(path)"
  39. >
  40. <ui-icon
  41. type="del"
  42. ctrl
  43. @click.stop="deletePath(path)"
  44. :class="{ disabled: isScenePlayIng }"
  45. />
  46. <img :src="getResource(getFileUrl(path.cover))" />
  47. </div>
  48. <div class="set-phone-attr" v-if="i !== paths.length - 1">
  49. <ui-input
  50. type="number"
  51. width="54px"
  52. height="26px"
  53. :modelValue="path.speed"
  54. @update:modelValue="(val: number) => updatePathInfo(i, { speed: val })"
  55. :ctrl="false"
  56. :min="0.1"
  57. :max="10"
  58. >
  59. <template #icon><span>m/s</span></template>
  60. </ui-input>
  61. <ui-input
  62. type="number"
  63. width="54px"
  64. height="26px"
  65. :modelValue="path.time"
  66. @update:modelValue="(val: number) => updatePathInfo(i, { time: val })"
  67. :ctrl="false"
  68. :min="0.1"
  69. :max="20"
  70. class="time"
  71. >
  72. <template #icon><span class="time">s</span></template>
  73. </ui-input>
  74. </div>
  75. </template>
  76. </div>
  77. </div>
  78. <p class="un-video" v-else>暂无导览</p>
  79. </div>
  80. </template>
  81. <script setup lang="ts">
  82. import { loadPack, togetherCallback, getFileUrl, asyncTimeout } from '@/utils'
  83. import { sdk, playSceneGuide, pauseSceneGuide, isScenePlayIng } from '@/sdk'
  84. import { createGuidePath, isTemploraryID, useAutoSetMode, guides, getGuidePaths, guidePaths } from '@/store'
  85. import { Dialog, Message } from 'bill/index'
  86. import { useViewStack } from '@/hook'
  87. import { nextTick, ref, toRaw, watchEffect } from 'vue'
  88. import { showRightPanoStack, showLeftCtrlPanoStack, showLeftPanoStack, showRightCtrlPanoStack, getResource } from '@/env'
  89. import type { Guide, GuidePaths, GuidePath } from '@/store'
  90. import type { CalcPathProps } from '@/sdk'
  91. const props = defineProps< { data: Guide }>()
  92. const paths = ref<GuidePaths>(getGuidePaths(props.data))
  93. const current = ref<GuidePath>(paths.value[0])
  94. const updatePathInfo = (index: number, calcInfo: CalcPathProps[1]) => {
  95. const info = sdk.calcPathInfo(
  96. paths.value.slice(index, index + 2) as any,
  97. calcInfo
  98. )
  99. Object.assign(paths.value[index], info)
  100. }
  101. useViewStack(() =>
  102. togetherCallback([
  103. showRightPanoStack.push(ref(false)),
  104. showLeftCtrlPanoStack.push(ref(false)),
  105. showLeftPanoStack.push(ref(false)),
  106. showRightCtrlPanoStack.push(ref(false)),
  107. ])
  108. );
  109. useAutoSetMode(paths, {
  110. save() {
  111. if (!paths.value.length) {
  112. Dialog.alert('无法保存空路径导览!')
  113. throw '无法保存空路径导览!'
  114. }
  115. const oldPaths = getGuidePaths(props.data)
  116. props.data.cover = paths.value[0].cover
  117. guidePaths.value = guidePaths.value
  118. .filter(path => !oldPaths.includes(path))
  119. .concat(paths.value)
  120. if (isTemploraryID(props.data.id)) {
  121. console.error("现在才保存?")
  122. guides.value.push(props.data)
  123. }
  124. },
  125. }, false)
  126. const addPath = () => {
  127. loadPack(async () => {
  128. const dataURL = await sdk.screenshot(260, 160)
  129. const res = await fetch(dataURL)
  130. const blob = await res.blob()
  131. const pose = sdk.getPose()
  132. const index = paths.value.indexOf(current.value) + 1
  133. const path: GuidePath = createGuidePath({
  134. ...pose,
  135. guideId: props.data.id,
  136. cover: { url: dataURL, blob }
  137. })
  138. paths.value.splice(index, 0, path)
  139. current.value = path
  140. if (paths.value.length > 1) {
  141. const index = paths.value.length - 2
  142. updatePathInfo(index, { time: 3 })
  143. }
  144. })
  145. }
  146. const deletePath = async (path: GuidePath, fore: boolean = false) => {
  147. if (fore || (await Dialog.confirm('确定要删除此画面吗?'))) {
  148. const index = paths.value.indexOf(path)
  149. if (~index) {
  150. paths.value.splice(index, 1)
  151. }
  152. if (path === current.value) {
  153. current.value = paths.value[index + (index === 0 ? 0 : -1)]
  154. }
  155. }
  156. }
  157. const deleteAll = async () => {
  158. if (await Dialog.confirm('确定要清空画面吗?')) {
  159. paths.value.length = 0
  160. current.value = paths.value[0]
  161. }
  162. }
  163. const changeCurrent = (path: GuidePath) => {
  164. sdk.comeTo({ dur: 300, ...path })
  165. current.value = path
  166. }
  167. const play = async () => {
  168. if (isScenePlayIng.value) {
  169. pauseSceneGuide()
  170. } else {
  171. changeCurrent(paths.value[0])
  172. await asyncTimeout(400)
  173. playSceneGuide(toRaw(paths.value), (index) => {
  174. current.value = paths.value[index - 1]
  175. })
  176. }
  177. }
  178. const listVm = ref<HTMLDivElement>()
  179. watchEffect(async () => {
  180. const index = paths.value.indexOf(current.value)
  181. if (~index && listVm.value) {
  182. await nextTick()
  183. const scrollWidth = listVm.value.scrollWidth / paths.value.length
  184. const centerWidth = listVm.value.offsetWidth / 2
  185. const offsetLeft = scrollWidth * index - centerWidth
  186. listVm.value.scroll({
  187. left: offsetLeft,
  188. top: 0,
  189. })
  190. }
  191. })
  192. </script>
  193. <style lang="scss" scoped>
  194. .video {
  195. position: relative;
  196. .overflow {
  197. position: absolute;
  198. left: 50%;
  199. bottom: 100%;
  200. transform: translateX(-50%);
  201. margin-bottom: 20px;
  202. display: flex;
  203. align-items: center;
  204. .icon {
  205. margin-right: 20px;
  206. color: #fff;
  207. font-size: 30px;
  208. }
  209. }
  210. .meta {
  211. font-size: 12px;
  212. border-bottom: 1px solid rgba(255,255,255,.16);
  213. padding: 10px 20px;
  214. display: flex;
  215. justify-content: space-between;
  216. .length span {
  217. margin-right: 10px;
  218. }
  219. .clear {
  220. display: flex;
  221. align-items: center;
  222. .icon {
  223. font-size: 1.4em;
  224. margin-right: 5px;
  225. }
  226. }
  227. }
  228. .photo-list {
  229. padding: 10px 20px 20px;
  230. overflow-x: auto;
  231. display: flex;
  232. .set-phone-attr {
  233. padding: 0 10px;
  234. display: flex;
  235. flex-direction: column;
  236. justify-content: space-evenly;
  237. align-items: center;
  238. position: relative;
  239. &::before,
  240. &::after {
  241. content: '';
  242. color: rgba(255,255,255,.6);
  243. position: absolute;
  244. top: 50%;
  245. transform: translateY(-50%);
  246. }
  247. &::before {
  248. left: 0;
  249. right: 7px;
  250. height: 2px;
  251. background-color: currentColor;
  252. }
  253. &::after {
  254. right: -5px;
  255. width: 0;
  256. height: 0;
  257. border: 5px solid transparent;
  258. border-left: 7px solid currentColor;
  259. }
  260. }
  261. .photo {
  262. cursor: pointer;
  263. flex: none;
  264. position: relative;
  265. &.active {
  266. outline: 2px solid var(--colors-primary-base);
  267. }
  268. .icon {
  269. position: absolute;
  270. right: 10px;
  271. top: 10px;
  272. width: 24px;
  273. font-size: 12px;
  274. height: 24px;
  275. background-color: rgba(0,0,0,0.6);
  276. color: rgba(255,255,255,.6);
  277. display: flex;
  278. align-items: center;
  279. justify-content: center;
  280. cursor: pointer;
  281. border-radius: 50%;
  282. }
  283. img {
  284. width: 230px;
  285. height: 160px;
  286. display: block;
  287. }
  288. }
  289. }
  290. }
  291. .un-video {
  292. height: 100px;
  293. line-height: 100px;
  294. text-align: center;
  295. color: rgba(255,255,255,0.6);
  296. font-size: 1.2em;
  297. }
  298. </style>
  299. <style lang="scss">
  300. .set-phone-attr {
  301. .ui-input .text input {
  302. padding: 8px 4px;
  303. }
  304. .ui-input .text {
  305. font-size: 12px;
  306. }
  307. .ui-input .text.suffix .retouch {
  308. right: 4px;
  309. }
  310. .ui-input .text.suffix input {
  311. padding-right: 28px;
  312. text-align: right;
  313. }
  314. .ui-input.time .text.suffix input {
  315. padding-right: 18px;
  316. text-align: right;
  317. }
  318. }
  319. </style>