232 lines
8.6 KiB
Vue
232 lines
8.6 KiB
Vue
<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.2");
|
||
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>
|