|
|
@@ -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;
|