Pārlūkot izejas kodu

二维码扫描

jean 1 mēnesi atpakaļ
vecāks
revīzija
2b048c0de5

+ 1 - 0
code/src/locales/en.ts

@@ -562,6 +562,7 @@ export default {
     iosNotReleased: 'iOS Not Available',
     android: 'Android Download',
     androidNotReleased: 'Android Not Available',
+    scanToDownload: 'Scan to download',
   },
   account: {
     title: 'Account Center',

+ 1 - 0
code/src/locales/hi.ts

@@ -566,6 +566,7 @@ export default {
     iosNotReleased: 'iOS अभी उपलब्ध नहीं',
     android: 'Android डाउनलोड',
     androidNotReleased: 'Android अभी उपलब्ध नहीं',
+    scanToDownload: 'स्कैन करके डाउनलोड करें',
   },
   account: {
     title: 'अकाउंट केंद्र',

+ 1 - 0
code/src/locales/id.ts

@@ -566,6 +566,7 @@ export default {
     iosNotReleased: 'iOS Belum Tersedia',
     android: 'Unduh Android',
     androidNotReleased: 'Android Belum Tersedia',
+    scanToDownload: 'Pindai untuk unduh',
   },
   account: {
     title: 'Pusat Akun',

+ 1 - 0
code/src/locales/ja.ts

@@ -568,6 +568,7 @@ export default {
     iosNotReleased: 'iOS 未公開',
     android: 'Androidダウンロード',
     androidNotReleased: 'Android 未公開',
+    scanToDownload: 'スキャンしてダウンロード',
   },
   account: {
     title: 'アカウントセンター',

+ 1 - 0
code/src/locales/ko.ts

@@ -565,6 +565,7 @@ export default {
     iosNotReleased: 'iOS 미출시',
     android: 'Android 다운로드',
     androidNotReleased: 'Android 미출시',
+    scanToDownload: 'QR 코드로 다운로드',
   },
   account: {
     title: '계정 센터',

+ 1 - 0
code/src/locales/zh-CN.ts

@@ -564,6 +564,7 @@ export default {
     iosNotReleased: 'iOS 暂未发布',
     android: 'Android 下载',
     androidNotReleased: '安卓暂未发布',
+    scanToDownload: '扫码下载',
   },
   account: {
     title: '个人中心',

+ 1 - 0
code/src/locales/zh-TW.ts

@@ -564,6 +564,7 @@ export default {
     iosNotReleased: 'iOS 暫未發布',
     android: 'Android 下載',
     androidNotReleased: '安卓暫未發布',
+    scanToDownload: '掃碼下載',
   },
   account: {
     title: '個人中心',

+ 112 - 21
code/src/views/DownloadView.vue

@@ -1,10 +1,11 @@
 <script setup lang="ts">
 // P16 下载页
-import { onMounted, computed, watch } from 'vue'
+import { onMounted, computed, watch, ref } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useAppVersionStore } from '@/stores/appVersion'
 import { useI18nStore } from '@/stores/i18n'
 import { useI18n } from 'vue-i18n'
+import { qrCodeImageUrl } from '@/utils/qr'
 import heroVisual  from '@/assets/download/hero-visual.png'
 import iconApple   from '@/assets/download/icon-apple.png'
 import iconAndroid from '@/assets/download/icon-android.png'
@@ -27,6 +28,17 @@ const iosUrl     = computed(() => ios.value?.downloadUrl || '')
 const androidUrl = computed(() => android.value?.downloadUrl || '')
 const latestInfo = computed(() => android.value ?? ios.value)
 
+const iosQr     = ref('')
+const androidQr = ref('')
+
+watch(iosUrl, async (url) => {
+  iosQr.value = url ? await qrCodeImageUrl(url, 160) : ''
+}, { immediate: true })
+
+watch(androidUrl, async (url) => {
+  androidQr.value = url ? await qrCodeImageUrl(url, 160) : ''
+}, { immediate: true })
+
 function handleDownload(e: MouseEvent, url: string) {
   if (!url) e.preventDefault()
 }
@@ -46,26 +58,42 @@ function handleDownload(e: MouseEvent, url: string) {
           </p>
 
           <div class="download-buttons">
-            <a
-              :href="iosUrl || '#'"
-              :class="['dl-btn', 'dark-card', { 'is-disabled': !iosUrl }]"
-              :target="iosUrl ? '_blank' : undefined"
-              rel="noopener"
-              @click="handleDownload($event, iosUrl)"
-            >
-              <img :src="iconApple" alt="iOS" class="dl-icon" />
-              <span class="dl-text">{{ loading ? t('common.loading') : (iosUrl ? t('download.ios') : t('download.iosNotReleased')) }}</span>
-            </a>
-            <a
-              :href="androidUrl || '#'"
-              :class="['dl-btn', 'dark-card', { 'is-disabled': !androidUrl }]"
-              :target="androidUrl ? '_blank' : undefined"
-              rel="noopener"
-              @click="handleDownload($event, androidUrl)"
-            >
-              <img :src="iconAndroid" alt="Android" class="dl-icon" />
-              <span class="dl-text">{{ loading ? t('common.loading') : (androidUrl ? t('download.android') : t('download.androidNotReleased')) }}</span>
-            </a>
+            <div class="dl-btn-wrap" :class="{ 'has-qr': !!iosUrl && !!iosQr }">
+              <a
+                :href="iosUrl || '#'"
+                :class="['dl-btn', 'dark-card', { 'is-disabled': !iosUrl }]"
+                :target="iosUrl ? '_blank' : undefined"
+                rel="noopener"
+                @click="handleDownload($event, iosUrl)"
+              >
+                <img :src="iconApple" alt="iOS" class="dl-icon" />
+                <span class="dl-text">{{ loading ? t('common.loading') : (iosUrl ? t('download.ios') : t('download.iosNotReleased')) }}</span>
+              </a>
+              <div v-if="iosUrl && iosQr" class="dl-qrcode-popover">
+                <div class="dl-qrcode-frame">
+                  <img :src="iosQr" :alt="t('download.scanToDownload')" class="dl-qrcode-img" />
+                </div>
+                <p class="dl-qrcode-hint">{{ t('download.scanToDownload') }}</p>
+              </div>
+            </div>
+            <div class="dl-btn-wrap" :class="{ 'has-qr': !!androidUrl && !!androidQr }">
+              <a
+                :href="androidUrl || '#'"
+                :class="['dl-btn', 'dark-card', { 'is-disabled': !androidUrl }]"
+                :target="androidUrl ? '_blank' : undefined"
+                rel="noopener"
+                @click="handleDownload($event, androidUrl)"
+              >
+                <img :src="iconAndroid" alt="Android" class="dl-icon" />
+                <span class="dl-text">{{ loading ? t('common.loading') : (androidUrl ? t('download.android') : t('download.androidNotReleased')) }}</span>
+              </a>
+              <div v-if="androidUrl && androidQr" class="dl-qrcode-popover">
+                <div class="dl-qrcode-frame">
+                  <img :src="androidQr" :alt="t('download.scanToDownload')" class="dl-qrcode-img" />
+                </div>
+                <p class="dl-qrcode-hint">{{ t('download.scanToDownload') }}</p>
+              </div>
+            </div>
           </div>
 
           <div v-if="latestInfo?.updateDescription" class="update-notes">
@@ -132,6 +160,69 @@ function handleDownload(e: MouseEvent, url: string) {
   display: flex;
   gap: var(--space-4);
   flex-wrap: wrap;
+  overflow: visible;
+}
+.dl-btn-wrap {
+  position: relative;
+}
+.dl-btn-wrap.has-qr:hover {
+  z-index: 2;
+}
+.dl-qrcode-popover {
+  position: absolute;
+  top: calc(100% + 12px);
+  left: 50%;
+  transform: translateX(-50%) translateY(-6px);
+  opacity: 0;
+  visibility: hidden;
+  pointer-events: none;
+  width: max-content;
+  padding: var(--space-4);
+  border-radius: var(--radius-md);
+  background: var(--color-bg-secondary);
+  border: 1px solid var(--color-border);
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
+  transition: opacity var(--transition), transform var(--transition), visibility var(--transition);
+  z-index: 10;
+  text-align: center;
+}
+.dl-qrcode-popover::before {
+  content: '';
+  position: absolute;
+  top: -6px;
+  left: 50%;
+  transform: translateX(-50%) rotate(45deg);
+  width: 12px;
+  height: 12px;
+  background: var(--color-bg-secondary);
+  border-top: 1px solid var(--color-border);
+  border-left: 1px solid var(--color-border);
+}
+.dl-btn-wrap.has-qr:hover .dl-qrcode-popover {
+  opacity: 1;
+  visibility: visible;
+  transform: translateX(-50%) translateY(0);
+}
+.dl-qrcode-frame {
+  width: 160px;
+  height: 160px;
+  padding: 8px;
+  border-radius: var(--radius-sm);
+  background: #fff;
+  box-sizing: border-box;
+}
+.dl-qrcode-img {
+  width: 100%;
+  height: 100%;
+  max-width: none;
+  object-fit: contain;
+  aspect-ratio: 1 / 1;
+}
+.dl-qrcode-hint {
+  margin-top: var(--space-3);
+  font-size: var(--text-caption);
+  color: var(--color-text-secondary);
+  white-space: nowrap;
 }
 .dl-btn {
   display: inline-flex;