feat: 新增应用更新功能和关于页面

- 集成 Tauri updater 实现应用版本检查与更新
- 添加关于页面,展示项目介绍、使用流程和版本信息
- 新增顶部栏检查更新和 GitHub 入口
- 完善国际化文案和更新弹窗组件
This commit is contained in:
Yuhang Wu
2026-10-14 10:13:31 +08:00
parent f6d8d783b4
commit 746a4be2bc
15 changed files with 663 additions and 7 deletions
+2
View File
@@ -6,6 +6,8 @@
"permissions": [
"core:default",
"opener:default",
"updater:default",
"process:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-unminimize",
+2 -1
View File
@@ -28,6 +28,7 @@
"bundle": {
"active": true,
"targets": "all",
"createUpdaterArtifacts": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -39,7 +40,7 @@
"plugins": {
"updater": {
"active": true,
"dialog": true,
"dialog": false,
"endpoints": ["https://ghproxy.net/https://github.com/SparksFly91/Frpilot/releases/latest/download/latest.json"],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU0REVGMTREODFFOTI2MUEKUldRYUp1bUJUZkhlNU1qRmlPQm5xSmdQd24waFltVmNtTjlUallYVmdva1VGNkp5bTVGcVlsZUgK"
}
+46
View File
@@ -0,0 +1,46 @@
import type { Update } from "@tauri-apps/plugin-updater"
import { isTauri } from "@/utils/window"
/** 项目 GitHub 仓库地址(与 tauri.conf.json 中 updater 端点保持一致) */
export const PROJECT_GITHUB_URL = "https://github.com/SparksFly91/Frpilot"
/**
* 检查是否有可用更新。
*
* 非 Tauri 环境(纯浏览器调试)下无法调用插件,直接返回 null。
*/
export async function checkForAppUpdate(): Promise<Update | null> {
if (!isTauri()) return null
const { check } = await import("@tauri-apps/plugin-updater")
return check()
}
/** 重启应用(macOS / Linux 安装更新后需要重启才能运行新版本) */
export async function relaunchApp() {
if (!isTauri()) return
const { relaunch } = await import("@tauri-apps/plugin-process")
await relaunch()
}
/** 用系统默认浏览器打开外部链接 */
export async function openExternal(url: string) {
if (!isTauri()) {
// 浏览器调试环境退化为新标签页打开
window.open(url, "_blank", "noopener,noreferrer")
return
}
const { openUrl } = await import("@tauri-apps/plugin-opener")
await openUrl(url)
}
/** 打开项目 GitHub 仓库首页 */
export function openProjectRepo() {
return openExternal(PROJECT_GITHUB_URL)
}
/** 读取当前应用版本(来自 tauri.conf.json / package.json) */
export async function getAppVersion(): Promise<string> {
if (!isTauri()) return "dev"
const { getVersion } = await import("@tauri-apps/api/app")
return getVersion()
}
+45
View File
@@ -163,6 +163,51 @@ body {
overflow-y: auto;
}
/*
* 更新弹窗:高度不超过窗口的 60%,内容过多时仅主体(body)滚动,
* 标题与底部按钮固定可见;沿用磨砂玻璃质感。
*/
.update-modal-wrap .ant-modal {
padding-bottom: 0;
}
.update-modal-wrap .ant-modal-content {
display: flex;
flex-direction: column;
max-height: 60vh;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.55);
background: rgba(255, 255, 255, 0.72) !important;
backdrop-filter: blur(24px) saturate(170%);
-webkit-backdrop-filter: blur(24px) saturate(170%);
box-shadow: 0 16px 48px rgba(15, 23, 42, 0.18) !important;
}
.update-modal-wrap .ant-modal-header,
.update-modal-wrap .ant-modal-footer {
flex: 0 0 auto;
border-color: rgba(15, 23, 42, 0.06);
background: transparent !important;
}
.dark .update-modal-wrap .ant-modal-content {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(30, 41, 59, 0.72) !important;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5) !important;
}
.dark .update-modal-wrap .ant-modal-header,
.dark .update-modal-wrap .ant-modal-footer {
border-color: rgba(255, 255, 255, 0.08);
}
.update-modal-wrap .ant-modal-body {
flex: 1 1 auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
}
/* 细滚动条 */
::-webkit-scrollbar {
width: 6px;
+36
View File
@@ -37,6 +37,27 @@
</template>
</a-button>
<!-- 检查更新 -->
<a-button
type="text"
class="tb-btn"
:title="t('titlebar.checkUpdate')"
:loading="updateStore.checking"
@click="onCheckUpdate"
>
<template #icon><CloudSyncOutlined /></template>
</a-button>
<!-- GitHub -->
<a-button
type="text"
class="tb-btn"
:title="t('titlebar.github')"
@click="onOpenGithub"
>
<template #icon><GithubOutlined /></template>
</a-button>
<!-- 设置 -->
<a-button
type="text"
@@ -74,6 +95,8 @@ import {
BulbFilled,
BulbOutlined,
CloseOutlined,
CloudSyncOutlined,
GithubOutlined,
MinusOutlined,
SettingOutlined,
TranslationOutlined,
@@ -81,17 +104,30 @@ import {
import { useI18n } from "vue-i18n"
import type { MenuProps } from "ant-design-vue"
import { useAppStore } from "@/stores/app"
import { useUpdateStore } from "@/stores/update"
import { languageOptions, type Language } from "@/i18n"
import { closeWindow, minimizeWindow } from "@/utils/window"
import { openProjectRepo } from "@/api/updater"
const emit = defineEmits<{ settings: [] }>()
const { t } = useI18n()
const appStore = useAppStore()
const updateStore = useUpdateStore()
const onLanguageClick: MenuProps["onClick"] = ({ key }) => {
appStore.setLanguage(key as Language)
}
/** 顶部栏手动检查更新(非静默,无更新 / 失败都会给出提示) */
function onCheckUpdate() {
void updateStore.checkForUpdate(false)
}
/** 打开项目 GitHub 仓库 */
function onOpenGithub() {
void openProjectRepo()
}
</script>
<style scoped lang="scss">
+153
View File
@@ -0,0 +1,153 @@
<template>
<a-modal
:open="store.modalOpen"
:title="modalTitle"
:closable="!store.installing"
:mask-closable="false"
:keyboard="!store.installing"
:width="520"
centered
wrap-class-name="update-modal-wrap"
@cancel="onCancel"
>
<!-- 俏皮的问候 + 版本变化 -->
<div class="update-hero">
<span class="update-hero-emoji">🎉</span>
<div class="min-w-0 flex-1">
<p class="m-0 text-sm font-medium">{{ t("update.hello") }}</p>
<div class="mt-2 flex flex-wrap items-center gap-1.5 text-xs">
<a-tag class="!me-0">v{{ info?.currentVersion }}</a-tag>
<ArrowRightOutlined class="text-[10px] opacity-50" />
<a-tag color="green" class="!me-0">v{{ info?.version }}</a-tag>
<span v-if="releaseDate" class="ml-1 opacity-50">{{ releaseDate }}</span>
</div>
</div>
</div>
<p class="update-tip">{{ t("update.tip") }}</p>
<!-- 更新日志 -->
<div class="update-log-header">
<BulbOutlined />
<span>{{ t("update.log") }}</span>
</div>
<div class="update-log">{{ logText }}</div>
<!-- 下载 / 安装进度 -->
<div v-if="store.installing" class="update-progress">
<a-progress :percent="store.progress" :stroke-color="'#6366f1'" />
<p class="m-0 text-xs opacity-60">{{ t("update.downloading") }}</p>
</div>
<!-- 底部操作:忽略该版本 / 取消 / 确认更新 -->
<template #footer>
<div class="update-footer">
<a-button v-if="!store.installing" type="text" size="small" @click="onIgnore">
{{ t("update.ignore") }}
</a-button>
<div class="ml-auto flex items-center gap-2">
<a-button :disabled="store.installing" @click="onCancel">
{{ t("common.cancel") }}
</a-button>
<a-button type="primary" :loading="store.installing" @click="onConfirm">
{{ t("update.confirm") }}
</a-button>
</div>
</div>
</template>
</a-modal>
</template>
<script setup lang="ts">
import dayjs from "dayjs"
import { ArrowRightOutlined, BulbOutlined } from "@ant-design/icons-vue"
import { useI18n } from "vue-i18n"
import { useUpdateStore } from "@/stores/update"
const { t } = useI18n()
const store = useUpdateStore()
const info = computed(() => store.info)
const modalTitle = computed(() => t("update.title", { version: info.value?.version ?? "" }))
const releaseDate = computed(() =>
info.value?.date ? dayjs(info.value.date).format("YYYY-MM-DD") : "",
)
const logText = computed(() => info.value?.body?.trim() || t("update.emptyLog"))
function onCancel() {
store.cancel()
}
function onIgnore() {
store.ignoreVersion()
}
function onConfirm() {
store.install()
}
</script>
<style scoped lang="scss">
.update-hero {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 12px;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.14), rgba(34, 211, 238, 0.14));
}
.dark .update-hero {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.22), rgba(34, 211, 238, 0.18));
}
.update-hero-emoji {
font-size: 26px;
line-height: 1;
}
.update-tip {
margin: 14px 2px 10px;
font-size: 12px;
opacity: 0.65;
}
.update-log-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
}
.update-log {
white-space: pre-wrap;
word-break: break-word;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.08);
background: rgba(148, 163, 184, 0.12);
font-size: 13px;
line-height: 1.7;
opacity: 0.9;
}
.dark .update-log {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(148, 163, 184, 0.14);
}
.update-progress {
margin-top: 14px;
}
.update-footer {
display: flex;
align-items: center;
gap: 8px;
}
</style>
+3 -3
View File
@@ -32,9 +32,9 @@ export function setI18nLanguage(language: Language) {
i18n.global.locale.value = language
}
/** 在非组件环境(如路由守卫)中获取翻译函数 */
export function translate(key: string) {
return i18n.global.t(key)
/** 在非组件环境(如路由守卫、store)中获取翻译函数 */
export function translate(key: string, params?: Record<string, unknown>) {
return i18n.global.t(key, params ?? {})
}
export default i18n
+42 -1
View File
@@ -8,6 +8,8 @@
"theme": "Theme",
"lightMode": "Light mode",
"darkMode": "Dark mode",
"checkUpdate": "Check for updates",
"github": "GitHub",
"settings": "Settings",
"minimize": "Minimize",
"maximize": "Maximize",
@@ -57,7 +59,8 @@
"title": "Settings",
"placeholder": "The settings page is under construction.",
"menu": {
"appearance": "Appearance"
"appearance": "Appearance",
"about": "About"
},
"appearance": {
"title": "Appearance",
@@ -210,6 +213,44 @@
"error": "Download failed"
}
},
"about": {
"title": "About Frpilot",
"subtitle": "A lightweight, friendly visual client for frp",
"introTitle": "Introduction",
"intro": "Frpilot is a visual client for frp built with Tauri 2 + Vue 3 + TypeScript. Manage frpc versions, servers and tunnels through a friendly GUI — and say goodbye to hand-writing frpc.toml.",
"flowTitle": "How to use",
"flow": {
"step1Title": "Download frpc",
"step1Desc": "Open \"Download frpc\", grab the build for your platform and architecture, or import a local one.",
"step2Title": "Configure a server",
"step2Desc": "Add the frps address, port and auth token under \"Servers\", then enable one of them.",
"step3Title": "Configure tunnels",
"step3Desc": "Create and enable the tunnels you need (TCP / HTTP, etc.) under \"Tunnels\".",
"step4Title": "One-click start",
"step4Desc": "Head back to the \"Dashboard\" and start Frpc with a single click, watching live logs and status."
},
"repoTitle": "Open source",
"repoDesc": "Stars, issues and contributions are all welcome~",
"openRepo": "Open GitHub",
"versionTitle": "Version",
"currentVersion": "Current version",
"checkUpdate": "Check for updates",
"newVersion": "New version v{version} is available"
},
"update": {
"title": "New version v{version} is available ✨",
"hello": "Ta-da~ A brand new version is ready and waiting for you (๑•̀ㅂ•́)و✧",
"tip": "Here's what this little update brings — scroll down and take a peek~",
"log": "What's new",
"emptyLog": "No release notes this time — the author was too lazy, but updating is still a good idea~",
"ignore": "Ignore this version",
"confirm": "Update now",
"downloading": "Downloading the package… hang tight, almost there!",
"desktopOnly": "Update checks are unavailable in dev mode. Please use the desktop app~",
"upToDate": "You're already on the latest version, enjoy~",
"checkFailed": "Update check failed: {message}",
"ignoreSuccess": "Version ignored. We won't bother you again~"
},
"common": {
"developing": "This feature is under construction",
"confirm": "OK",
+42 -1
View File
@@ -8,6 +8,8 @@
"theme": "主题",
"lightMode": "浅色模式",
"darkMode": "深色模式",
"checkUpdate": "检查更新",
"github": "GitHub",
"settings": "设置",
"minimize": "最小化",
"maximize": "最大化",
@@ -57,7 +59,8 @@
"title": "设置",
"placeholder": "设置页面开发中,敬请期待。",
"menu": {
"appearance": "外观设置"
"appearance": "外观设置",
"about": "关于"
},
"appearance": {
"title": "外观设置",
@@ -210,6 +213,44 @@
"error": "下载失败"
}
},
"about": {
"title": "关于 Frpilot",
"subtitle": "轻量、顺手的 frp 可视化桌面客户端",
"introTitle": "项目介绍",
"intro": "Frpilot 是一个基于 Tauri 2 + Vue 3 + TypeScript 打造的 frp 可视化客户端,帮你用图形界面管理 frpc 版本、服务器与隧道,从此告别手写 frpc.toml。",
"flowTitle": "使用流程",
"flow": {
"step1Title": "下载 frpc",
"step1Desc": "进入「Frpc 下载」,按平台与架构下载,或导入本地已有的 frpc。",
"step2Title": "配置服务器",
"step2Desc": "在「服务器」中填写 frps 地址、端口与鉴权 Token,并启用其中一台。",
"step3Title": "配置隧道",
"step3Desc": "在「隧道列表」里按需创建 TCP / HTTP 等类型的隧道并启用。",
"step4Title": "一键启动",
"step4Desc": "回到「仪表盘」一键启动 Frpc,实时查看运行日志与状态。"
},
"repoTitle": "开源地址",
"repoDesc": "欢迎 Star、提 Issue 或一起参与共建~",
"openRepo": "打开 GitHub",
"versionTitle": "版本信息",
"currentVersion": "当前版本",
"checkUpdate": "检查更新",
"newVersion": "发现新版本 v{version}"
},
"update": {
"title": "发现新版本 v{version} ✨",
"hello": "呀吼~新版本已经打包好,就等你翻牌啦 (๑•̀ㅂ•́)و✧",
"tip": "这次的小更新带来了下面这些变化,往下滑看看嘛~",
"log": "更新日志",
"emptyLog": "作者这次偷懒没写更新说明,总之先更新了就对了~",
"ignore": "忽略该版本",
"confirm": "确认更新",
"downloading": "正在下载安装包…坐好扶稳,马上就到!",
"desktopOnly": "开发模式下无法检查更新,请在桌面应用中使用~",
"upToDate": "已经是最新版本啦,安心享用~",
"checkFailed": "检查更新失败:{message}",
"ignoreSuccess": "已经帮你忽略这个版本啦,下次不再打扰~"
},
"common": {
"developing": "功能开发中,敬请期待",
"confirm": "确定",
+11
View File
@@ -12,17 +12,28 @@
<router-view />
</main>
</div>
<!-- 全局更新弹窗 -->
<UpdateModal />
</div>
</template>
<script setup lang="ts">
import Header from "./header/Header.vue"
import Aside from "./aside/Aside.vue"
import UpdateModal from "@/components/UpdateModal.vue"
import { useUpdateStore } from "@/stores/update"
const router = useRouter()
const updateStore = useUpdateStore()
/** 点击标题栏“设置”按钮 → 跳转设置页 */
function goToSetting() {
router.push("/setting")
}
// 应用启动后静默检查一次更新(已忽略的版本不会打扰用户)
onMounted(() => {
updateStore.checkForUpdate(true)
})
</script>
+6
View File
@@ -45,6 +45,12 @@ const routes: RouteRecordRaw[] = [
component: () => import("@/views/setting/Appearance.vue"),
meta: { title: "settings.menu.appearance" },
},
{
path: "about",
name: "About",
component: () => import("@/views/setting/About.vue"),
meta: { title: "settings.menu.about" },
},
],
},
],
+173
View File
@@ -0,0 +1,173 @@
import { defineStore } from "pinia"
import { message } from "ant-design-vue"
import type { Update } from "@tauri-apps/plugin-updater"
import { checkForAppUpdate, relaunchApp } from "@/api/updater"
import { isTauri } from "@/utils/window"
import { translate } from "@/i18n"
/** 供弹窗展示的更新信息(纯数据,不含方法) */
export interface UpdateInfo {
version: string
currentVersion: string
date?: string
body?: string
}
/**
* 应用更新状态:检查 / 忽略版本 / 下载安装。
*
* 更新弹窗由 {@link UpdateModal} 全局渲染,顶部栏与「关于」页共用此处的检查逻辑。
*/
export const useUpdateStore = defineStore("update", () => {
/** 用户选择「忽略该版本」后持久化的版本号,静默检查时不再提示 */
const ignoredVersion = useStorage<string>("frpilot:ignored-update", "")
const checking = ref(false)
const installing = ref(false)
const modalOpen = ref(false)
const info = ref<UpdateInfo | null>(null)
/** 下载进度 */
const downloaded = ref(0)
const contentLength = ref<number | null>(null)
const progress = ref(0)
/** 持有原始 Update 资源(含下载 / 安装方法),不放进响应式以免代理破坏其方法 */
let pending: Update | null = null
const hasUpdate = computed(() => info.value !== null)
function errMsg(e: unknown) {
return e instanceof Error ? e.message : String(e)
}
function resetProgress() {
downloaded.value = 0
contentLength.value = null
progress.value = 0
}
/**
* 检查更新。
*
* @param silent 静默检查(启动时自动调用):无更新或已忽略该版本时都不打扰用户。
*/
async function checkForUpdate(silent = false) {
if (checking.value) return
if (!isTauri()) {
if (!silent) message.info(translate("update.desktopOnly"))
return
}
checking.value = true
try {
const update = await checkForAppUpdate()
if (!update) {
if (!silent) message.success(translate("update.upToDate"))
return
}
// 静默检查时跳过用户已忽略的版本
if (silent && ignoredVersion.value === update.version) {
await update.close()
return
}
pending = update
info.value = {
version: update.version,
currentVersion: update.currentVersion,
date: update.date,
body: update.body,
}
resetProgress()
modalOpen.value = true
} catch (e) {
if (!silent) {
message.error(translate("update.checkFailed", { message: errMsg(e) }))
}
} finally {
checking.value = false
}
}
/** 放弃当前更新:关闭弹窗并释放后端资源 */
async function discard() {
const update = pending
pending = null
info.value = null
modalOpen.value = false
resetProgress()
try {
await update?.close()
} catch {
// 资源释放失败不影响交互
}
}
/** 取消(本次不更新,下次启动仍会提示) */
function cancel() {
void discard()
}
/** 忽略该版本(持久化,静默检查不再提示) */
async function ignoreVersion() {
if (info.value) ignoredVersion.value = info.value.version
await discard()
message.success(translate("update.ignoreSuccess"))
}
/** 下载并安装更新,随后重启应用以运行新版本 */
async function install() {
if (!pending || installing.value) return
installing.value = true
resetProgress()
try {
await pending.downloadAndInstall((event) => {
switch (event.event) {
case "Started":
contentLength.value = event.data.contentLength ?? null
break
case "Progress":
downloaded.value += event.data.chunkLength
progress.value = contentLength.value
? Math.min(100, Math.round((downloaded.value / contentLength.value) * 100))
: Math.min(99, progress.value + 1)
break
case "Finished":
progress.value = 100
break
}
})
// Windows 会在安装后自动退出;macOS / Linux 需要主动重启
pending = null
info.value = null
modalOpen.value = false
await relaunchApp()
} catch (e) {
message.error(errMsg(e))
} finally {
installing.value = false
}
}
return {
ignoredVersion,
checking,
installing,
modalOpen,
info,
downloaded,
contentLength,
progress,
hasUpdate,
checkForUpdate,
cancel,
ignoreVersion,
install,
}
})
+2
View File
@@ -36,6 +36,7 @@ declare module 'vue' {
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
ASteps: typeof import('ant-design-vue/es')['Steps']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
@@ -49,6 +50,7 @@ declare module 'vue' {
TitleBarActions: typeof import('./../components/TitleBarActions.vue')['default']
TunnelCard: typeof import('./../components/tunnel/TunnelCard.vue')['default']
TunnelFormModal: typeof import('./../components/tunnel/TunnelFormModal.vue')['default']
UpdateModal: typeof import('./../components/UpdateModal.vue')['default']
WindowsTitleBar: typeof import('./../components/WindowsTitleBar.vue')['default']
}
}
+6 -1
View File
@@ -20,7 +20,7 @@
<script setup lang="ts">
import { h } from "vue"
import { BgColorsOutlined } from "@ant-design/icons-vue"
import { BgColorsOutlined, InfoCircleOutlined } from "@ant-design/icons-vue"
import { useI18n } from "vue-i18n"
import type { MenuProps } from "ant-design-vue"
@@ -36,6 +36,11 @@ const menuItems = computed<MenuProps["items"]>(() => [
icon: () => h(BgColorsOutlined),
label: t("settings.menu.appearance"),
},
{
key: "/setting/about",
icon: () => h(InfoCircleOutlined),
label: t("settings.menu.about"),
},
])
const onMenuClick: MenuProps["onClick"] = ({ key }) => {
+94
View File
@@ -0,0 +1,94 @@
<template>
<div class="max-w-3xl">
<h2 class="mb-1 text-lg font-semibold">{{ t("about.title") }}</h2>
<p class="mb-6 text-xs opacity-60">{{ t("about.subtitle") }}</p>
<!-- 项目介绍 -->
<section class="mb-6">
<div class="mb-2 text-sm font-medium">{{ t("about.introTitle") }}</div>
<p class="text-sm leading-relaxed opacity-80">{{ t("about.intro") }}</p>
<div class="mt-3 flex flex-wrap gap-2">
<a-tag v-for="tech in stack" :key="tech" color="blue" class="!me-0">{{ tech }}</a-tag>
</div>
</section>
<a-divider class="!my-5" />
<!-- 使用流程 -->
<section class="mb-6">
<div class="mb-3 text-sm font-medium">{{ t("about.flowTitle") }}</div>
<a-steps direction="vertical" size="small" :current="flowItems.length" :items="flowItems" />
</section>
<a-divider class="!my-5" />
<!-- 开源地址 -->
<section class="mb-6">
<div class="mb-3 text-sm font-medium">{{ t("about.repoTitle") }}</div>
<a-card class="glass-card" size="small" :body-style="{ padding: '14px 16px' }">
<div class="flex items-center gap-3">
<GithubOutlined class="text-2xl" />
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium">SparksFly91 / Frpilot</div>
<div class="truncate text-xs opacity-60">{{ PROJECT_GITHUB_URL }}</div>
</div>
<a-button type="primary" size="small" :icon="h(LinkOutlined)" @click="onOpenRepo">
{{ t("about.openRepo") }}
</a-button>
</div>
</a-card>
<p class="mt-2 text-xs opacity-60">{{ t("about.repoDesc") }}</p>
</section>
<a-divider class="!my-5" />
<!-- 版本信息 / 检查更新 -->
<section>
<div class="mb-3 text-sm font-medium">{{ t("about.versionTitle") }}</div>
<div class="flex flex-wrap items-center gap-3">
<span class="text-xs opacity-60">{{ t("about.currentVersion") }}</span>
<a-tag color="purple" class="!me-0">v{{ version }}</a-tag>
<a-button size="small" :loading="updateStore.checking" @click="onCheckUpdate">
{{ t("about.checkUpdate") }}
</a-button>
<span v-if="updateStore.hasUpdate" class="text-xs text-[#6366f1]">
{{ t("about.newVersion", { version: updateStore.info?.version }) }}
</span>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { h } from "vue"
import { GithubOutlined, LinkOutlined } from "@ant-design/icons-vue"
import { useI18n } from "vue-i18n"
import { getAppVersion, openProjectRepo, PROJECT_GITHUB_URL } from "@/api/updater"
import { useUpdateStore } from "@/stores/update"
const { t } = useI18n()
const updateStore = useUpdateStore()
const version = ref("")
const stack = ["Tauri 2", "Vue 3", "TypeScript", "Rust", "Vite"]
const flowItems = computed(() => [
{ title: t("about.flow.step1Title"), description: t("about.flow.step1Desc") },
{ title: t("about.flow.step2Title"), description: t("about.flow.step2Desc") },
{ title: t("about.flow.step3Title"), description: t("about.flow.step3Desc") },
{ title: t("about.flow.step4Title"), description: t("about.flow.step4Desc") },
])
onMounted(async () => {
version.value = await getAppVersion()
})
function onOpenRepo() {
void openProjectRepo()
}
function onCheckUpdate() {
void updateStore.checkForUpdate(false)
}
</script>