LongImage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <template>
  2. <div
  3. class="long-image"
  4. @mousedown="onMouseDown"
  5. @mousemove="onMouseMove"
  6. @mouseup="onMouseUp"
  7. @mouseleave="onMouseLeave"
  8. @touchstart.passive="onTouchStart"
  9. @touchmove.prevent="onTouchMove"
  10. @touchend="onTouchEnd"
  11. @touchcancel="onTouchCancel"
  12. @wheel.passive="onWheel"
  13. >
  14. <audio
  15. ref="bgAudio$"
  16. loop
  17. autoplay
  18. :src="bgAudio"
  19. @play="bgAudioStatus = true"
  20. @pause="bgAudioStatus = false"
  21. />
  22. <div ref="longref$">
  23. <component
  24. class="time-item"
  25. v-for="(timeItem, index) in timeList"
  26. :key="timeItem.id"
  27. :is="timeItem.component"
  28. :info="{ ...timeItem.info, id: timeItem.id }"
  29. :style="{
  30. left: `calc(${itemW}% * ${index} - ${translateLength}px)`,
  31. }"
  32. @onClickTimeItem="onClickTimeItem"
  33. />
  34. </div>
  35. <Interaction ref="interaction$" :currentTimeIdx="currentTimeIdx" :list="timeList" />
  36. <Vmenu
  37. :currentTimeIdx="currentTimeIdx"
  38. @onClickMenuItem="onClickMenuItem"
  39. @onClickTimeItem="onClickTimeItem"
  40. :list="timeList"
  41. :bgAudioStatus="bgAudioStatus"
  42. />
  43. <teleport to="body">
  44. <Transition>
  45. <div v-if="isLongImageVideo" class="fade-in-video-wrap">
  46. <button class="skip-button" v-if="isShowSkip" @click="onSkipClick">
  47. <img :src="skipImg" alt="" draggable="false">
  48. </button>
  49. <button
  50. class="bofang-button"
  51. @click="onVideoCanPlayThrough"
  52. v-if="isNeedToBofang"
  53. >
  54. <img :src="bofangImg" alt="" draggable="false">
  55. </button>
  56. <video
  57. ref="video$"
  58. muted
  59. autoplay
  60. :poster="videoPostImg"
  61. class="initial-video"
  62. playsinline="true"
  63. x5-playsinline="true"
  64. webkit-playsinline="true"
  65. :src="`${config.cdnDir}videos/video2.mp4`"
  66. @playing="isNeedToBofang = false"
  67. @ended="onFadeInVideoEnd"
  68. @mousedown.passive.stop
  69. @touchstart.passive.stop
  70. @canplaythrough="onVideoCanPlayThrough"
  71. @wheel.passive.stop
  72. />
  73. </div>
  74. </Transition>
  75. <Transition>
  76. <div v-if="isShowDir">
  77. <Directory @close="isShowDir = false" />
  78. </div>
  79. </Transition>
  80. <Transition>
  81. <div class="guide" v-if="isShowGuide">
  82. <img :src="guideImg" alt="" draggable="false">
  83. <div class="tips">
  84. <p>滚动鼠标滚轮,浏览更多内容</p>
  85. <img :src="mouseImg" alt="" draggable="false">
  86. <div @click="onCloseGuide">
  87. <img :src="comfirImg" alt="" draggable="false">
  88. </div>
  89. </div>
  90. </div>
  91. </Transition>
  92. <div v-if="store.currentHotspot">
  93. <Hotspot @close="store.currentHotspot = ''" />
  94. </div>
  95. </teleport>
  96. </div>
  97. </template>
  98. <script setup>
  99. import { ref, getCurrentInstance, watch, computed, onMounted } from "vue"
  100. import appStore from "@/store/index";
  101. import timeList from "@/data/index";
  102. import Vmenu from "@/components/menu.vue"
  103. import Directory from "@/components/directory.vue"
  104. import Hotspot from "@/components/Hotspot.vue"
  105. import Interaction from "@/components/Interaction.vue"
  106. import { useRouter } from "vue-router"
  107. const router = useRouter()
  108. const store = appStore();
  109. const isMouseDown = ref(false);
  110. const lastMoveEventTimeStamp = ref(0);
  111. const moveSpeed = ref(0);
  112. const lastTouchPos = ref(0);
  113. const maxTranslateLength = ref(0);
  114. const guideImg = utils.getImageUrl(`guide.jpg`)
  115. const mouseImg = utils.getImageUrl(`mouse.png`)
  116. const comfirImg = utils.getImageUrl(`btn_concern.png`)
  117. const skipImg = utils.getImageUrl(`skip.png`)
  118. const bofangImg = utils.getImageUrl(`bofang.png`)
  119. const longref$ = ref(null)
  120. const video$ = ref(null)
  121. const interaction$ = ref(null)
  122. // 背景音乐相关
  123. const bgAudio = utils.getAudioUrl('bg.mp3')
  124. const bgAudio$ = ref(null)
  125. onMounted(() => {
  126. bgAudio$.value.volume = 0.2
  127. })
  128. const bgAudioStatus = ref(false)
  129. function switchBgAudio() {
  130. if (bgAudio$.value.paused) {
  131. bgAudio$.value.play()
  132. } else {
  133. bgAudio$.value.pause()
  134. }
  135. }
  136. // 过渡视频相关
  137. const videoPostImg = utils.getImageUrl(`videobg.jpg`)
  138. const isLongImageVideo = ref(true)
  139. const isNeedToBofang = ref(true)
  140. const isShowSkip = ref(false)
  141. const onVideoCanPlayThrough = () => {
  142. if (video$.value) {
  143. video$.value.play()
  144. isNeedToBofang.value = false
  145. }
  146. }
  147. const onSkipClick = () => {
  148. isShowGuide.value = true
  149. setTimeout(() => {
  150. isLongImageVideo.value = false
  151. }, 100);
  152. }
  153. function onFadeInVideoEnd() {
  154. isShowGuide.value = true
  155. setTimeout(() => {
  156. isLongImageVideo.value = false
  157. }, 100);
  158. }
  159. // 动画帧相关
  160. const lastAnimationTimeStamp = ref(0);
  161. const animationFrameId = ref(0);
  162. // 镜头平移相关
  163. const translateLength = ref(0);
  164. const currentTimeIdx = ref(0);
  165. const instance = getCurrentInstance()
  166. const globalProperties = instance.appContext.app.config.globalProperties
  167. const itemW = computed(() => globalProperties.$isMobile || window.innerWidth < 1400 ? 158 : 138)
  168. const isShowDir = ref(false)
  169. const isShowGuide = ref(false)
  170. let firstIn = true
  171. const onCloseGuide = () => {
  172. isShowGuide.value = false
  173. if (firstIn) {
  174. firstIn = false
  175. interaction$.value.handleShow()
  176. store.canPlayLongImageBgAudio = true
  177. }
  178. }
  179. const animationFrameTask = () => {
  180. const timeStamp = Date.now()
  181. const timeElapsed = timeStamp - lastAnimationTimeStamp.value
  182. // 速度减慢
  183. if (moveSpeed.value > 0) {
  184. moveSpeed.value -= (globalProperties.$isMobile ? 0.001 : 0.003) * timeElapsed
  185. if (moveSpeed.value < 0) {
  186. moveSpeed.value = 0
  187. }
  188. } else if (moveSpeed.value < 0) {
  189. moveSpeed.value += (globalProperties.$isMobile ? 0.001 : 0.003) * timeElapsed
  190. if (moveSpeed.value > 0) {
  191. moveSpeed.value = 0
  192. }
  193. }
  194. // 根据速度更新距离
  195. if (store.canMoveCamera) {
  196. translateLength.value += moveSpeed.value * timeElapsed
  197. if (translateLength.value < 0) {
  198. translateLength.value = 0
  199. } else if (translateLength.value > maxTranslateLength.value) {
  200. translateLength.value = maxTranslateLength.value
  201. moveSpeed.value = 0
  202. }
  203. }
  204. lastAnimationTimeStamp.value = timeStamp
  205. animationFrameId.value = requestAnimationFrame(animationFrameTask)
  206. }
  207. const onClickTimeItem = (index) => {
  208. translateLength.value = longref$.value.children[0].offsetWidth * index
  209. console.log('result:', longref$.value.children[0].offsetWidth * index);
  210. }
  211. const onClickMenuItem = (item) => {
  212. if (item.id === 'bgAudio') {
  213. switchBgAudio()
  214. } else if (item.id === 'search') {
  215. isShowDir.value = true
  216. } else if (item.id === 'tip') {
  217. isShowGuide.value = true
  218. } else if (item.id === 'home') {
  219. router.push({ path: '/' })
  220. }
  221. }
  222. const calcTranslateLimit = () => {
  223. maxTranslateLength.value = longref$.value.children[0].offsetWidth * (timeList.length - 1)
  224. }
  225. const onMouseDown = () => {
  226. isMouseDown.value = true
  227. moveSpeed.value = 0
  228. lastMoveEventTimeStamp.value = 0
  229. lastAnimationTimeStamp.value = Date.now()
  230. }
  231. const onMouseMove = (e) => {
  232. if (isMouseDown.value) {
  233. // 有些pc端浏览器比如firefox会有两次事件时间戳相同的情况发生。
  234. if (lastMoveEventTimeStamp.value && (e.timeStamp - lastMoveEventTimeStamp.value > 1)) {
  235. // 更新speed
  236. const currentMoveSpeed = - e.movementX / (e.timeStamp - lastMoveEventTimeStamp.value)
  237. moveSpeed.value = moveSpeed.value * 0.9 + currentMoveSpeed * 0.1
  238. }
  239. lastMoveEventTimeStamp.value = e.timeStamp
  240. }
  241. }
  242. const onMouseUp = () => {
  243. isMouseDown.value = false
  244. }
  245. const onMouseLeave = () => {
  246. isMouseDown.value = false
  247. }
  248. const onTouchStart = (e) => {
  249. isMouseDown.value = true
  250. moveSpeed.value = 0
  251. lastMoveEventTimeStamp.value = 0
  252. lastAnimationTimeStamp.value = Date.now()
  253. lastTouchPos.value = (globalProperties.$isRotate ? e.changedTouches[0].clientY : e.changedTouches[0].clientX)
  254. }
  255. const onTouchMove = (e) => {
  256. if (isMouseDown.value && e.changedTouches.length === 1) {
  257. // 疯狂操作的极端情况下两个时间戳之间的时差会不合理,甚至为0
  258. if (lastMoveEventTimeStamp.value && (e.timeStamp - lastMoveEventTimeStamp.value > 1)) {
  259. // 更新speed
  260. const currentMoveSpeed = - ((globalProperties.$isRotate ? e.changedTouches[0].clientY : e.changedTouches[0].clientX) - lastTouchPos.value) / (e.timeStamp - lastMoveEventTimeStamp.value) * (globalProperties.$isFirefox ? 2.2 : 1.5)
  261. moveSpeed.value = moveSpeed.value * 0.9 + currentMoveSpeed * 0.1
  262. lastTouchPos.value = globalProperties.$isRotate ? e.changedTouches[0].clientY : e.changedTouches[0].clientX
  263. }
  264. lastMoveEventTimeStamp.value = e.timeStamp
  265. }
  266. }
  267. const onTouchEnd = () => {
  268. isMouseDown.value = false
  269. }
  270. const onTouchCancel = () => {
  271. isMouseDown.value = false
  272. }
  273. const onWheel = (e) => {
  274. if (store.canMoveCamera) {
  275. translateLength.value += e.deltaY
  276. if (translateLength.value < 0) {
  277. translateLength.value = 0
  278. } else if (translateLength.value > maxTranslateLength.value) {
  279. translateLength.value = maxTranslateLength.value
  280. moveSpeed.value = 0
  281. }
  282. }
  283. }
  284. watch(translateLength, (vNew) => {
  285. try {
  286. currentTimeIdx.value = Math.round(translateLength.value / longref$.value.children[0].offsetWidth)
  287. } catch (error) {
  288. console.error('translateLength error: ', error)
  289. }
  290. })
  291. onMounted(() => {
  292. animationFrameId.value = requestAnimationFrame(animationFrameTask)
  293. if (store.longImageTranslateLengthRecord) {
  294. translateLength.value = store.longImageTranslateLengthRecord
  295. store.longImageTranslateLengthRecord = null
  296. }
  297. calcTranslateLimit()
  298. window.addEventListener('resize', calcTranslateLimit)
  299. setTimeout(() => {
  300. isShowSkip.value = true
  301. }, 6000);
  302. })
  303. </script>
  304. <style lang="scss" scoped>
  305. .long-image {
  306. height: 100%;
  307. width: 100%;
  308. position: relative;
  309. overflow: hidden;
  310. .time-item {
  311. position: absolute;
  312. top: 0;
  313. height: 100%;
  314. width: calc(v-bind(itemW) * 1%);
  315. max-width: calc(v-bind(itemW) * 1%);
  316. text-align: center;
  317. justify-content: flex-start;
  318. display: flex;
  319. align-items: center;
  320. }
  321. }
  322. .guide {
  323. position: fixed;
  324. left: 0;
  325. right: 0;
  326. bottom: 0;
  327. top: 0;
  328. width: 100%;
  329. height: 100%;
  330. z-index: 99;
  331. background-color: rgba($color: #000000, $alpha: 0.65);
  332. backdrop-filter: blur(15px);
  333. display: flex;
  334. justify-content: center;
  335. align-items: flex-end;
  336. >img {
  337. width: 100%;
  338. }
  339. .tips {
  340. position: absolute;
  341. top: 25%;
  342. left: 50%;
  343. transform: translateX(-50%);
  344. z-index: 100;
  345. text-align: center;
  346. color: #fff;
  347. width: 20%;
  348. >img {
  349. margin: 1.63rem 0 1rem;
  350. width: 80%;
  351. }
  352. >div {
  353. cursor: pointer;
  354. >img {
  355. width: 7rem;
  356. }
  357. }
  358. }
  359. }
  360. .fade-in-video-wrap {
  361. .skip-button {
  362. position: absolute;
  363. top: 1.21rem;
  364. right: 1.46rem;
  365. height: 1.75rem;
  366. z-index: 10000;
  367. img {
  368. height: 100%;
  369. }
  370. }
  371. .bofang-button {
  372. position: absolute;
  373. left: 50%;
  374. bottom: 30%;
  375. transform: translateX(-50%);
  376. z-index: 10000;
  377. width: 6rem;
  378. img {
  379. width: 100%;
  380. }
  381. }
  382. .initial-video {
  383. position: absolute; // 微信内嵌浏览器里视频必须决定对定位且z-index为负,否则,开始播放后,应该层叠在其上的决对定位的元素不会显示。
  384. top: 0;
  385. left: 0;
  386. width: 100%;
  387. height: 100%;
  388. background: #1f0f05;
  389. object-fit: cover;
  390. }
  391. }
  392. </style>