HistoryNew.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. <template>
  2. <div
  3. class="history-view"
  4. @mousedown.passive="onMouseDown"
  5. @mousemove.passive="onMouseMove"
  6. @mouseup="onMouseUp"
  7. @mouseleave="onMouseLeave"
  8. >
  9. <video
  10. src="@/assets/videos/bg-history.mp4"
  11. class="bg"
  12. autoplay
  13. loop
  14. />
  15. <div
  16. class="gear-wrap"
  17. @click="doTest"
  18. >
  19. <img
  20. class="gear"
  21. :src="require(`@/assets/images/gear/表盘1_${gearFrameIdx.toString().padStart(5, '0')}.png`)"
  22. alt=""
  23. draggable="false"
  24. >
  25. </div>
  26. <div
  27. v-if="currentTimeIdx !== null"
  28. class="current-stage-name"
  29. >
  30. <h1>{{ stageList[currentTimeIdx].name }}</h1>
  31. <img
  32. class="underline"
  33. src="@/assets/images/underline-history-stage.png"
  34. alt=""
  35. draggable="false"
  36. >
  37. </div>
  38. <div
  39. class="time-axis-wrap"
  40. :style="{
  41. left: `${timeAxisLeft}px`,
  42. }"
  43. >
  44. <div
  45. v-for="index in timeAxisScaleRepeat"
  46. :key="index"
  47. class="scale-line"
  48. :style="{
  49. width: `${timeAxisScaleWidth}px`,
  50. marginRight: `${timeAxisScaleMargin}px`,
  51. opacity: computeTimeAxisScaleOpacity(index),
  52. }"
  53. />
  54. <div
  55. v-for="index in timeAxisScaleRepeat"
  56. :key="index"
  57. class="scale-line"
  58. :style="{
  59. width: `${timeAxisScaleWidth}px`,
  60. marginRight: `${timeAxisScaleMargin}px`,
  61. opacity: computeTimeAxisScaleOpacity(index + timeAxisScaleRepeat),
  62. }"
  63. />
  64. </div>
  65. <div
  66. v-for="stageLabel in stageLabelList"
  67. :key="stageLabel.name"
  68. class="stage-label"
  69. :style="{
  70. left: stageLabel.left + 'px',
  71. }"
  72. >
  73. {{ stageLabel.startTime }}
  74. <img
  75. draggable="false"
  76. src="@/assets/images/star.png"
  77. alt=""
  78. class="star animation-show-hide"
  79. >
  80. </div>
  81. <HistoryPersonCard
  82. v-for="(item) in personList"
  83. :key="item.id"
  84. class="history-person-card"
  85. :name="item.name"
  86. :img="item.img"
  87. :text="item.description || item.story"
  88. :tall-or-fat="item.tallOrFat"
  89. :style="{
  90. left: item.left + 'px',
  91. top: item.top + 'px',
  92. zIndex: item.zIndex,
  93. filter: item.filter,
  94. }"
  95. />
  96. </div>
  97. </template>
  98. <script>
  99. import { reactive, toRefs, ref, watch, onMounted, onBeforeUnmount, computed } from 'vue'
  100. import HistoryPersonCard from "@/components/HistoryPersonCard.vue"
  101. import axios from "axios"
  102. export default {
  103. components: {
  104. HistoryPersonCard,
  105. },
  106. setup () {
  107. const spaceEachPerson = 220
  108. // 时代阶段
  109. const stageList = reactive([
  110. {
  111. name: '开埠通商',
  112. startTime: '1843',
  113. endTime: '1894',
  114. startTimeFriendly: '19世纪40年代',
  115. endTimeFriendly: '19世纪90年代',
  116. components: '上海近代工业蹒跚起步',
  117. },
  118. {
  119. name: '曲折发展',
  120. startTime: '1895',
  121. endTime: '1936',
  122. startTimeFriendly: '19世纪90年代',
  123. endTimeFriendly: '20世纪30年代',
  124. components: '上海近代工业起起伏伏',
  125. },
  126. {
  127. name: '步履维艰',
  128. startTime: '1937',
  129. endTime: '1949',
  130. startTimeFriendly: '20世纪30年代',
  131. endTimeFriendly: '20世纪50年代',
  132. components: '上海民族工业几经崩溃',
  133. },
  134. {
  135. name: '筚路蓝缕',
  136. startTime: '1950',
  137. endTime: '1978',
  138. startTimeFriendly: '20世纪50年代',
  139. endTimeFriendly: '20世纪70年代',
  140. components: '上海现代工业扬帆起航',
  141. },
  142. {
  143. name: '改革开放',
  144. startTime: '1979',
  145. endTime: '1996',
  146. startTimeFriendly: '20世纪70年代',
  147. endTimeFriendly: '20世纪90年代',
  148. components: '上海工业凤凰涅槃',
  149. },
  150. {
  151. name: '战略负重',
  152. startTime: '1997',
  153. endTime: '2011',
  154. startTimeFriendly: '20世纪90年代',
  155. endTimeFriendly: '本世纪10年代',
  156. components: '上海工业筑梦前行',
  157. },
  158. {
  159. name: '创新驱动',
  160. startTime: '2012',
  161. endTime: '2020',
  162. startTimeFriendly: '本世纪10年代',
  163. endTimeFriendly: '本世纪20年代',
  164. components: '上海工业转型升级',
  165. },
  166. {
  167. name: '追梦未来',
  168. startTime: '2021',
  169. endTime: '2035',
  170. startTimeFriendly: '本世纪20年代',
  171. endTimeFriendly: '本世纪30年代',
  172. components: '上海工业再创辉煌',
  173. },
  174. ])
  175. // 获取数据
  176. Promise.allSettled(stageList.map((stageItem) => {
  177. return axios({
  178. method: 'post',
  179. url: `https://sit-shgybwg.4dage.com/api/show/history/pageList`,
  180. headers: {
  181. "Content-Type": "application/json",
  182. },
  183. data: {
  184. stage: stageItem.name
  185. },
  186. }).then((res) => {
  187. return res.data.data.records
  188. }).then((res) => {
  189. stageItem.personList = res
  190. const personWidth = spaceEachPerson
  191. stageItem.width = personWidth * res.length
  192. }).then(() => {
  193. // 获取各人物的照片
  194. for (const personItem of stageItem.personList) {
  195. api.getHistoryDetail(personItem.id).then((res) => {
  196. try {
  197. personItem.img = process.env.VUE_APP_API_ORIGIN + res.file[0].filePath
  198. } catch (e) {
  199. console.log(e)
  200. }
  201. })
  202. }
  203. })
  204. })).then((res) => {
  205. // 计算各个时代的起始终止位置
  206. let temp = 0
  207. for (const stageItem of stageList) {
  208. stageItem.startPos = temp
  209. stageItem.endPos = temp + stageItem.width
  210. temp = stageItem.endPos
  211. }
  212. maxTranslateLength.value = stageList[stageList.length - 1].endPos
  213. })
  214. // 用户操作相关
  215. const isMouseDown = ref(false)
  216. const lastMoveEventTimeStamp = ref(0)
  217. const lastCursorPos = ref(0)
  218. // 镜头平移相关
  219. const moveSpeed = ref(0)
  220. const translateLength = ref(0)
  221. const maxTranslateLength = ref(0)
  222. // 用户操作改变镜头平移速度
  223. function onMouseDown(e) {
  224. isMouseDown.value = true
  225. moveSpeed.value = 0
  226. lastMoveEventTimeStamp.value = 0
  227. lastAnimationTimeStamp = Date.now()
  228. lastCursorPos.value = e.clientX
  229. if (beginAutoMoveIntervalId) {
  230. clearInterval(beginAutoMoveIntervalId)
  231. beginAutoMoveIntervalId = null
  232. }
  233. if (isAutoMoving.value) {
  234. isAutoMoving.value = false
  235. }
  236. }
  237. function onMouseUp(e) {
  238. isMouseDown.value = false
  239. beginAutoMoveIntervalId = setInterval(() => {
  240. if (moveSpeed.value === 0) {
  241. isAutoMoving.value = true
  242. }
  243. }, 2000)
  244. }
  245. function onMouseLeave() {
  246. isMouseDown.value = false
  247. }
  248. function onMouseMove(e) {
  249. if (isMouseDown.value) {
  250. // 疯狂操作的极端情况下两个时间戳之间的时差会不合理,甚至为0
  251. if (lastMoveEventTimeStamp.value && (e.timeStamp - lastMoveEventTimeStamp.value > 1)) {
  252. // 更新speed
  253. const currentMoveSpeed = - (e.clientX - lastCursorPos.value) / (e.timeStamp - lastMoveEventTimeStamp.value) * 1
  254. moveSpeed.value = moveSpeed.value * 0.9 + currentMoveSpeed * 0.1
  255. lastCursorPos.value = e.clientX
  256. }
  257. lastMoveEventTimeStamp.value = e.timeStamp
  258. }
  259. }
  260. /**
  261. * 镜头自动平移
  262. */
  263. let isAutoMoving = ref(false)
  264. let beginAutoMoveIntervalId = null
  265. watch(isAutoMoving, (vNew) => {
  266. if (vNew) {
  267. moveSpeed.value = 0.03
  268. } else {
  269. moveSpeed.value = 0
  270. }
  271. })
  272. onBeforeUnmount(() => {
  273. clearInterval(beginAutoMoveIntervalId)
  274. })
  275. /**
  276. * 动画帧相关
  277. */
  278. let lastAnimationTimeStamp = 0
  279. let animationFrameId = null
  280. // 在每一帧更新镜头速度、位置
  281. function animationFrameTask() {
  282. const timeStamp = Date.now()
  283. const timeElapsed = timeStamp - lastAnimationTimeStamp
  284. if (!isAutoMoving.value) {
  285. // 速度减慢
  286. if (moveSpeed.value > 0) {
  287. moveSpeed.value -= ($.valueisMobile ? 0.001 : 0.003) * timeElapsed
  288. if (moveSpeed.value < 0) {
  289. moveSpeed.value = 0
  290. }
  291. } else if (moveSpeed.value < 0) {
  292. moveSpeed.value += ($.valueisMobile ? 0.001 : 0.003) * timeElapsed
  293. if (moveSpeed.value > 0) {
  294. moveSpeed.value = 0
  295. }
  296. }
  297. }
  298. // 根据速度更新位置
  299. translateLength.value += moveSpeed.value * timeElapsed
  300. if (translateLength.value < 0) {
  301. translateLength.value = 0
  302. moveSpeed.value = 0
  303. } else if (translateLength.value > maxTranslateLength.value) {
  304. if (isAutoMoving.value) {
  305. translateLength.value = 0
  306. } else {
  307. translateLength.value = maxTranslateLength.value
  308. moveSpeed.value = 0
  309. }
  310. }
  311. lastAnimationTimeStamp = timeStamp
  312. animationFrameId = requestAnimationFrame(animationFrameTask)
  313. }
  314. onMounted(() => {
  315. animationFrameId = requestAnimationFrame(animationFrameTask)
  316. })
  317. onBeforeUnmount(() => {
  318. cancelAnimationFrame(animationFrameId)
  319. })
  320. // 动画
  321. // 恢复历史位置
  322. // if (this.longImageTranslateLengthRecord) {
  323. // this.translateLength = this.longImageTranslateLengthRecord
  324. // this.setLongImageTranslateLengthRecord(null)
  325. // }
  326. /**
  327. * 当前时代相关
  328. */
  329. const currentTimeIdx = ref(0)
  330. watch(translateLength, (vNew) => {
  331. for (let index = 0; index < stageList.length; index++) {
  332. const stageItem = stageList[index]
  333. if (vNew >= (stageItem.startPos - window.innerWidth / 7) && vNew <= (stageItem.endPos - window.innerWidth / 7)) {
  334. currentTimeIdx.value = index
  335. break
  336. } else {
  337. currentTimeIdx.value = null
  338. }
  339. }
  340. })
  341. // 时间轴
  342. const timeAxisLeft = ref(0)
  343. const timeAxisScaleWidth = 2
  344. const timeAxisScaleMargin = 120
  345. const timeAxisScaleRepeat = 20
  346. watch(translateLength, (vNew) => {
  347. timeAxisLeft.value = -(vNew % ((timeAxisScaleWidth + timeAxisScaleMargin) * timeAxisScaleRepeat))
  348. })
  349. const windowCenterX = window.innerWidth / 2
  350. function computeTimeAxisScaleOpacity(index) { // 注意index是从1开始的
  351. const scaleCenterXInWindow = (timeAxisScaleWidth + timeAxisScaleMargin) * (index - 1) + timeAxisScaleWidth / 2 + timeAxisLeft.value
  352. return String(Math.max(1 - Math.abs(windowCenterX - scaleCenterXInWindow) / (windowCenterX), 0))
  353. }
  354. // 齿轮
  355. const gearFrameNumberTotal = 24
  356. const gearFrameIdx = ref(0)
  357. const gearFrameNumberPerScale = 3
  358. const translateLengthPerGrearFrame = (timeAxisScaleWidth + timeAxisScaleMargin) / gearFrameNumberPerScale
  359. watch(translateLength, (vNew) => {
  360. const framePassed = Math.round(translateLength.value / translateLengthPerGrearFrame)
  361. if (framePassed >= 0) {
  362. gearFrameIdx.value = framePassed % gearFrameNumberTotal
  363. } else {
  364. gearFrameIdx.value = gearFrameNumberTotal + framePassed % gearFrameNumberTotal - 1
  365. }
  366. })
  367. // 各个时代标签
  368. const stageLabelList = computed(() => {
  369. return stageList.map((item) => {
  370. if (item?.personList?.length) {
  371. return {
  372. name: item.name,
  373. initialLeft: (item.startPos + 10) * 3,
  374. left: (item.startPos + 10) * 3,
  375. startTime: item.startTime + '年',
  376. }
  377. } else {
  378. return undefined
  379. }
  380. }).filter((item) => {
  381. return !!item
  382. })
  383. })
  384. watch(translateLength, (vNew) => {
  385. for (const iterator of stageLabelList.value) {
  386. iterator.left = iterator.initialLeft - vNew * 3
  387. }
  388. })
  389. // 各个人物
  390. const personList = computed(() => {
  391. const ret = []
  392. stageList.map((stageItem) => {
  393. if (stageItem.personList) {
  394. for (const personItem of stageItem.personList) {
  395. const tallOrFat = (ret.length % 2 === 0 ? 'tall' : 'fat')
  396. ret.push({
  397. ...personItem,
  398. initialLeft: (ret.length * spaceEachPerson + spaceEachPerson / (tallOrFat === 'tall' ? 2 : 1.3)) * (tallOrFat === 'tall' ? 2 : 1.5),
  399. left: (ret.length * spaceEachPerson + spaceEachPerson / (tallOrFat === 'tall' ? 2 : 1.3)) * (tallOrFat === 'tall' ? 2 : 1.5),
  400. top: Math.random() * 70 + (tallOrFat === 'tall' ? 250 : 350),
  401. tallOrFat,
  402. zIndex: tallOrFat === 'tall' ? 5 : 4,
  403. filter: `brightness(${tallOrFat === 'tall' ? '1' : '0.75'})`,
  404. })
  405. }
  406. }
  407. })
  408. return ret
  409. })
  410. watch(translateLength, (vNew) => {
  411. for (const iterator of personList.value) {
  412. iterator.left = iterator.initialLeft - vNew * (iterator.tallOrFat === 'tall' ? 2 : 1.5)
  413. }
  414. })
  415. return {
  416. onMouseDown,
  417. onMouseMove,
  418. onMouseUp,
  419. onMouseLeave,
  420. timeAxisLeft,
  421. timeAxisScaleWidth,
  422. timeAxisScaleMargin,
  423. timeAxisScaleRepeat,
  424. computeTimeAxisScaleOpacity,
  425. gearFrameIdx,
  426. stageList,
  427. currentTimeIdx,
  428. stageLabelList,
  429. personList,
  430. }
  431. }
  432. }
  433. </script>
  434. <style lang="less" scoped>
  435. .history-view {
  436. position: absolute;
  437. left: 0;
  438. top: 0;
  439. width: 100%;
  440. height: 100%;
  441. user-select: none;
  442. >video.bg{
  443. position: absolute;
  444. left: 0;
  445. top: 0;
  446. width: 100%;
  447. height: 100%;
  448. object-fit: cover;
  449. }
  450. >.gear-wrap {
  451. position: absolute;
  452. top: 0;
  453. left: 50%;
  454. transform: translateX(-50%);
  455. width: calc(100vw * 940 / 1920);
  456. >img.gear{
  457. width: 100%;
  458. }
  459. }
  460. >.current-stage-name {
  461. position: absolute;
  462. left: 50%;
  463. top: 10.5vw;
  464. transform: translateX(-50%);
  465. width: 250px;
  466. text-align: center;
  467. >h1{
  468. font-size: 36px;
  469. font-family: Source Han Sans CN-Bold, Source Han Sans CN;
  470. font-weight: bold;
  471. color: #FFFFFF;
  472. line-height: 42px;
  473. }
  474. >img.underline{
  475. margin-top: -20px;
  476. width: 100%;
  477. }
  478. }
  479. >.time-axis-wrap{
  480. bottom: 32.4%;
  481. position: absolute;
  482. display: flex;
  483. >.scale-line{
  484. height: 30px;
  485. background-color: #91886b;
  486. }
  487. }
  488. >.stage-label{
  489. position: absolute;
  490. bottom: 15.7%;
  491. font-size: 96px;
  492. font-family: Source Han Sans CN-Bold, Source Han Sans CN;
  493. font-weight: bold;
  494. color: #FFFFFF;
  495. line-height: 97px;
  496. text-shadow: 10px 0px 0 black;
  497. z-index: 10;
  498. width: 407px;
  499. text-align: left;
  500. >img.star{
  501. position: absolute;
  502. left: 0;
  503. top: 0;
  504. transform: translate(-50%, -80%);
  505. }
  506. }
  507. >.history-person-card{
  508. position: absolute;
  509. }
  510. }
  511. </style>