@@ -0,0 +1,231 @@
< script setup >
import { computed , onMounted , ref } from "vue" ;
import { useAppStore } from "../stores/app" ;
import { getRuntime , getVersion } from "../api" ;
import { isDesktop } from "../auth" ;
import vntIcon from "../assets/vnt-icon.png" ;
const PROJECT _URL = "https://github.com/vnt-dev/vnt" ;
const RELEASES _URL = ` ${ PROJECT _URL } /releases ` ;
const RELEASES _API = "https://api.github.com/repos/vnt-dev/vnt/releases?per_page=20" ;
const app = useAppStore ( ) ;
const runtime = ref ( isDesktop ? "desktop" : "" ) ;
const checking = ref ( false ) ;
const installing = ref ( false ) ;
const updateInfo = ref ( null ) ;
const resultKind = ref ( "" ) ;
const message = ref ( "" ) ;
const downloaded = ref ( 0 ) ;
const contentLength = ref ( 0 ) ;
const currentVersion = computed ( ( ) => app . version || "2.0.0" ) ;
const progress = computed ( ( ) => {
if ( ! contentLength . value ) return 0 ;
return Math . min ( 100 , Math . round ( ( downloaded . value / contentLength . value ) * 100 ) ) ;
} ) ;
const versionParts = ( value ) => {
const match = String ( value || "" ) . trim ( ) . match ( /^v?(\d+)\.(\d+)\.(\d+)(?:[+-].*)?$/ ) ;
return match ? match . slice ( 1 ) . map ( Number ) : null ;
} ;
const compareVersions = ( left , right ) => {
const a = versionParts ( left ) ;
const b = versionParts ( right ) ;
if ( ! a || ! b ) return 0 ;
for ( let index = 0 ; index < 3 ; index += 1 ) {
if ( a [ index ] !== b [ index ] ) return a [ index ] > b [ index ] ? 1 : - 1 ;
}
return 0 ;
} ;
const openUrl = async ( url ) => {
if ( globalThis . _ _VNT _WEB _ACCESS _ _ ? . openUrl ) {
await globalThis . _ _VNT _WEB _ACCESS _ _ . openUrl ( url ) ;
} else {
window . open ( url , "_blank" , "noopener,noreferrer" ) ;
}
} ;
const checkGithubRelease = async ( ) => {
const response = await fetch ( RELEASES _API , {
headers : { Accept : "application/vnd.github+json" } ,
cache : "no-store" ,
} ) ;
if ( ! response . ok ) throw new Error ( ` GitHub 返回 ${ response . status } ` ) ;
const releases = ( await response . json ( ) ) . filter (
( release ) => ! release . draft && ! release . prerelease && versionParts ( release . tag _name ) ,
) ;
releases . sort ( ( a , b ) => compareVersions ( b . tag _name , a . tag _name ) ) ;
const latest = releases [ 0 ] ;
if ( ! latest ) throw new Error ( "没有找到可用的发布版本" ) ;
return {
version : latest . tag _name . replace ( /^v/ , "" ) ,
body : latest . body || "" ,
url : latest . html _url || RELEASES _URL ,
} ;
} ;
const checkUpdate = async ( ) => {
checking . value = true ;
resultKind . value = "" ;
message . value = "" ;
updateInfo . value = null ;
try {
if ( isDesktop ) {
let update ;
try {
update = await globalThis . _ _VNT _UPDATER _ _ ? . check ( ) ;
} catch {
const latest = await checkGithubRelease ( ) ;
if ( compareVersions ( latest . version , currentVersion . value ) <= 0 ) {
resultKind . value = "latest" ;
message . value = "当前已是最新版本" ;
return ;
}
updateInfo . value = { ... latest , manualOnly : true } ;
resultKind . value = "update" ;
message . value = ` 发现新版本 v ${ latest . version } ,该版本暂未提供自动更新包。 ` ;
return ;
}
if ( ! update ) {
resultKind . value = "latest" ;
message . value = "当前已是最新版本" ;
return ;
}
updateInfo . value = { ... update , url : RELEASES _URL } ;
resultKind . value = "update" ;
message . value = ` 发现新版本 v ${ update . version } ,可以直接下载并更新。 ` ;
return ;
}
runtime . value || = await getRuntime ( ) ;
const latest = await checkGithubRelease ( ) ;
if ( compareVersions ( latest . version , currentVersion . value ) <= 0 ) {
resultKind . value = "latest" ;
message . value = "当前已是最新版本" ;
return ;
}
updateInfo . value = latest ;
resultKind . value = "update" ;
message . value = runtime . value === "desktop_web"
? ` 发现新版本 v ${ latest . version } ,请回到 VNT Desktop 的“关于”页面完成更新。 `
: ` 发现新版本 v ${ latest . version } ,请下载新版本并替换当前 vnt2_web 程序。 ` ;
} catch ( error ) {
resultKind . value = "error" ;
message . value = ` 检查更新失败: ${ error ? . message || error } ` ;
} finally {
checking . value = false ;
}
} ;
const downloadAndInstall = async ( ) => {
installing . value = true ;
downloaded . value = 0 ;
contentLength . value = 0 ;
message . value = "正在准备下载更新…" ;
try {
await globalThis . _ _VNT _UPDATER _ _ . downloadAndInstall ( ( event ) => {
downloaded . value = event . downloaded ;
contentLength . value = event . contentLength ;
message . value = event . event === "Finished" ? "下载完成,正在安装…" : "正在下载更新…" ;
} ) ;
} catch ( error ) {
resultKind . value = "error" ;
message . value = ` 更新失败: ${ error ? . message || error } ` ;
installing . value = false ;
}
} ;
onMounted ( async ( ) => {
if ( ! app . version ) {
try {
app . version = await getVersion ( ) ;
} catch {
// 顶栏的版本加载逻辑仍会继续重试。
}
}
if ( ! isDesktop ) {
try {
runtime . value = await getRuntime ( ) ;
} catch {
runtime . value = "standalone_web" ;
}
}
} ) ;
< / script >
< template >
< div class = "mx-auto max-w-3xl space-y-5" >
< div >
< h1 class = "page-title" > 关于 < / h1 >
< p class = "page-subtitle" > VNT 客户端信息与软件更新 < / p >
< / div >
< section class = "card flex items-center gap-4" >
< img :src = "vntIcon" alt = "VNT" class = "h-16 w-16 shrink-0 rounded-2xl" / >
< div class = "min-w-0" >
< h2 class = "text-lg font-bold text-slate-900 dark:text-white" > VNT < / h2 >
< p class = "mt-1 text-sm text-slate-500 dark:text-slate-400" > 简单 、 高效的异地组网与内网穿透工具 < / p >
< p class = "mt-2 font-mono text-xs text-slate-400" > 当前版本 v { { currentVersion } } < / p >
< / div >
< / section >
< section class = "card" >
< h2 class = "text-sm font-semibold text-slate-900 dark:text-white" > 开源项目 < / h2 >
< p class = "mt-2 text-sm leading-6 text-slate-500 dark:text-slate-400" > 项目代码 、 使用说明和问题反馈均托管在 GitHub 。 < / p >
< button class = "btn-ghost mt-4" type = "button" @click ="openUrl(PROJECT_URL)" >
< svg class = "h-4 w-4" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" >
< path d = "M14 5h5v5m0-5-9 9M19 13v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5" stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 1.8 " / >
< / svg >
github . com / vnt - dev / vnt
< / button >
< / section >
< section class = "card" >
< div class = "flex flex-wrap items-start justify-between gap-4" >
< div >
< h2 class = "text-sm font-semibold text-slate-900 dark:text-white" > 软件更新 < / h2 >
< p class = "mt-2 text-sm text-slate-500 dark:text-slate-400" >
{ { isDesktop ? "检查并安装 VNT Desktop 的最新版本。" : "检查 GitHub 上发布的最新版本。" } }
< / p >
< / div >
< button class = "btn-primary" type = "button" : disabled = "checking || installing" @click ="checkUpdate" >
{{ checking ? " 正在检查 … " : " 检查更新 " }}
< / button >
< / div >
< div
v-if = "message"
class = "mt-5 rounded-lg border px-4 py-3 text-sm"
: class = "resultKind === 'error'
? 'border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300'
: resultKind === 'update'
? 'border-indigo-200 bg-indigo-50 text-indigo-700 dark:border-indigo-900 dark:bg-indigo-950/40 dark:text-indigo-300'
: 'border-slate-200 bg-slate-50 text-slate-600 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-300'"
>
{ { message } }
< / div >
< div v-if = "installing && contentLength" class="mt-4" >
< div class = "mb-1.5 flex justify-between text-xs text-slate-400" >
< span > 下载进度 < / span >
< span > { { progress } } % < / span >
< / div >
< div class = "h-1.5 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700" >
< div class = "h-full rounded-full bg-indigo-600 transition-[width] dark:bg-indigo-500" : style = "{ width: `${progress}%` }" > < / div >
< / div >
< / div >
< div v-if = "resultKind === 'update'" class="mt-4 flex flex-wrap gap-2" >
< button v-if = "isDesktop && !updateInfo?.manualOnly" class="btn-primary" type="button" :disabled="installing" @click="downloadAndInstall" >
{{ installing ? " 正在更新 … " : " 下载并更新 " }}
< / button >
< button v-else-if = "updateInfo?.manualOnly || runtime === 'standalone_web'" class="btn-ghost" type="button" @click="openUrl(updateInfo?.url || RELEASES_URL)" > 查看发布版本 < / button >
< / div >
< p v-if = "isDesktop" class="mt-4 text-xs leading-5 text-slate-400" > 安装更新时桌面客户端可能自动退出 , 完成后将重新启动 。 < / p >
< / section >
< / div >
< / template >