tours.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. <template>
  2. <div
  3. class="tour-list 333"
  4. v-if="tours.length > 0"
  5. :class="{ ban: flying || isSelect, barshow: showTours }"
  6. >
  7. <teleport to=".kankan-app">
  8. <!-- 导览字幕 -->
  9. <div
  10. class="tours-captions"
  11. :class="{ active: showTours }"
  12. v-if="tours.length > 0 && isPlay && !tours[partId].list[frameId].tagId"
  13. >
  14. <!-- <div class="tours-captions" :class="{ active: showTours }" > -->
  15. <div class="captions-title" v-if="tours[partId] && tours[partId].title">
  16. {{ tours[partId].title || "" }}
  17. </div>
  18. <div
  19. class="captions-desc"
  20. v-if="tours[partId] && tours[partId].description"
  21. >
  22. {{ tours[partId].description || "" }}
  23. </div>
  24. </div>
  25. </teleport>
  26. <div class="part-content" ref="tourScroll">
  27. <!-- 多个片段 -->
  28. <ul class="part-list" v-if="tours.length > 1">
  29. <li
  30. class="part-item"
  31. v-for="(item, index) in tours"
  32. :key="index"
  33. @click="changeFrame(1, index)"
  34. :class="{
  35. active: partId == index && progressNum > 0,
  36. loopspan: item.name.length > spanlength && partId == index,
  37. disabled: isPlay && partId != index,
  38. }"
  39. :name="index"
  40. >
  41. <span v-if="partId == index">{{ item.name }}</span>
  42. <span v-else>{{
  43. item.name.length > spanlength
  44. ? item.name.slice(0, spanlength)
  45. : item.name
  46. }}</span>
  47. <div v-if="partId == index && progressNum > 0" class="tourbar">
  48. <div :style="`width:${progressNum}%;`" class="tourline"></div>
  49. </div>
  50. </li>
  51. </ul>
  52. <!-- 只有一个片段 -->
  53. <ul class="part-list part-frame" v-else>
  54. <li
  55. class="part-item"
  56. v-for="(item, index) in tours[0].list"
  57. :key="index"
  58. @click="changeFrame(2, index)"
  59. :style="`background-image:url(${common.changeUrl(
  60. item.enter.cover
  61. )});`"
  62. :class="{
  63. active: frameId == index && progressNum > 0,
  64. activeborder: frameId == index && progressNum <= 0,
  65. disabled: isPlay && frameId != index,
  66. }"
  67. :name="index"
  68. >
  69. <div v-if="frameId == index && progressNum > 0" class="tourbar">
  70. <div :style="`width:${progressNum}%;`" class="tourline"></div>
  71. </div>
  72. </li>
  73. </ul>
  74. </div>
  75. </div>
  76. </template>
  77. <script setup>
  78. import { computed, inject, onMounted, watch, ref, nextTick } from "vue";
  79. import { Scrollbar, Dialog } from "@/global_components";
  80. import { useApp, getApp } from "@/app";
  81. import { useStore } from "vuex";
  82. import common from "@/utils/common";
  83. import { useMusicPlayer } from "@/utils/sound";
  84. import { watchEffect } from "vue";
  85. const musicPlayer = useMusicPlayer();
  86. const spanlength = ref(5);
  87. const triggerTour = inject("triggerTour");
  88. const isOpenTours = inject("isOpenTours");
  89. let timer = null;
  90. const isSelect = ref(false);
  91. const store = useStore();
  92. const tourScroll = ref(null);
  93. const flying = computed(() => store.getters["flying"]);
  94. const controls = computed(() => store.getters["scene/metadata"].controls || {});
  95. const showTours = computed(() => store.getters["tour/showTours"]);
  96. const partId = computed(() => {
  97. let id = store.getters["tour/partId"];
  98. if (isPlay.value) {
  99. slideScroll();
  100. }
  101. return id;
  102. });
  103. const frameId = computed(() => {
  104. let id = store.getters["tour/frameId"];
  105. if (isPlay.value) {
  106. slideScroll();
  107. }
  108. return id;
  109. });
  110. const progressNum = ref(0);
  111. const isPlay = computed(() => {
  112. let status = store.getters["tour/isPlay"];
  113. let map = document.querySelector(".kankan-app div[xui_min_map]");
  114. if (map) {
  115. if (status) {
  116. map.classList.add("disabled");
  117. } else {
  118. map.classList.remove("disabled");
  119. }
  120. }
  121. return status;
  122. });
  123. const isInit = ref(false);
  124. const tours = computed(() => {
  125. let tours = store.getters["tour/tours"];
  126. return tours;
  127. });
  128. const onModeChange = (name) => {
  129. store.commit("setMode", name);
  130. };
  131. const playTour = async () => {
  132. let player = await getApp().TourManager.player;
  133. if (isPlay.value) {
  134. store.commit("tour/setData", { isPlay: true });
  135. player.pause();
  136. } else {
  137. store.commit("tour/setData", { isPlay: true });
  138. player.play(partId.value);
  139. }
  140. };
  141. const hanlderTourPartPlay = (time) => {
  142. if (!timer) {
  143. timer = KanKan.Animate.transitions.start((progress) => {
  144. progressNum.value = progress * 100;
  145. }, time);
  146. }
  147. };
  148. const cancelTimer = () => {
  149. if (timer) {
  150. KanKan.Animate.transitions.cancel(timer);
  151. timer = null;
  152. }
  153. };
  154. const slideScroll = () => {
  155. nextTick(() => {
  156. let t = setTimeout(() => {
  157. clearTimeout(t);
  158. let id = tours.value.length > 1 ? partId.value : frameId.value;
  159. let item = document.querySelector(`.part-item[name="${id}"]`);
  160. item.scrollIntoView({
  161. block: "center",
  162. behavior: "smooth",
  163. inline: "center",
  164. });
  165. }, 100);
  166. });
  167. };
  168. const hanlderTour = async () => {
  169. let player = await getApp().TourManager.player;
  170. player.on("play", (data) => {
  171. // musicPlayer.pause(true)
  172. window.parent.postMessage(
  173. {
  174. source: "qjkankan",
  175. event: "toggleBgmStatus",
  176. params: {
  177. status: false,
  178. },
  179. },
  180. "*"
  181. );
  182. });
  183. player.on("pause", (tours) => {
  184. console.log("pause", player);
  185. // musicPlayer.resume()
  186. window.parent.postMessage(
  187. {
  188. source: "qjkankan",
  189. event: "toggleBgmStatus",
  190. params: {
  191. status: true,
  192. },
  193. },
  194. "*"
  195. );
  196. progressNum.value = 0;
  197. cancelTimer();
  198. store.commit("tour/setData", { isPlay: false });
  199. });
  200. player.on("end", (tours) => {
  201. // musicPlayer.resume()
  202. window.parent.postMessage(
  203. {
  204. source: "qjkankan",
  205. event: "toggleBgmStatus",
  206. params: {
  207. status: true,
  208. },
  209. },
  210. "*"
  211. );
  212. progressNum.value = 100;
  213. slideScroll();
  214. store.commit("tour/setData", { isPlay: false });
  215. cancelTimer();
  216. });
  217. let currPartId = null;
  218. let currProgress = 0;
  219. let currFrames = 0;
  220. player.on("progress", (data) => {
  221. if (tours.value.length == 1) {
  222. progressNum.value = data.progress * 100;
  223. } else {
  224. if (currPartId != data.partId) {
  225. currPartId = data.partId;
  226. currFrames = tours.value[data.partId].list.length;
  227. currProgress = 0;
  228. } else {
  229. currProgress += data.progress / currFrames;
  230. if (currProgress >= 100) {
  231. currProgress = 100;
  232. }
  233. progressNum.value = currProgress;
  234. }
  235. }
  236. store.commit("tour/setData", {
  237. partId: data.partId,
  238. frameId: data.frameId,
  239. isPlay: true,
  240. });
  241. });
  242. };
  243. const getPartTime = (partId) => {
  244. cancelTimer();
  245. let time = 0;
  246. for (let i = 0; i < tours.value[partId].list.length; i++) {
  247. if (!tours.value[partId].list[i]._end) {
  248. time += tours.value[partId].list[i].time - 0;
  249. if (!tours.value[partId].list[i]._notrans) {
  250. time += 1000;
  251. }
  252. }
  253. }
  254. return time;
  255. };
  256. const openTours = () => {
  257. // showTours.value = !showTours.value
  258. store.commit("tour/setData", { showTours: !showTours.value });
  259. nextTick(() => {
  260. if (isPlay.value) {
  261. slideScroll();
  262. }
  263. });
  264. };
  265. const changeFrame = async (type, id) => {
  266. progressNum.value = 0;
  267. // recorder.selectFrame(id)
  268. let player = await getApp().TourManager.player;
  269. // player.selectFrame(id)
  270. isSelect.value = true;
  271. if (type == 1) {
  272. player.selectPart(id);
  273. console.log(tours.value[id].frameId);
  274. let f_id = 0;
  275. if (tours.value[id].frameId) {
  276. f_id = tours.value[id].frameId;
  277. }
  278. player.selectFrame(f_id).then(() => {
  279. isSelect.value = false;
  280. });
  281. store.commit("tour/setData", {
  282. frameId: f_id,
  283. partId: id,
  284. });
  285. } else {
  286. player.selectFrame(id).then(() => {
  287. isSelect.value = false;
  288. });
  289. store.commit("tour/setData", {
  290. frameId: id,
  291. });
  292. }
  293. slideScroll();
  294. };
  295. const onClickHandler = async () => {
  296. if (isPlay.value) {
  297. let player = await getApp().TourManager.player;
  298. player.pause();
  299. // musicPlayer.resume()
  300. window.parent.postMessage(
  301. {
  302. source: "qjkankan",
  303. event: "toggleBgmStatus",
  304. params: {
  305. status: true,
  306. },
  307. },
  308. "*"
  309. );
  310. }
  311. };
  312. watch(triggerTour, () => {
  313. playTour();
  314. });
  315. watch(isOpenTours, () => {
  316. openTours();
  317. });
  318. watch(isPlay, () => {
  319. window.parent.postMessage(
  320. {
  321. source: "qjkankan",
  322. event: "isPlayTours",
  323. params: {
  324. isPlay: isPlay.value,
  325. },
  326. },
  327. "*"
  328. );
  329. });
  330. onMounted(() => {
  331. useApp().then(async (sdk) => {
  332. hanlderTour();
  333. });
  334. nextTick(() => {
  335. let player = document.querySelector('.player[name="main"]');
  336. player.addEventListener("click", onClickHandler);
  337. });
  338. watchEffect(() => {
  339. if (tours.length > 0 && tourScroll.value && !isInit.value) {
  340. isInit.value = true;
  341. new Scrollbar(tourScroll.value, { onlyHorizontal: true });
  342. }
  343. });
  344. });
  345. </script>
  346. <style lang="scss" scoped>
  347. $width: 1150px;
  348. .controls-left-buttons {
  349. margin-left: 20px;
  350. margin-bottom: 20px;
  351. display: flex;
  352. }
  353. .buttons.tour {
  354. margin-right: 10px;
  355. > div {
  356. margin-left: 0px;
  357. margin-right: 0px;
  358. padding: 0 10px;
  359. &.show-list {
  360. border-left: solid 1px var(--editor-font-color);
  361. }
  362. .icon-pull-down {
  363. font-size: 12px;
  364. }
  365. span {
  366. right: -10px;
  367. }
  368. }
  369. }
  370. .tour-list {
  371. position: fixed;
  372. bottom: 68px;
  373. left: 50%;
  374. transform: translateX(-50%);
  375. text-align: center;
  376. max-width: $width;
  377. overflow: hidden;
  378. max-height: 0;
  379. transition: 0.3s all ease;
  380. z-index: 9;
  381. &.ban {
  382. pointer-events: none;
  383. }
  384. .part-content {
  385. display: flex;
  386. flex-direction: row;
  387. overflow: hidden;
  388. padding: 10px 30px;
  389. background: linear-gradient(
  390. 268deg,
  391. rgba(0, 0, 0, 0) 0%,
  392. rgba(0, 0, 0, 0.5) 8%,
  393. rgba(0, 0, 0, 0.5) 92%,
  394. rgba(0, 0, 0, 0) 100%
  395. );
  396. .part-list {
  397. display: flex;
  398. > li {
  399. width: 90px;
  400. height: 40px;
  401. background: rgba(24, 24, 24, 0.5);
  402. line-height: 40px;
  403. margin: 0 6px;
  404. cursor: pointer;
  405. border-radius: 4px;
  406. > span,
  407. > div > span {
  408. cursor: pointer;
  409. display: inline-block;
  410. color: rgba(255, 255, 255, 0.8);
  411. }
  412. &.loopspan {
  413. > span,
  414. > div > span {
  415. animation: 5s wordsLoop linear infinite normal;
  416. }
  417. }
  418. &.active {
  419. position: relative;
  420. > span {
  421. color: var(--colors-primary-base);
  422. }
  423. .tourbar {
  424. position: absolute;
  425. width: 78px;
  426. left: 6px;
  427. right: 6px;
  428. bottom: 4px;
  429. height: 2px;
  430. border-radius: 2px;
  431. background: #000;
  432. overflow: hidden;
  433. .tourline {
  434. width: 50%;
  435. height: 100%;
  436. background: var(--colors-primary-base);
  437. }
  438. }
  439. }
  440. &:hover {
  441. opacity: 0.7;
  442. }
  443. }
  444. .activeborder {
  445. border: 1px solid var(--editor-main-color);
  446. }
  447. }
  448. .part-frame {
  449. > li {
  450. width: 120px;
  451. height: 80px;
  452. background-size: cover;
  453. &.active {
  454. .tourbar {
  455. position: absolute;
  456. width: 100%;
  457. left: 0;
  458. right: 0;
  459. bottom: 0;
  460. height: 4px;
  461. background: #000;
  462. overflow: hidden;
  463. opacity: 0.5;
  464. .tourline {
  465. width: 50%;
  466. height: 100%;
  467. background: var(--colors-primary-base);
  468. }
  469. }
  470. }
  471. }
  472. }
  473. }
  474. }
  475. .barshow {
  476. max-height: 102px;
  477. }
  478. @keyframes wordsLoop {
  479. 0% {
  480. transform: translateX(100%);
  481. -webkit-transform: translateX(100%);
  482. }
  483. 100% {
  484. transform: translateX(-180%);
  485. -webkit-transform: translateX(-180%);
  486. }
  487. }
  488. </style>
  489. <style lang="scss">
  490. .tours-captions {
  491. position: absolute;
  492. bottom: 64px;
  493. left: 20px;
  494. width: 480px;
  495. word-break: break-all;
  496. text-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
  497. text-align: justify;
  498. color: #fff;
  499. pointer-events: none;
  500. z-index: 1;
  501. transition: all 0.3s;
  502. &.active {
  503. bottom: 5rem;
  504. }
  505. .captions-title {
  506. font-size: 28px;
  507. margin-bottom: 10px;
  508. font-weight: 700;
  509. }
  510. .captions-desc {
  511. font-size: 16px;
  512. line-height: 24px;
  513. }
  514. }
  515. </style>