HistoryNew.vue 20 KB

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