处理下载链接
All checks were successful
site-dist-deploy / build-and-deploy (push) Successful in 1m17s

This commit is contained in:
speakeloudest 2026-04-17 14:35:48 +03:00
parent 3313b95ab1
commit 6c296dbaaf
3 changed files with 165 additions and 38 deletions

View File

@ -73,9 +73,18 @@
<div class="mx-auto h-[101px] w-[247px] md:h-[202px] md:w-[494px]"> <div class="mx-auto h-[101px] w-[247px] md:h-[202px] md:w-[494px]">
<div <div
@click="handleDownload('ios')" @click="handleDownload('ios')"
class="block h-full w-full cursor-pointer transition-transform active:scale-95" class="relative block h-full w-full cursor-pointer transition-transform active:scale-95"
> >
<img src="./Group%20100.png" class="h-full w-full" alt="下载" /> <img src="./Group%20100.png" class="h-full w-full" alt="下载" />
<!-- Loading Overlay -->
<div
v-if="isLoadingIos"
class="absolute inset-0 flex items-center justify-center rounded-[20px] bg-black/40"
>
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-[#ADFF5B] border-t-transparent md:h-12 md:w-12"
></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -109,11 +118,21 @@
<template v-for="client in otherClients" :key="client.type"> <template v-for="client in otherClients" :key="client.type">
<div <div
@click="handleDownload(client.type)" @click="handleDownload(client.type)"
class="cursor-pointer transition-transform hover:brightness-110 active:scale-95" class="relative cursor-pointer transition-transform hover:brightness-110 active:scale-95"
> >
<MacosIcon v-if="client.type === 'mac'" /> <MacosIcon v-if="client.type === 'mac'" />
<WindowsIcon v-if="client.type === 'window'" /> <WindowsIcon v-if="client.type === 'window'" />
<AndroidIcon v-if="client.type === 'android'" /> <AndroidIcon v-if="client.type === 'android'" />
<!-- Loading Overlay for Android -->
<div
v-if="client.type === 'android' && isLoadingAndroid"
class="absolute inset-0 flex items-center justify-center rounded-lg bg-black/40"
>
<div
class="h-6 w-6 animate-spin rounded-full border-2 border-[#ADFF5B] border-t-transparent"
></div>
</div>
</div> </div>
</template> </template>
</div> </div>
@ -132,6 +151,7 @@ import WindowsIcon from './windows.svg?component'
import AndroidIcon from './android.svg?component' import AndroidIcon from './android.svg?component'
import request from '@/utils/request' import request from '@/utils/request'
import { getAllQueryString } from '@/utils/url-utils.ts' import { getAllQueryString } from '@/utils/url-utils.ts'
import { toast } from 'vue-sonner'
import DownloadMethodList from './DownloadMethodList/DownloadMethodList.vue' import DownloadMethodList from './DownloadMethodList/DownloadMethodList.vue'
import FAQAccordion from './FAQAccordion/index.vue' import FAQAccordion from './FAQAccordion/index.vue'
@ -147,13 +167,46 @@ const handleDownload = async (key: string) => {
const platform = platformMap[key] || 'windows' const platform = platformMap[key] || 'windows'
const ic = getAllQueryString('ic') || sessionStorage.getItem('ic') || 'uSSfg1Y1vt' const ic = getAllQueryString('ic') || sessionStorage.getItem('ic') || 'uSSfg1Y1vt'
if (platform === 'ios') { //
if (window.OI_SDK && window.OI_SDK.OI) { const ua = navigator.userAgent
window.OI_SDK.OI.wakeupOrInstall({ data: { platform: 'download', inviteCode: ic } }) const isAndroidDevice = /Android/i.test(ua)
const isIosDevice =
/iPhone|iPod|iPad/i.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
const isMobileDevice = isAndroidDevice || isIosDevice
// 1.
if (key === 'android') {
if (isAndroidDevice) {
isLoadingAndroid.value = true
await triggerOpenInstall(ic)
isLoadingAndroid.value = false
return
} else {
window.open(
'https://api.hifast.biz/v1/common/client/download/file/Hi%E5%BF%ABVPN-android-1.0.0.apk',
'_blank',
)
return return
} }
} }
// 2. iOS/Mac
if (key === 'ios' || key === 'mac') {
if (isMobileDevice) {
if (key === 'ios') isLoadingIos.value = true
await triggerOpenInstall(ic)
isLoadingIos.value = false
return
} else {
window.open(
'https://apps.apple.com/us/app/hi%E5%BF%ABvpn/id6755683167?l=zh-Hans-CN',
'_blank',
)
return
}
}
// 3. ( Windows)
try { try {
const res: any = await request.get('/api/v1/common/client/download', { const res: any = await request.get('/api/v1/common/client/download', {
invite_code: ic, invite_code: ic,
@ -167,7 +220,27 @@ const handleDownload = async (key: string) => {
} }
} }
// OpenInstall
const triggerOpenInstall = async (ic: string) => {
if (!(window as any).OI_SDK?.isReady && (window as any).OI_SDK_PROMISE) {
try {
await Promise.race([
(window as any).OI_SDK_PROMISE,
new Promise((_, reject) => setTimeout(() => reject(new Error('OpenInstall timeout')), 3000)),
])
} catch (e) {
console.warn('OpenInstall readiness wait failed or timeout:', e)
}
}
if ((window as any).OI_SDK && (window as any).OI_SDK.OI) {
;(window as any).OI_SDK.OI.wakeupOrInstall({ data: { platform: 'download', inviteCode: ic } })
}
}
const activeIndex = ref(0) const activeIndex = ref(0)
const isLoadingIos = ref(false)
const isLoadingAndroid = ref(false)
const otherClients = computed(() => { const otherClients = computed(() => {
return [ return [

View File

@ -30,7 +30,7 @@
v-else v-else
:id="mainButton?.id" :id="mainButton?.id"
:aria-label="mainButton?.label" :aria-label="mainButton?.label"
@click="mainButton.key === 'win' ? handleDownload(mainButton.key) : null" @click="handleDownload(mainButton.key)"
class="flex h-full w-full cursor-pointer items-center justify-center transition-transform hover:brightness-110 active:scale-95" class="flex h-full w-full cursor-pointer items-center justify-center transition-transform hover:brightness-110 active:scale-95"
> >
<component <component
@ -68,7 +68,7 @@
v-else v-else
:id="item.id" :id="item.id"
:aria-label="item.label" :aria-label="item.label"
@click="item.key === 'win' ? handleDownload(item.key) : null" @click="handleDownload(item.key)"
class="cursor-pointer transition-transform hover:brightness-110 active:scale-95" class="cursor-pointer transition-transform hover:brightness-110 active:scale-95"
> >
<component :is="item.secondaryIcon" class="h-[24px] text-white md:h-[34px]" /> <component :is="item.secondaryIcon" class="h-[24px] text-white md:h-[34px]" />
@ -142,10 +142,47 @@ const handleDownload = async (key: string) => {
ios: 'ios', ios: 'ios',
} }
const platform = platformMap[key] || 'windows' const platform = platformMap[key] || 'windows'
const ic = getAllQueryString('ic') || sessionStorage.getItem('ic') || 'uSSfg1Y1vt'
//
const ua = navigator.userAgent
const isAndroidDevice = /Android/i.test(ua)
const isIosDevice =
/iPhone|iPod|iPad/i.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
const isMobileDevice = isAndroidDevice || isIosDevice
// 1.
if (key === 'android') {
if (isAndroidDevice) {
await triggerOpenInstall(ic)
return
} else {
window.open(
'https://api.hifast.biz/v1/common/client/download/file/Hi%E5%BF%ABVPN-android-1.0.0.apk',
'_blank',
)
return
}
}
// 2. iOS/Mac
if (key === 'ios' || key === 'mac') {
if (isMobileDevice) {
await triggerOpenInstall(ic)
return
} else {
window.open(
'https://apps.apple.com/us/app/hi%E5%BF%ABvpn/id6755683167?l=zh-Hans-CN',
'_blank',
)
return
}
}
// 3. ( Windows)
try { try {
const res: any = await request.get('/api/v1/common/client/download', { const res: any = await request.get('/api/v1/common/client/download', {
invite_code: getAllQueryString('ic') || sessionStorage.getItem('ic') || 'uSSfg1Y1vt', invite_code: ic,
platform: platform, platform: platform,
}) })
if (res.url) { if (res.url) {
@ -159,6 +196,24 @@ const handleDownload = async (key: string) => {
} }
} }
// OpenInstall
const triggerOpenInstall = async (ic: string) => {
if (!(window as any).OI_SDK?.isReady && (window as any).OI_SDK_PROMISE) {
try {
await Promise.race([
(window as any).OI_SDK_PROMISE,
new Promise((_, reject) => setTimeout(() => reject(new Error('OpenInstall timeout')), 3000)),
])
} catch (e) {
console.warn('OpenInstall readiness wait failed or timeout:', e)
}
}
if ((window as any).OI_SDK && (window as any).OI_SDK.OI) {
;(window as any).OI_SDK.OI.wakeupOrInstall({ data: { platform: 'download', inviteCode: ic } })
}
}
const allDownloadOptions = computed(() => [ const allDownloadOptions = computed(() => [
{ {
key: 'win', key: 'win',

View File

@ -11,8 +11,13 @@ script.charset = 'UTF-8'
script.src = 'https://web.cdn.openinstall.io/openinstall.js' script.src = 'https://web.cdn.openinstall.io/openinstall.js'
document.head.appendChild(script) document.head.appendChild(script)
let resolveReady: (sdk: OpenInstallSdk) => void
window.OI_SDK_PROMISE = new Promise((resolve) => {
resolveReady = resolve
})
script.addEventListener('load', () => { script.addEventListener('load', () => {
window.OI_SDK = new OpenInstallSdk() window.OI_SDK = new OpenInstallSdk(resolveReady)
}) })
class OpenInstallSdk { class OpenInstallSdk {
@ -20,7 +25,9 @@ class OpenInstallSdk {
public OI: Record<string, any> // openinstall 实例 public OI: Record<string, any> // openinstall 实例
constructor() { public isReady = false
constructor(private onReadyCallback?: (sdk: OpenInstallSdk) => void) {
this.OI = {} this.OI = {}
this.urlQuery = window.OpenInstall.parseUrlParams() this.urlQuery = window.OpenInstall.parseUrlParams()
const id = getAllQueryString('id') const id = getAllQueryString('id')
@ -34,47 +41,39 @@ class OpenInstallSdk {
} }
async init() { async init() {
const self = this
try { try {
this.OI = new window.OpenInstall( this.OI = new window.OpenInstall(
{ {
appKey: 'alf57p', appKey: 'alf57p',
onready: function () { onready: function () {
// 初始化成功回调方法。当初始化完成后,会自动进入 // 初始化成功回调方法。当初始化完成后,会自动进入
this.schemeWakeup() // 尝试使用scheme打开App主要用于Android以及iOS的QQ环境中 // 注意:此时的 this 是 OpenInstall 的原始实例对象 (m)
const m = this const m = this
m.schemeWakeup() // 尝试使用scheme打开App主要用于Android以及iOS的QQ环境中
const button = document.getElementById('downloadButton_apple') const button = document.getElementById('downloadButton_apple')
const button_mac = document.getElementById('downloadButton_mac') const button_mac = document.getElementById('downloadButton_mac')
const button1 = document.getElementById('downloadButton_android') const button1 = document.getElementById('downloadButton_android')
const ic = getAllQueryString('ic') || 'uSSfg1Y1vt' const ic = getAllQueryString('ic') || 'uSSfg1Y1vt'
if (button) {
button.onclick = function () { const clickHandler = function () {
if (ic) { if (ic) {
m.wakeupOrInstall({ data: { platform: 'download', inviteCode: ic } }) m.wakeupOrInstall({ data: { platform: 'download', inviteCode: ic } })
} else { } else {
m.wakeupOrInstall() // 此方法为scheme、Universal Link唤醒以及引导下载的作用必须调用且不可额外自行跳转下载 m.wakeupOrInstall()
}
return false
} }
return false
} }
if (button_mac) {
button_mac.onclick = function () { if (button) button.onclick = clickHandler
if (ic) { if (button_mac) button_mac.onclick = clickHandler
m.wakeupOrInstall({ data: { platform: 'download', inviteCode: ic } }) if (button1) button1.onclick = clickHandler
} else {
m.wakeupOrInstall() // 此方法为scheme、Universal Link唤醒以及引导下载的作用必须调用且不可额外自行跳转下载 // 标记就绪并触发 Promise
} self.isReady = true
return false if (self.onReadyCallback) {
} self.onReadyCallback(self)
}
if (button1) {
button1.onclick = function () {
if (ic) {
m.wakeupOrInstall({ data: { platform: 'download', inviteCode: ic } })
} else {
m.wakeupOrInstall() // 此方法为scheme、Universal Link唤醒以及引导下载的作用必须调用且不可额外自行跳转下载
}
return false
}
} }
}, },
}, },