refactor: 重构更新逻辑,新增可视化更新弹窗
1. 新增tauri-plugin-prevent-default插件,区分开发/生产模式拦截默认事件 2. 重构updater工具函数,拆分检查与更新流程,新增进度状态管理 3. 新增UpdateModal更新弹窗组件,支持markdown格式更新说明与下载进度展示 4. 全局注册新增的naive-ui组件与自定义更新弹窗 5. 新增markdown-it依赖 6. 替换原有弹窗逻辑,将更新流程交由弹窗组件处理
This commit is contained in:
parent
c45e8b0d43
commit
72b2d5bdfe
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "stealthreader",
|
||||
"private": true,
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"axios": "^1.19.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"markdown-it": "^15.0.0",
|
||||
"naive-ui": "^2.45.0",
|
||||
"pinia": "^4.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ importers:
|
|||
dayjs:
|
||||
specifier: ^1.11.21
|
||||
version: 1.11.23
|
||||
markdown-it:
|
||||
specifier: ^15.0.0
|
||||
version: 15.0.0
|
||||
naive-ui:
|
||||
specifier: ^2.45.0
|
||||
version: 2.45.0(vue@3.5.41(typescript@5.6.3))
|
||||
|
|
@ -586,6 +589,9 @@ packages:
|
|||
alien-signals@3.2.1:
|
||||
resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
|
||||
|
||||
argparse@3.0.0:
|
||||
resolution: {integrity: sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==}
|
||||
|
||||
ast-kit@2.1.2:
|
||||
resolution: {integrity: sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==}
|
||||
engines: {node: '>=20.18.0'}
|
||||
|
|
@ -679,6 +685,10 @@ packages:
|
|||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@8.0.0:
|
||||
resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -874,6 +884,9 @@ packages:
|
|||
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
linkify-it@6.1.0:
|
||||
resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==}
|
||||
|
||||
local-pkg@1.2.1:
|
||||
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -894,10 +907,17 @@ packages:
|
|||
magic-string@1.2.0:
|
||||
resolution: {integrity: sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw==}
|
||||
|
||||
markdown-it@15.0.0:
|
||||
resolution: {integrity: sha512-Lf8ajvVNdRpzSNB4VegxNy7gjs8gU35l4b4+ET49LrQC5PKYwLZ72u60LeJ9gv3qiaesuYjJWCyVeQmv/QWKQw==}
|
||||
hasBin: true
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mdurl@2.1.0:
|
||||
resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
@ -990,6 +1010,10 @@ packages:
|
|||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
punycode.js@2.3.1:
|
||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
quansync@0.2.11:
|
||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||
|
||||
|
|
@ -1032,6 +1056,9 @@ packages:
|
|||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uc.micro@3.0.0:
|
||||
resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==}
|
||||
|
||||
ufo@1.6.4:
|
||||
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
|
||||
|
||||
|
|
@ -1616,6 +1643,8 @@ snapshots:
|
|||
|
||||
alien-signals@3.2.1: {}
|
||||
|
||||
argparse@3.0.0: {}
|
||||
|
||||
ast-kit@2.1.2:
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.8
|
||||
|
|
@ -1706,6 +1735,8 @@ snapshots:
|
|||
|
||||
entities@7.0.1: {}
|
||||
|
||||
entities@8.0.0: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
|
@ -1860,6 +1891,10 @@ snapshots:
|
|||
lightningcss-win32-arm64-msvc: 1.33.0
|
||||
lightningcss-win32-x64-msvc: 1.33.0
|
||||
|
||||
linkify-it@6.1.0:
|
||||
dependencies:
|
||||
uc.micro: 3.0.0
|
||||
|
||||
local-pkg@1.2.1:
|
||||
dependencies:
|
||||
mlly: 1.8.2
|
||||
|
|
@ -1882,8 +1917,19 @@ snapshots:
|
|||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
markdown-it@15.0.0:
|
||||
dependencies:
|
||||
argparse: 3.0.0
|
||||
entities: 8.0.0
|
||||
linkify-it: 6.1.0
|
||||
mdurl: 2.1.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 3.0.0
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mdurl@2.1.0: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
|
|
@ -1976,6 +2022,8 @@ snapshots:
|
|||
|
||||
proxy-from-env@2.1.0: {}
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
||||
quansync@0.2.11: {}
|
||||
|
||||
readdirp@5.1.1: {}
|
||||
|
|
@ -2027,6 +2075,8 @@ snapshots:
|
|||
|
||||
typescript@5.6.3: {}
|
||||
|
||||
uc.micro@3.0.0: {}
|
||||
|
||||
ufo@1.6.4: {}
|
||||
|
||||
undici-types@8.3.0: {}
|
||||
|
|
|
|||
|
|
@ -2052,6 +2052,15 @@ dependencies = [
|
|||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
|
|
@ -4076,6 +4085,7 @@ dependencies = [
|
|||
"tauri-helper",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-prevent-default",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
|
|
@ -4122,6 +4132,27 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
|
@ -4480,6 +4511,20 @@ dependencies = [
|
|||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-prevent-default"
|
||||
version = "5.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "674bac5c9831f4ca7ae42840768ac968b76353dcbe30bb0aad149c56f0be1389"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"itertools",
|
||||
"serde",
|
||||
"strum",
|
||||
"tauri",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-process"
|
||||
version = "2.3.1"
|
||||
|
|
|
|||
|
|
@ -41,3 +41,4 @@ regex = "1.13.1"
|
|||
chardetng = "1.0.0"
|
||||
tauri-plugin-updater = "2.10.1"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-prevent-default = "5.0.2"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use commands::book::*;
|
|||
use services::db::init_pool;
|
||||
use tauri::Manager;
|
||||
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
|
|
@ -13,6 +15,7 @@ pub fn run() {
|
|||
.plugin(tauri_plugin_dialog::init()) // 对话框插件
|
||||
.plugin(tauri_plugin_updater::Builder::new().build()) // 更新插件
|
||||
.plugin(tauri_plugin_process::init()) // 进程插件(更新后重启)
|
||||
.plugin(prevent_default())
|
||||
.setup(|app| {
|
||||
let pool = tauri::async_runtime::block_on(async {
|
||||
init_pool(app.handle())
|
||||
|
|
@ -28,3 +31,15 @@ pub fn run() {
|
|||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
// 开发模式: 保留DevTools和Reload
|
||||
#[cfg(debug_assertions)]
|
||||
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
tauri_plugin_prevent_default::Builder::new().with_flags(Flags::all().difference(Flags::DEV_TOOLS | Flags::RELOAD)).build()
|
||||
}
|
||||
|
||||
// 生产模式: 阻止所有默认事件
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
tauri_plugin_prevent_default::init()
|
||||
}
|
||||
22
src/App.vue
22
src/App.vue
|
|
@ -1,14 +1,13 @@
|
|||
<template>
|
||||
<n-config-provider
|
||||
:locale="zhCN"
|
||||
:data-locale="dateZhCN"
|
||||
:theme="isDark ? darkTheme : null"
|
||||
>
|
||||
<n-config-provider :locale="zhCN" :data-locale="dateZhCN" :theme="isDark ? darkTheme : null">
|
||||
<n-message-provider>
|
||||
<n-notification-provider>
|
||||
<n-dialog-provider>
|
||||
<GlobalLoading />
|
||||
<RouterView />
|
||||
<n-modal-provider>
|
||||
<GlobalLoading />
|
||||
<UpdateModal />
|
||||
<RouterView />
|
||||
</n-modal-provider>
|
||||
</n-dialog-provider>
|
||||
</n-notification-provider>
|
||||
</n-message-provider>
|
||||
|
|
@ -18,7 +17,8 @@
|
|||
<script setup lang="ts">
|
||||
import { zhCN, dateZhCN, darkTheme } from "naive-ui"
|
||||
import GlobalLoading from "@/components/GlobalLoading.vue"
|
||||
import { checkAndUpdate } from "@/utils/updater"
|
||||
import UpdateModal from "@/components/UpdateModal.vue"
|
||||
import { checkAndPrompt } from "@/utils/updater"
|
||||
import { useSettingStore } from "@/stores/setting"
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
|
@ -40,7 +40,7 @@ const onSystemThemeChange = (e: MediaQueryListEvent) => {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkAndUpdate()
|
||||
checkAndPrompt()
|
||||
media.addEventListener("change", onSystemThemeChange)
|
||||
})
|
||||
|
||||
|
|
@ -49,6 +49,4 @@ onUnmounted(() => {
|
|||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,238 @@
|
|||
<template>
|
||||
<n-modal
|
||||
:show="showUpdateModal"
|
||||
preset="card"
|
||||
:title="modalTitle"
|
||||
:bordered="false"
|
||||
:closable="!busy"
|
||||
:mask-closable="false"
|
||||
:close-on-esc="false"
|
||||
style="width: 560px"
|
||||
@close="cancelUpdate"
|
||||
>
|
||||
<template #header-extra>
|
||||
<span v-if="updateInfo" class="update-current">当前 v{{ updateInfo.currentVersion }}</span>
|
||||
</template>
|
||||
|
||||
<div class="update-body">
|
||||
<n-scrollbar class="update-scroll">
|
||||
<div v-if="renderedBody" class="markdown-body" v-html="renderedBody" />
|
||||
<n-empty v-else description="暂无更新说明" />
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
|
||||
<div v-if="downloading || installing" class="update-progress">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="progress"
|
||||
:processing="downloading"
|
||||
indicator-placement="inside"
|
||||
:height="18"
|
||||
/>
|
||||
<div class="progress-hint">{{ progressHint }}</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="update-footer">
|
||||
<n-button quaternary :disabled="busy" @click="skipVersion">跳过此版本</n-button>
|
||||
<div class="update-footer-right">
|
||||
<n-button v-if="!busy" @click="cancelUpdate">取消</n-button>
|
||||
<n-button type="primary" :loading="downloading" @click="startUpdate">
|
||||
{{ installing ? "安装中..." : downloading ? "下载中..." : "立即更新" }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import MarkdownIt from "markdown-it"
|
||||
import {
|
||||
showUpdateModal,
|
||||
updateInfo,
|
||||
downloading,
|
||||
installing,
|
||||
progress,
|
||||
downloadedBytes,
|
||||
totalBytes,
|
||||
formatBytes,
|
||||
startUpdate,
|
||||
skipVersion,
|
||||
cancelUpdate,
|
||||
} from "@/utils/updater"
|
||||
|
||||
const md = new MarkdownIt({ html: false, linkify: true })
|
||||
|
||||
const busy = computed(() => downloading.value || installing.value)
|
||||
|
||||
const modalTitle = computed(() =>
|
||||
updateInfo.value ? `发现新版本 v${updateInfo.value.version}` : "发现新版本"
|
||||
)
|
||||
|
||||
const renderedBody = computed(() => md.render(updateInfo.value?.body || ""))
|
||||
|
||||
const progressHint = computed(() => {
|
||||
if (installing.value) return "正在安装更新,即将重启应用..."
|
||||
if (downloading.value) {
|
||||
if (totalBytes.value > 0) {
|
||||
return `正在下载更新 ${progress.value}% · ${formatBytes(downloadedBytes.value)} / ${formatBytes(totalBytes.value)}`
|
||||
}
|
||||
return `正在下载更新... ${formatBytes(downloadedBytes.value)}`
|
||||
}
|
||||
return ""
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.update-current {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.update-body {
|
||||
.update-scroll {
|
||||
height: 340px;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
padding-right: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text-primary);
|
||||
word-break: break-word;
|
||||
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
:deep(h3),
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
margin: 14px 0 8px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
:deep(h1) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(h2) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin: 6px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
:deep(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-surface-hover);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
margin: 8px 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface-hover);
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
margin: 8px 0;
|
||||
padding: 4px 12px;
|
||||
border-left: 3px solid var(--color-border-hover);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
:deep(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
:deep(table) {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(th),
|
||||
:deep(td) {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
:deep(th) {
|
||||
background: var(--color-surface-hover);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.update-progress {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.progress-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.update-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.update-footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -36,9 +36,11 @@ declare module 'vue' {
|
|||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
|
|
@ -47,5 +49,6 @@ declare module 'vue' {
|
|||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
UpdateModal: typeof import('./../components/UpdateModal.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
declare module "markdown-it" {
|
||||
interface MarkdownItOptions {
|
||||
html?: boolean
|
||||
xhtmlOut?: boolean
|
||||
breaks?: boolean
|
||||
langPrefix?: string
|
||||
linkify?: boolean
|
||||
typographer?: boolean
|
||||
quotes?: string | string[]
|
||||
highlight?: (str: string, lang: string, attrs: string) => string
|
||||
}
|
||||
|
||||
class MarkdownIt {
|
||||
constructor(options?: MarkdownItOptions)
|
||||
set(options: MarkdownItOptions): this
|
||||
render(src: string, env?: unknown): string
|
||||
parse(src: string, env?: unknown): unknown[]
|
||||
options: MarkdownItOptions
|
||||
renderer: unknown
|
||||
}
|
||||
|
||||
export default MarkdownIt
|
||||
}
|
||||
|
|
@ -1,54 +1,137 @@
|
|||
import { check, type Update } from "@tauri-apps/plugin-updater"
|
||||
import { ask } from "@tauri-apps/plugin-dialog"
|
||||
import { ref } from "vue"
|
||||
import { check, type Update, type DownloadEvent } from "@tauri-apps/plugin-updater"
|
||||
import { relaunch } from "@tauri-apps/plugin-process"
|
||||
|
||||
/**
|
||||
* 更新检查结果
|
||||
* - updated:已确认更新并开始安装(随后应用重启)
|
||||
* - none:当前已是最新版本
|
||||
* - cancelled:存在更新但用户取消
|
||||
* - error:检查或安装过程出错
|
||||
*/
|
||||
export type CheckUpdateResult = "updated" | "none" | "cancelled" | "error"
|
||||
export type CheckPromptResult = "none" | "error" | "prompting"
|
||||
|
||||
/**
|
||||
* 仅检查是否有可用更新,返回 Update 对象或 null
|
||||
*/
|
||||
export async function checkUpdate(): Promise<Update | null> {
|
||||
return await check()
|
||||
// 当前待更新的 Update 对象(非响应式,避免代理破坏资源句柄)
|
||||
let currentUpdate: Update | null = null
|
||||
|
||||
export const checking = ref(false)
|
||||
export const downloading = ref(false)
|
||||
export const installing = ref(false)
|
||||
export const showUpdateModal = ref(false)
|
||||
export const progress = ref(0)
|
||||
export const downloadedBytes = ref(0)
|
||||
export const totalBytes = ref(0)
|
||||
|
||||
export const updateInfo = ref<{
|
||||
currentVersion: string
|
||||
version: string
|
||||
date?: string
|
||||
body?: string
|
||||
} | null>(null)
|
||||
|
||||
const SKIPPED_KEY = "skippedUpdateVersion"
|
||||
|
||||
function getSkippedVersion(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(SKIPPED_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function setSkippedVersion(version: string) {
|
||||
try {
|
||||
localStorage.setItem(SKIPPED_KEY, version)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes <= 0) return "0 B"
|
||||
const units = ["B", "KB", "MB", "GB"]
|
||||
let v = bytes
|
||||
let i = 0
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024
|
||||
i++
|
||||
}
|
||||
return `${v.toFixed(v >= 10 || i === 0 ? 0 : 1)} ${units[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 询问用户是否立即更新,确认后下载安装并重启应用
|
||||
* @param update 更新对象
|
||||
* @returns true=已确认并开始安装;false=用户取消
|
||||
* 检查更新,若有可用更新则展示更新弹窗
|
||||
* @returns none=无更新/已跳过;error=检查出错;prompting=已展示弹窗等待用户操作
|
||||
*/
|
||||
export async function promptAndInstall(update: Update): Promise<boolean> {
|
||||
const yes = await ask(
|
||||
`发现新版本 ${update.version},是否现在更新?\n\n${update.body || ""}`,
|
||||
{ title: "发现更新", kind: "info" }
|
||||
)
|
||||
if (!yes) return false
|
||||
|
||||
await update.downloadAndInstall()
|
||||
await relaunch()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键检查并更新(完整流程)。
|
||||
* 适合应用启动时静默调用:无更新、用户取消或出错均不打扰用户。
|
||||
* @returns 更新结果状态
|
||||
*/
|
||||
export async function checkAndUpdate(): Promise<CheckUpdateResult> {
|
||||
export async function checkAndPrompt(): Promise<CheckPromptResult> {
|
||||
if (checking.value || downloading.value || installing.value) return "none"
|
||||
checking.value = true
|
||||
try {
|
||||
const update = await check()
|
||||
if (!update) return "none"
|
||||
if (getSkippedVersion() === update.version) return "none"
|
||||
|
||||
const installed = await promptAndInstall(update)
|
||||
return installed ? "updated" : "cancelled"
|
||||
currentUpdate = update
|
||||
updateInfo.value = {
|
||||
currentVersion: update.currentVersion,
|
||||
version: update.version,
|
||||
date: update.date,
|
||||
body: update.body,
|
||||
}
|
||||
progress.value = 0
|
||||
downloadedBytes.value = 0
|
||||
totalBytes.value = 0
|
||||
showUpdateModal.value = true
|
||||
return "prompting"
|
||||
} catch (error) {
|
||||
console.error("检查更新失败:", error)
|
||||
return "error"
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始下载并安装更新(带进度回调),完成后重启应用
|
||||
*/
|
||||
export async function startUpdate(): Promise<void> {
|
||||
if (!currentUpdate || downloading.value || installing.value) return
|
||||
downloading.value = true
|
||||
progress.value = 0
|
||||
downloadedBytes.value = 0
|
||||
totalBytes.value = 0
|
||||
try {
|
||||
await currentUpdate.download((event: DownloadEvent) => {
|
||||
if (event.event === "Started") {
|
||||
totalBytes.value = event.data.contentLength ?? 0
|
||||
} else if (event.event === "Progress") {
|
||||
downloadedBytes.value += event.data.chunkLength
|
||||
if (totalBytes.value > 0) {
|
||||
progress.value = Math.min(
|
||||
100,
|
||||
Math.round((downloadedBytes.value / totalBytes.value) * 100)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
downloading.value = false
|
||||
installing.value = true
|
||||
await currentUpdate.install()
|
||||
await relaunch()
|
||||
} catch (error) {
|
||||
console.error("更新失败:", error)
|
||||
downloading.value = false
|
||||
installing.value = false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳过当前版本:记录已跳过版本并关闭弹窗 */
|
||||
export function skipVersion() {
|
||||
if (currentUpdate) {
|
||||
setSkippedVersion(currentUpdate.version)
|
||||
}
|
||||
currentUpdate = null
|
||||
updateInfo.value = null
|
||||
showUpdateModal.value = false
|
||||
}
|
||||
|
||||
/** 取消更新:关闭弹窗 */
|
||||
export function cancelUpdate() {
|
||||
currentUpdate = null
|
||||
updateInfo.value = null
|
||||
showUpdateModal.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ import {
|
|||
ThunderboltOutlined,
|
||||
} from "@vicons/antd"
|
||||
import appIcon from "../../../app-icon.svg"
|
||||
import { checkAndUpdate } from "@/utils/updater"
|
||||
import { checkAndPrompt } from "@/utils/updater"
|
||||
|
||||
const message = useMessage()
|
||||
const year = new Date().getFullYear()
|
||||
|
|
@ -92,13 +92,13 @@ const onCheckUpdate = async () => {
|
|||
if (checkingUpdate.value) return
|
||||
checkingUpdate.value = true
|
||||
try {
|
||||
const result = await checkAndUpdate()
|
||||
const result = await checkAndPrompt()
|
||||
if (result === "none") {
|
||||
message.success("当前已是最新版本")
|
||||
} else if (result === "error") {
|
||||
message.error("检查更新失败,请稍后重试")
|
||||
}
|
||||
// updated:即将重启应用;cancelled:用户取消,无需提示
|
||||
// prompting:已展示更新弹窗,由弹窗处理后续流程
|
||||
} finally {
|
||||
checkingUpdate.value = false
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue