QrCode.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. <script setup lang="ts">
  2. import { useI18n } from 'vue-i18n'
  3. import { useQrCodePopup } from '~/composables/useQrCodePopup'
  4. import QrcodeVue from 'qrcode.vue'
  5. import { skillApi } from '~/api/skill'
  6. import { useAuth } from '~/composables/useAuth'
  7. import type { SkillSimpleDTO } from '~/types/api'
  8. import { sleep, buildPng } from '~/utils/helpers'
  9. const {
  10. state,
  11. formattedPrice,
  12. currencyAmount,
  13. isGeneralTab,
  14. isSpecificForm,
  15. switchTab,
  16. setSpecificQty,
  17. generateSpecificQr,
  18. close,
  19. handleCopyLink,
  20. updateQrcodeValue,
  21. updateCurrencyAmount,
  22. updateSkillInfo,
  23. resetSpecificStep,
  24. } = useQrCodePopup()
  25. const { open: openSelectListPopup, state: selectListState } = useSelectListPopup()
  26. const { playmateInfo, userProfile } = useAuth()
  27. const { t } = useI18n()
  28. const skills = computed<SkillSimpleDTO[]>(() => playmateInfo.value?.skills ?? [])
  29. const specificTotalPrice = computed(() => (state.price || 0) * (state.specificQty || 0))
  30. const formattedSpecificTotalPrice = computed(() =>
  31. specificTotalPrice.value ? specificTotalPrice.value.toLocaleString() : '0',
  32. )
  33. const saveImagePopupVisible = ref(false)
  34. const saveImageRef = ref<HTMLElement | null>(null)
  35. const handleSpecificQtyChange = (value: number) => {
  36. setSpecificQty(value)
  37. }
  38. const initSkillFromPlaymate = () => {
  39. if (state.skillId) return
  40. const list = skills.value
  41. if (!list.length) return
  42. const first = list[0]
  43. if (!first) return
  44. updateSkillInfo({
  45. skillId: first.id,
  46. skillName: first.name,
  47. skillIcon: first.icon,
  48. price: first.price,
  49. unit: first.unit,
  50. })
  51. }
  52. const generateQrCode = async (type: 1 | 2) => {
  53. if (!state.skillId) return
  54. try {
  55. const payload = {
  56. skillId: state.skillId,
  57. type,
  58. purchaseQty: type === 2 ? state.specificQty : undefined,
  59. }
  60. const result = await skillApi.createOrderQrcode(payload)
  61. const origin = window.location.origin
  62. if (result?.qrCode) {
  63. const url = `${origin}/user/category?id=${state.skillId}&code=${result.qrCode}`
  64. updateQrcodeValue(url)
  65. if (typeof result.currencyAmount === 'number')
  66. updateCurrencyAmount(result.currencyAmount)
  67. }
  68. else {
  69. console.error('No QR Code returned', result)
  70. }
  71. }
  72. catch (e) {
  73. console.error('Failed to generate QR code:', e)
  74. }
  75. }
  76. const handleSaveImage = async () => {
  77. // 展示保存图片的预览卡片
  78. saveImagePopupVisible.value = true
  79. if (typeof window === 'undefined')
  80. return
  81. await sleep(1000)
  82. const node = saveImageRef.value
  83. if (!node)
  84. return
  85. try {
  86. const dataUrl = await buildPng(node as HTMLElement)
  87. const link = document.createElement('a')
  88. link.download = 'gami-qrcode.png'
  89. link.href = dataUrl
  90. link.click()
  91. }
  92. catch (error) {
  93. console.error('Failed to save QR image', error)
  94. }
  95. finally {
  96. saveImagePopupVisible.value = false
  97. }
  98. }
  99. watch(
  100. () => playmateInfo.value,
  101. () => {
  102. if (!state.visible) return
  103. initSkillFromPlaymate()
  104. },
  105. { deep: true },
  106. )
  107. watch(() => [state.visible, state.activeTab, state.skillId], ([visible, tab, skillId]) => {
  108. if (!visible) return
  109. initSkillFromPlaymate()
  110. if (tab === 'general' && skillId) {
  111. generateQrCode(1)
  112. }
  113. else if (tab === 'specific' && skillId) {
  114. resetSpecificStep()
  115. }
  116. })
  117. watch(() => state.specificStep, (step) => {
  118. if (step === 'preview' && state.activeTab === 'specific') {
  119. generateQrCode(2)
  120. }
  121. })
  122. const handleSkillSelect = () => {
  123. openSelectListPopup({
  124. options: skills.value.map(s => ({
  125. label: s.name,
  126. value: s.id,
  127. icon: s.icon,
  128. })),
  129. defaultValue: state.skillId,
  130. onSelect: (value, _option) => {
  131. const skill = skills.value.find(s => s.id === value)
  132. if (skill) {
  133. updateSkillInfo({
  134. skillId: skill.id,
  135. skillName: skill.name,
  136. skillIcon: skill.icon,
  137. price: skill.price,
  138. unit: skill.unit,
  139. })
  140. }
  141. },
  142. })
  143. }
  144. const handleUpdateShow = (value: boolean) => {
  145. if (!value) {
  146. close()
  147. }
  148. }
  149. </script>
  150. <template>
  151. <van-popup
  152. :show="state.visible"
  153. round
  154. position="bottom"
  155. class="qr-popup"
  156. @update:show="handleUpdateShow"
  157. >
  158. <div class="qr-popup__container">
  159. <!-- Header: game info -->
  160. <div
  161. class="qr-popup__header"
  162. @click="handleSkillSelect"
  163. >
  164. <div class="qr-popup__header-left">
  165. <div class="qr-popup__skill-icon">
  166. <img
  167. v-if="state.skillIcon"
  168. :src="state.skillIcon"
  169. :alt="state.skillName"
  170. loading="lazy"
  171. >
  172. </div>
  173. <span class="qr-popup__title">
  174. {{ state.skillName }}
  175. </span>
  176. <van-icon
  177. name="arrow"
  178. class="qr-popup__header-arrow"
  179. :class="{ 'qr-popup__header-arrow--active': selectListState.visible }"
  180. size="14"
  181. />
  182. </div>
  183. </div>
  184. <!-- Tabs -->
  185. <div class="qr-popup__tabs">
  186. <button
  187. type="button"
  188. class="qr-popup__tab"
  189. :class="{ 'qr-popup__tab--active': isGeneralTab }"
  190. @click="switchTab('general')"
  191. >
  192. {{ t('qrCode.tabs.general') }}
  193. </button>
  194. <button
  195. type="button"
  196. class="qr-popup__tab"
  197. :class="{ 'qr-popup__tab--active': !isGeneralTab }"
  198. @click="switchTab('specific')"
  199. >
  200. {{ t('qrCode.tabs.specific') }}
  201. </button>
  202. </div>
  203. <!-- 特定码 Tab - 状态 1:输入金额 + 调整时间 -->
  204. <div
  205. v-if="isSpecificForm"
  206. class="qr-popup__specific"
  207. >
  208. <section class="qr-popup__specific-card">
  209. <div class="qr-popup__specific-amount-row">
  210. <span
  211. class="qr-popup__coin qr-popup__coin--small"
  212. aria-hidden="true"
  213. />
  214. <span class="qr-popup__specific-price-value">
  215. {{ formattedPrice }}
  216. </span>
  217. <span
  218. v-if="state.unit"
  219. class="qr-popup__unit"
  220. >
  221. /{{ state.unit }}
  222. </span>
  223. </div>
  224. <div class="qr-popup__specific-divider" />
  225. <div class="qr-popup__specific-time-row">
  226. <span class="qr-popup__time-label">
  227. Times ({{ state.unit }})
  228. </span>
  229. <CommonStepper
  230. :model-value="state.specificQty"
  231. @update:model-value="handleSpecificQtyChange"
  232. />
  233. </div>
  234. </section>
  235. <div class="qr-popup__specific-price">
  236. <span
  237. class="qr-popup__coin qr-popup__coin--small"
  238. aria-hidden="true"
  239. />
  240. <span
  241. v-if="specificTotalPrice"
  242. class="qr-popup__specific-price-value"
  243. >
  244. {{ formattedSpecificTotalPrice }}
  245. </span>
  246. <span
  247. v-else
  248. class="qr-popup__specific-price-value qr-popup__specific-price-value--disabled"
  249. >
  250. 0
  251. </span>
  252. <span class="qr-popup__unit">
  253. /{{ state.specificQty }} {{ state.unit }}
  254. </span>
  255. </div>
  256. <button
  257. type="button"
  258. class="qr-popup__generate-btn"
  259. :disabled="!specificTotalPrice"
  260. @click="generateSpecificQr"
  261. >
  262. {{ t('qrCode.specific.generate') }}
  263. </button>
  264. </div>
  265. <!-- 通用码 Tab,或特定码 Tab 的状态 2(二维码预览,与通用码一致) -->
  266. <template v-else>
  267. <!-- QR container (placeholder) -->
  268. <div class="qr-popup__qr-wrapper">
  269. <div class="qr-popup__qr-card">
  270. <qrcode-vue
  271. :value="state.qrcodeValue"
  272. :size="145"
  273. :gradient="true"
  274. :gradient-start-color="`#4ED2FF`"
  275. :gradient-end-color="`#B1EF5D`"
  276. :image-settings="{ src: '/logo.png', height: 24, width: 24, excavate: true }"
  277. :level="`H`"
  278. />
  279. </div>
  280. </div>
  281. <!-- Price line -->
  282. <div
  283. v-if="isGeneralTab"
  284. class="qr-popup__price-row"
  285. >
  286. <span
  287. class="qr-popup__coin"
  288. aria-hidden="true"
  289. />
  290. <span class="qr-popup__price">
  291. {{ formattedPrice }}
  292. </span>
  293. <span
  294. v-if="state.unit"
  295. class="qr-popup__unit"
  296. >
  297. /{{ state.unit }}
  298. </span>
  299. </div>
  300. <div
  301. v-else
  302. class="qr-popup__price-row"
  303. >
  304. <span
  305. class="qr-popup__coin"
  306. aria-hidden="true"
  307. />
  308. <span class="qr-popup__price">
  309. {{ formattedSpecificTotalPrice }}
  310. </span>
  311. <span
  312. v-if="state.unit"
  313. class="qr-popup__unit"
  314. >
  315. /{{ state.specificQty }} {{ state.unit }}
  316. </span>
  317. </div>
  318. <!-- Actions -->
  319. <div class="qr-popup__actions">
  320. <button
  321. type="button"
  322. class="qr-popup__btn qr-popup__btn--secondary"
  323. @click="handleSaveImage"
  324. >
  325. {{ t('qrCode.actions.saveImage') }}
  326. </button>
  327. <button
  328. type="button"
  329. class="qr-popup__btn qr-popup__btn--primary"
  330. @click="handleCopyLink"
  331. >
  332. {{ t('qrCode.actions.copyLink') }}
  333. </button>
  334. </div>
  335. </template>
  336. </div>
  337. </van-popup>
  338. <van-popup
  339. :show="saveImagePopupVisible"
  340. position="center"
  341. class="save-image-popup"
  342. :close-on-click-overlay="false"
  343. >
  344. <div
  345. ref="saveImageRef"
  346. class="save-image-popup__container"
  347. >
  348. <div class="save-image-popup__avatar-wrapper">
  349. <div class="save-image-popup__avatar">
  350. <img
  351. :src="userProfile?.avatar ? userProfile?.avatar : '/avatar.png'"
  352. :alt="userProfile?.nickname || 'avatar'"
  353. loading="lazy"
  354. >
  355. </div>
  356. </div>
  357. <p class="save-image-popup__name">
  358. {{ userProfile?.nickname }}
  359. </p>
  360. <CommonStarRating
  361. class="save-image-popup__rating"
  362. :model-value="playmateInfo?.star || 0"
  363. :size="10"
  364. readonly
  365. />
  366. <div class="save-image-popup__qr-card">
  367. <qrcode-vue
  368. :value="state.qrcodeValue"
  369. :size="145"
  370. :gradient="true"
  371. :gradient-start-color="`#4ED2FF`"
  372. :gradient-end-color="`#B1EF5D`"
  373. :image-settings="{ src: '/logo.png', height: 24, width: 24, excavate: true }"
  374. :level="`H`"
  375. />
  376. </div>
  377. <div class="save-image-popup__price-row">
  378. <span
  379. class="save-image-popup__coin"
  380. aria-hidden="true"
  381. />
  382. <span class="save-image-popup__price">
  383. {{ isGeneralTab ? formattedPrice : formattedSpecificTotalPrice }}
  384. </span>
  385. <span class="save-image-popup__symbol">
  386. </span>
  387. <span class="save-image-popup__currency">
  388. IDR {{ currencyAmount }}
  389. </span>
  390. </div>
  391. <button
  392. type="button"
  393. class="save-image-popup__cta"
  394. >
  395. {{ t('qrCode.saveImage.cta') }}
  396. </button>
  397. <div class="save-image-popup__footer">
  398. <div class="save-image-popup__footer-left">
  399. <div class="save-image-popup__game-avatar">
  400. <img
  401. v-if="state.skillIcon"
  402. :src="state.skillIcon"
  403. :alt="state.skillName"
  404. loading="lazy"
  405. >
  406. </div>
  407. <span class="save-image-popup__game-name">
  408. {{ state.skillName }}
  409. </span>
  410. </div>
  411. <span
  412. v-if="!isGeneralTab"
  413. class="save-image-popup__duration"
  414. >
  415. {{ state.specificQty }} {{ state.unit }}
  416. </span>
  417. </div>
  418. </div>
  419. </van-popup>
  420. </template>
  421. <style scoped lang="scss">
  422. .qr-popup {
  423. background-color: transparent;
  424. &__container {
  425. padding: 16px 16px 24px;
  426. border-radius: 20px 20px 0 0;
  427. background-color: var(--color-bg-primary);
  428. display: flex;
  429. flex-direction: column;
  430. gap: 16px;
  431. }
  432. &__header {
  433. display: flex;
  434. align-items: center;
  435. justify-content: space-between;
  436. }
  437. &__header-left {
  438. display: flex;
  439. align-items: center;
  440. gap: 8px;
  441. }
  442. &__skill-icon {
  443. @include size(20px);
  444. border-radius: 999px;
  445. overflow: hidden;
  446. background-color: #e5e6eb;
  447. img {
  448. width: 100%;
  449. height: 100%;
  450. display: block;
  451. object-fit: cover;
  452. }
  453. }
  454. &__title {
  455. font-family: var(--font-title);
  456. font-size: 12px;
  457. font-weight: 600;
  458. color: #1d2129;
  459. }
  460. &__header-arrow {
  461. transition: transform 0.2s ease;
  462. &--active {
  463. transform: rotate(90deg);
  464. }
  465. }
  466. &__tabs {
  467. height: 40px;
  468. padding: 4px;
  469. border-radius: 30px;
  470. background-color: #e5e6eb;
  471. display: flex;
  472. gap: 4px;
  473. }
  474. &__tab {
  475. flex: 1;
  476. border: none;
  477. border-radius: 30px;
  478. background-color: transparent;
  479. font-size: 14px;
  480. font-weight: 600;
  481. color: #4e5969;
  482. cursor: default;
  483. }
  484. &__tab--active {
  485. background-color: #ffffff;
  486. color: #1d2129;
  487. }
  488. &__tab--inactive {
  489. opacity: 0.9;
  490. }
  491. &__qr-wrapper {
  492. display: flex;
  493. justify-content: center;
  494. }
  495. &__qr-card {
  496. width: 168px;
  497. height: 168px;
  498. border-radius: 12px;
  499. background-color: #ffffff;
  500. display: flex;
  501. align-items: center;
  502. justify-content: center;
  503. }
  504. &__price-row {
  505. display: flex;
  506. align-items: center;
  507. justify-content: center;
  508. gap: 4px;
  509. }
  510. &__coin {
  511. @include bg('~/assets/images/common/coin.png');
  512. @include size(24px);
  513. &--small {
  514. @include size(18px);
  515. }
  516. }
  517. &__price {
  518. font-family: var(--font-title);
  519. font-size: 24px;
  520. font-weight: 600;
  521. color: #1d2129;
  522. }
  523. &__unit {
  524. font-size: 14px;
  525. color: #1d2129;
  526. }
  527. &__actions {
  528. margin-top: 8px;
  529. display: flex;
  530. align-items: center;
  531. justify-content: center;
  532. gap: 10px;
  533. }
  534. &__btn {
  535. height: 47px;
  536. padding: 0 24px;
  537. border-radius: 999px;
  538. border: none;
  539. font-family: var(--font-title);
  540. font-size: 16px;
  541. font-weight: 600;
  542. display: flex;
  543. align-items: center;
  544. justify-content: center;
  545. cursor: pointer;
  546. -webkit-tap-highlight-color: transparent;
  547. white-space: nowrap;
  548. }
  549. &__btn--secondary {
  550. min-width: 144px;
  551. padding-inline: 20px;
  552. border: 1px solid #4e5969;
  553. color: #4e5969;
  554. font-weight: 400;
  555. }
  556. &__btn--primary {
  557. min-width: 189px;
  558. flex: 1;
  559. background-image: linear-gradient(90deg, #2f95ff 0%, #50ffd8 100%);
  560. color: #ffffff;
  561. }
  562. &__specific {
  563. display: flex;
  564. flex-direction: column;
  565. gap: 16px;
  566. }
  567. &__specific-card {
  568. background-color: #ffffff;
  569. border-radius: 12px;
  570. padding: 12px 16px;
  571. display: flex;
  572. flex-direction: column;
  573. gap: 8px;
  574. }
  575. &__specific-amount-row {
  576. display: flex;
  577. align-items: center;
  578. gap: 8px;
  579. }
  580. &__amount-input {
  581. flex: 1;
  582. border: none;
  583. outline: none;
  584. font-family: var(--font-title);
  585. font-size: 18px;
  586. font-weight: 600;
  587. color: #1d2129;
  588. &::placeholder {
  589. color: #c9cdd4;
  590. }
  591. }
  592. &__specific-divider {
  593. height: 1px;
  594. width: 100%;
  595. background-color: #f2f3f5;
  596. }
  597. &__specific-time-row {
  598. display: flex;
  599. align-items: center;
  600. justify-content: space-between;
  601. }
  602. &__time-label {
  603. font-size: 14px;
  604. color: #1d2129;
  605. }
  606. &__stepper {
  607. display: inline-flex;
  608. align-items: center;
  609. gap: 6px;
  610. padding: 4px 8px;
  611. border-radius: 32px;
  612. }
  613. &__stepper-btn {
  614. @include size(24px);
  615. border-radius: 26px;
  616. border: none;
  617. display: flex;
  618. align-items: center;
  619. justify-content: center;
  620. font-size: 16px;
  621. cursor: pointer;
  622. -webkit-tap-highlight-color: transparent;
  623. }
  624. &__stepper-btn--minus {
  625. background-color: #f2f3f5;
  626. color: #c9cdd4;
  627. }
  628. &__stepper-btn--plus {
  629. background-color: #f1f2f5;
  630. color: #4e5969;
  631. }
  632. &__stepper-value {
  633. min-width: 24px;
  634. text-align: center;
  635. font-size: 14px;
  636. color: #1f2024;
  637. }
  638. &__specific-price {
  639. display: flex;
  640. align-items: center;
  641. justify-content: flex-end;
  642. gap: 4px;
  643. }
  644. &__specific-price-value {
  645. font-family: var(--font-title);
  646. font-size: 18px;
  647. font-weight: 600;
  648. color: var(--color-text-primary);
  649. &--disabled {
  650. color: #86909C;
  651. }
  652. }
  653. &__generate-btn {
  654. margin-top: 4px;
  655. @include size(100%, 47px);
  656. border: none;
  657. border-radius: 999px;
  658. background-image: linear-gradient(90deg, #2f95ff 0%, #50ffd8 100%);
  659. color: #ffffff;
  660. font-family: var(--font-title);
  661. font-size: 16px;
  662. font-weight: 600;
  663. display: flex;
  664. align-items: center;
  665. justify-content: center;
  666. cursor: pointer;
  667. -webkit-tap-highlight-color: transparent;
  668. &:disabled {
  669. opacity: 0.4;
  670. cursor: default;
  671. }
  672. }
  673. }
  674. .save-image-popup {
  675. background: transparent;
  676. --van-padding-md: 0;
  677. &__container {
  678. @include size(375px, 500px);
  679. @include bg('~/assets/images/mine/bg-qrcode.png');
  680. display: flex;
  681. flex-direction: column;
  682. align-items: center;
  683. padding: 30px 56px 24px;
  684. box-sizing: border-box;
  685. }
  686. &__avatar-wrapper {
  687. @include size(75px);
  688. border-radius: 999px;
  689. border: 2px solid #ffffff;
  690. background-color: #f2f3f5;
  691. overflow: hidden;
  692. }
  693. &__avatar {
  694. width: 100%;
  695. height: 100%;
  696. img {
  697. width: 100%;
  698. height: 100%;
  699. display: block;
  700. object-fit: cover;
  701. }
  702. }
  703. &__name {
  704. margin-top: 2px;
  705. font-family: var(--font-title);
  706. font-size: 18px;
  707. font-weight: 600;
  708. color: #1d2129;
  709. line-height: 22px;
  710. }
  711. &__rating {
  712. margin-top: 2px;
  713. }
  714. &__qr-card {
  715. margin-top: 12px;
  716. display: flex;
  717. align-items: center;
  718. justify-content: center;
  719. }
  720. &__price-row {
  721. margin-top: 6px;
  722. height: 22px;
  723. display: flex;
  724. align-items: center;
  725. justify-content: center;
  726. gap: 4px;
  727. }
  728. &__coin {
  729. @include bg('~/assets/images/common/coin.png');
  730. @include size(20px);
  731. }
  732. &__price {
  733. font-family: var(--font-title);
  734. font-size: 18px;
  735. font-weight: 600;
  736. line-height: 22px;
  737. color: #1d2129;
  738. }
  739. &__symbol, &__currency {
  740. font-size: 14px;
  741. color: #17171A;
  742. }
  743. &__cta {
  744. margin-top: 8px;
  745. height: 30px;
  746. padding: 0 25px;
  747. border-radius: 200px;
  748. border: none;
  749. background-image: linear-gradient(90deg, #4ed2ff 0%, #b1ef5d 137.08%);
  750. color: #ffffff;
  751. font-size: 14px;
  752. font-weight: 600;
  753. display: flex;
  754. align-items: center;
  755. justify-content: center;
  756. cursor: default;
  757. -webkit-tap-highlight-color: transparent;
  758. }
  759. &__footer {
  760. margin-top: 16px;
  761. padding-top: 12px;
  762. width: 100%;
  763. display: flex;
  764. align-items: center;
  765. justify-content: space-between;
  766. }
  767. &__footer-left {
  768. display: flex;
  769. align-items: center;
  770. gap: 8px;
  771. }
  772. &__game-avatar {
  773. @include size(20px);
  774. border-radius: 999px;
  775. background-color: #e5e6eb;
  776. overflow: hidden;
  777. img {
  778. width: 100%;
  779. height: 100%;
  780. display: block;
  781. object-fit: cover;
  782. }
  783. }
  784. &__game-name {
  785. font-size: 12px;
  786. font-weight: 600;
  787. color: #1d2129;
  788. }
  789. &__duration {
  790. font-size: 14px;
  791. color: #1d2129;
  792. }
  793. }
  794. </style>
  795. <style lang="scss">
  796. .van-popup.qr-popup {
  797. overflow-y: unset;
  798. }
  799. </style>