Add lightweight update check via Gitea releases; bump to 0.2.0
- check_update command: query Gitea releases/latest, compare versions (404 = no release yet -> treated as up to date), return notes + asset URL - open_external command to open the download page / installer - "检查更新" button in the top bar (manual check only) - docs/发布与更新.md: release & update workflow - bump app version to 0.2.0 (tauri.conf / package.json / Cargo) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fd2dfbdfb8
commit
8320432789
|
|
@ -0,0 +1,32 @@
|
|||
# 发布与更新(轻量版)
|
||||
|
||||
应用内「检查更新」按钮(顶栏右上角)走**轻量版本检查**:查询 Gitea 最新 release,与当前版本号比较,有新版则弹窗显示发布说明并提供「前往下载」。不做签名校验、不在应用内静默安装——用户下载安装包后手动运行覆盖安装。
|
||||
|
||||
## 工作原理
|
||||
|
||||
- 检查接口(硬编码于 `src-tauri/src/main.rs` 的 `GITEA_LATEST_RELEASE_API`):
|
||||
`http://gitea.xxhhcty.xyz:8080/api/v1/repos/zcdsj/tcjs/releases/latest`
|
||||
- 当前版本取自 `src-tauri/tauri.conf.json` 的 `version`(`app.package_info().version`)。
|
||||
- 比较规则:release 的 `tag_name`(去掉前导 `v`/`V`)按点分十进制与当前版本比大小。
|
||||
- 「前往下载」打开 release 资产里第一个 `.exe`/`.msi`,没有资产则打开 release 页面。
|
||||
|
||||
## 发布一个新版本
|
||||
|
||||
1. **改版本号**:编辑 `src-tauri/tauri.conf.json` 的 `version`(如 `0.1.0` → `0.2.0`)。建议同步 `package.json`、`src-tauri/Cargo.toml`,保持一致。
|
||||
2. **构建安装包**:`npm run tauri:build`
|
||||
产物在 `src-tauri/target/release/bundle/`:
|
||||
- `nsis/*.exe`(推荐发布这个)
|
||||
- `msi/*.msi`
|
||||
3. **在 Gitea 建 release**:
|
||||
- 仓库 `zcdsj/tcjs` → Releases → New Release。
|
||||
- **Tag** 用 `v0.2.0`(与第 1 步版本一致;检查时会去掉前导 `v`)。
|
||||
- 把第 2 步的 `.exe`(和/或 `.msi`)作为**附件**上传。
|
||||
- release 正文(body)会作为「发布说明」显示在更新弹窗里,写更新内容即可。
|
||||
- 发布(标记为最新 release)。
|
||||
4. 旧版本用户点「检查更新」→ 发现 `v0.2.0` 比本地新 → 弹窗 → 「前往下载」→ 下载安装包覆盖安装。
|
||||
|
||||
## 备注 / 后续可选
|
||||
|
||||
- **HTTP 站点**:当前 ureq 关闭了 TLS(Gitea 走 http)。若 Gitea 迁到 https,需要给 `src-tauri/Cargo.toml` 的 `ureq` 打开 `tls`(rustls)特性。
|
||||
- **改成静默自动更新**:若以后要"应用内下载+安装+重启",可切换到官方 `tauri-plugin-updater`(需生成签名密钥、构建时签名、每次发布额外上传 `latest.json` 与 `.sig`)。本版刻意不做以保持轻量。
|
||||
- **API 私有仓库**:若仓库非公开,Gitea API 需要 token;当前实现按公开仓库匿名访问。
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "ceramic-radioactivity-app",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --config ui/vite.config.ts",
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ceramic-radioactivity-tauri"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"ceramic-radioactivity",
|
||||
"rusqlite",
|
||||
|
|
@ -295,6 +295,7 @@ dependencies = [
|
|||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3763,6 +3764,31 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
|
|
@ -3794,6 +3820,12 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ceramic-radioactivity-tauri"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
|
|
@ -14,3 +14,4 @@ serde = { version = "1", features = ["derive"] }
|
|||
serde_json = "1.0.150"
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
ureq = { version = "3.3.0", default-features = false }
|
||||
|
|
|
|||
|
|
@ -78,6 +78,110 @@ fn export_excel(app: tauri::AppHandle, db: State<Db>, id: i64) -> Result<Option<
|
|||
excel::write_to_path(&path, &detail).map(Some)
|
||||
}
|
||||
|
||||
/// Gitea「最新 release」API。如迁移到 https 站点,需为 ureq 启用 tls 特性。
|
||||
const GITEA_LATEST_RELEASE_API: &str =
|
||||
"http://gitea.xxhhcty.xyz:8080/api/v1/repos/zcdsj/tcjs/releases/latest";
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct UpdateInfo {
|
||||
current_version: String,
|
||||
latest_version: String,
|
||||
has_update: bool,
|
||||
notes: String,
|
||||
release_url: String,
|
||||
download_url: Option<String>,
|
||||
}
|
||||
|
||||
/// 查询 Gitea 最新 release,与当前版本比较(轻量版本检查,不做签名校验)。
|
||||
#[tauri::command]
|
||||
fn check_update(app: tauri::AppHandle) -> Result<UpdateInfo, String> {
|
||||
let current = app.package_info().version.to_string();
|
||||
|
||||
let mut resp = match ureq::get(GITEA_LATEST_RELEASE_API).call() {
|
||||
Ok(resp) => resp,
|
||||
// 仓库尚无任何发布:视为已是最新,而非报错。
|
||||
Err(ureq::Error::StatusCode(404)) => return Ok(up_to_date(current)),
|
||||
Err(ureq::Error::StatusCode(code)) => return Err(format!("Gitea 返回状态 {code}")),
|
||||
Err(e) => return Err(format!("请求 Gitea 失败:{e}")),
|
||||
};
|
||||
match resp.status().as_u16() {
|
||||
200 => {}
|
||||
404 => return Ok(up_to_date(current)),
|
||||
code => return Err(format!("Gitea 返回状态 {code}")),
|
||||
}
|
||||
let body = resp.body_mut().read_to_string().map_err(|e| e.to_string())?;
|
||||
let json: serde_json::Value = serde_json::from_str(&body).map_err(|e| e.to_string())?;
|
||||
|
||||
let latest_version = json["tag_name"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.trim_start_matches(['v', 'V'])
|
||||
.to_string();
|
||||
let notes = json["body"].as_str().unwrap_or_default().to_string();
|
||||
let release_url = json["html_url"].as_str().unwrap_or_default().to_string();
|
||||
let download_url = json["assets"].as_array().and_then(|assets| {
|
||||
assets.iter().find_map(|asset| {
|
||||
let name = asset["name"].as_str().unwrap_or_default().to_ascii_lowercase();
|
||||
if name.ends_with(".exe") || name.ends_with(".msi") {
|
||||
asset["browser_download_url"].as_str().map(str::to_string)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Ok(UpdateInfo {
|
||||
has_update: is_newer(&latest_version, ¤t),
|
||||
current_version: current,
|
||||
latest_version,
|
||||
notes,
|
||||
release_url,
|
||||
download_url,
|
||||
})
|
||||
}
|
||||
|
||||
/// 构造「已是最新」结果(无更新)。
|
||||
fn up_to_date(current: String) -> UpdateInfo {
|
||||
UpdateInfo {
|
||||
latest_version: current.clone(),
|
||||
current_version: current,
|
||||
has_update: false,
|
||||
notes: String::new(),
|
||||
release_url: String::new(),
|
||||
download_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 用默认浏览器打开 URL(跳转到 release 下载页 / 安装包)。
|
||||
#[tauri::command]
|
||||
fn open_external(url: String) -> Result<(), String> {
|
||||
let mut command = std::process::Command::new("cmd");
|
||||
command.args(["/C", "start", "", &url]);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
command.creation_flags(0x0800_0000); // CREATE_NO_WINDOW,避免闪现控制台
|
||||
}
|
||||
command.spawn().map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 比较点分十进制版本:a 是否比 b 新(缺位按 0)。
|
||||
fn is_newer(a: &str, b: &str) -> bool {
|
||||
let parse = |s: &str| -> Vec<u64> {
|
||||
s.split('.').map(|p| p.trim().parse().unwrap_or(0)).collect()
|
||||
};
|
||||
let (va, vb) = (parse(a), parse(b));
|
||||
for i in 0..va.len().max(vb.len()) {
|
||||
let x = va.get(i).copied().unwrap_or(0);
|
||||
let y = vb.get(i).copied().unwrap_or(0);
|
||||
if x != y {
|
||||
return x > y;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
|
|
@ -95,8 +199,26 @@ fn main() {
|
|||
list_records,
|
||||
get_record,
|
||||
delete_record,
|
||||
export_excel
|
||||
export_excel,
|
||||
check_update,
|
||||
open_external
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run Tauri application");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_newer;
|
||||
|
||||
#[test]
|
||||
fn version_comparison() {
|
||||
assert!(is_newer("0.2.0", "0.1.0"));
|
||||
assert!(is_newer("1.0.0", "0.9.9"));
|
||||
assert!(is_newer("0.1.1", "0.1.0"));
|
||||
assert!(is_newer("0.2", "0.1.5")); // 缺位按 0
|
||||
assert!(!is_newer("0.1.0", "0.1.0"));
|
||||
assert!(!is_newer("0.1.0", "0.2.0"));
|
||||
assert!(!is_newer("", "0.1.0"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Building Material Radioactivity",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"identifier": "com.tcjs.ceramic-radioactivity",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { App as AntApp, ConfigProvider, Tabs } from "antd";
|
|||
import zhCN from "antd/locale/zh_CN";
|
||||
import { CalculatorPanel } from "./CalculatorPanel";
|
||||
import { HistoryTab } from "./HistoryTab";
|
||||
import { UpdateButton } from "./UpdateButton";
|
||||
import type { SampleInput } from "./types";
|
||||
|
||||
function Brand() {
|
||||
|
|
@ -47,7 +48,7 @@ function App() {
|
|||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className="main-tabs"
|
||||
tabBarExtraContent={{ left: <Brand /> }}
|
||||
tabBarExtraContent={{ left: <Brand />, right: <UpdateButton /> }}
|
||||
items={[
|
||||
{
|
||||
key: "calc",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { App, Button } from "antd";
|
||||
import type { UpdateInfo } from "./types";
|
||||
|
||||
/** 手动「检查更新」:查询 Gitea 最新 release,有新版则提示并可跳转下载。 */
|
||||
function UpdateButton() {
|
||||
const { message, modal } = App.useApp();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const check = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const info = await invoke<UpdateInfo>("check_update");
|
||||
if (info.has_update) {
|
||||
modal.confirm({
|
||||
title: `发现新版本 v${info.latest_version}`,
|
||||
width: 540,
|
||||
okText: "前往下载",
|
||||
cancelText: "稍后",
|
||||
content: (
|
||||
<div>
|
||||
<p className="update-versions">
|
||||
当前 v{info.current_version} → 最新 <strong>v{info.latest_version}</strong>
|
||||
</p>
|
||||
{info.notes ? <pre className="update-notes">{info.notes}</pre> : null}
|
||||
</div>
|
||||
),
|
||||
onOk: () =>
|
||||
invoke("open_external", { url: info.download_url || info.release_url }).catch((err) =>
|
||||
message.error(`打开下载页失败:${String(err)}`)
|
||||
)
|
||||
});
|
||||
} else {
|
||||
message.success(`已是最新版本 v${info.current_version}`);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(`检查更新失败:${String(err)}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button size="small" className="update-btn" loading={loading} onClick={() => void check()}>
|
||||
检查更新
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { UpdateButton };
|
||||
|
|
@ -83,6 +83,39 @@ body {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 顶栏右侧「检查更新」按钮:navy 底上的浅色描边按钮 */
|
||||
.update-btn {
|
||||
background: transparent;
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
color: #e8edf5;
|
||||
}
|
||||
|
||||
.update-btn:hover,
|
||||
.update-btn:focus {
|
||||
background: transparent !important;
|
||||
border-color: #f2b50c !important;
|
||||
color: #f2b50c !important;
|
||||
}
|
||||
|
||||
.update-versions {
|
||||
margin: 4px 0 10px;
|
||||
color: #69727c;
|
||||
}
|
||||
|
||||
.update-notes {
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #e9edf1;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.results-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
|
|
|||
|
|
@ -109,6 +109,15 @@ export type RecordDetail = {
|
|||
result: CalculationResult;
|
||||
};
|
||||
|
||||
export type UpdateInfo = {
|
||||
current_version: string;
|
||||
latest_version: string;
|
||||
has_update: boolean;
|
||||
notes: string;
|
||||
release_url: string;
|
||||
download_url: string | null;
|
||||
};
|
||||
|
||||
export type RecordFilter = {
|
||||
sample_id: string | null;
|
||||
material_type: MaterialType | null;
|
||||
|
|
|
|||
Loading…
Reference in New Issue