创建应用
- 环境依赖
- 创建项目
sh
pnpm create tauri-app
应用程序更新
1.安装依赖
安装后会自动写入到 package.json 文件和 Cargo.toml 文件,支持 前端 JS 调用和 后端 Rust 调用
sh
pnpm tauri add updater
2.配置
密钥生成,在package.json文件中添加,如下命令生成更新公钥和私钥
json
"@description:updater": "Tauri CLI 提供了 signer generate 命令 生成更新密钥",
"updater": "tauri signer generate -w ~/.tauri/myapp.key"
在windows环境变量配置私钥,输入cmd 命令行执行 win cmd
sh
set TAURI_PRIVATE_KEY="content of the generated key"
set TAURI_KEY_PASSWORD="password"
powershell
sh
$env:TAURI_PRIVATE_KEY="content of the generated key"
$env:TAURI_KEY_PASSWORD="password"
在 src-tauri\tauri.conf.json 文件中开启自动升级,并将公钥添加到里面,设置你的升级信息json文件获取的url路径
json
{
"app": {},
"bundle": {
"createUpdaterArtifacts": true,
"icon": []
},
"plugins": {
"updater": {
"active": true,
"windows": {
"installMode": "passive"
},
"pubkey": "公钥",
"endpoints": ["https://xxx/download/latest.json"]
}
}
}
更新 latest.json 内容
json
{
"version": "v1.0.0",
"notes": "Test version",
"pub_date": "2020-06-22T19:25:57Z",
"platforms": {
"darwin-x86_64": {
"signature": "Content of app.tar.gz.sig",
"url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x86_64.app.tar.gz"
},
"darwin-aarch64": {
"signature": "Content of app.tar.gz.sig",
"url": "https://github.com/username/reponame/releases/download/v1.0.0/app-aarch64.app.tar.gz"
},
"linux-x86_64": {
"signature": "Content of app.AppImage.tar.gz.sig",
"url": "https://github.com/username/reponame/releases/download/v1.0.0/app-amd64.AppImage.tar.gz"
},
"windows-x86_64": {
"signature": "Content of app.msi.sig",
"url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x64.msi.zip"
}
}
}
tauri 权限配置 src-tauri\capabilities\default.json
json
{
"permissions": [
"updater:default",
"updater:allow-check",
"updater:allow-download",
"updater:allow-install"
]
}
3.封装hooks
src\hooks\updater.ts
ts
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
export default () => {
const message = window.$message;
const dialog = window.$dialog;
const checkV = async () => {
return await check()
.then((e: any) => {
if (!e?.available) {
return;
}
return {
version: e.version,
meg: `新版本 ${e.version} ,发布时间: ${e.date} 升级信息: ${e.body}`,
};
})
.catch((e) => {
console.error("检查更新错误,请稍后再试 " + e);
});
};
const updater = async () => {
dialog.success({
title: "系统提示",
content: "您确认要更新吗 ?",
positiveText: "更新",
negativeText: "不更新",
maskClosable: false,
closable: false,
onPositiveClick: async () => {
message.success("正在下载更新,请稍等");
await check()
.then(async (e: any) => {
if (!e?.available) {
return;
}
await e.downloadAndInstall((event: any) => {
switch (event.event) {
case "Started":
message.success(
"文件大小:" + event.data.contentLength
? event.data.contentLength
: 0
);
break;
case "Progress":
message.success("正在下载" + event.data.chunkLength);
break;
case "Finished":
message.success("安装包下载成功,10s后重启并安装");
setTimeout(async () => {
await relaunch();
}, 10000);
break;
}
});
})
.catch((e) => {
console.error("检查更新错误,请稍后再试 " + e);
});
},
onNegativeClick: () => {
message.info("您已取消更新");
},
});
};
return {
checkV,
updater,
};
};
4.调用示例
vue
<template>
<div>
{{ meg }}
<n-button type="primary" @click="updateTask">检查更新</n-button>
</div>
</template>
<script setup lang="ts">
import { message } from "@tauri-apps/plugin-dialog";
import pkg from "../../package.json";
import useUpdater from "@/hooks/updater";
import { ref } from "vue";
const meg = ref("版本检测 ");
const { checkV, updater } = useUpdater();
const state = ref(false);
const updateTask = async () => {
if (state.value) {
await updater();
} else {
let res = await checkV();
if (res) {
meg.value = "发现新版本:" + res.meg;
state.value = pkg.version !== res.version;
}
}
};
</script>
自定义系统托盘
前端方式(hooks函数)【推荐】
1.配置
添加自定义图标权限 src-tauri\Cargo.toml
toml
[dependencies]
tauri = { version = "2", features = ["tray-icon", "image-png"] }
2.封装hooks
src\hooks\tray.ts
ts
// 获取当前窗口
import { getCurrentWindow } from "@tauri-apps/api/window";
// 导入系统托盘
import { TrayIcon, TrayIconOptions, TrayIconEvent } from "@tauri-apps/api/tray";
// 托盘菜单
import { Menu } from "@tauri-apps/api/menu";
// 进程管理
import { exit } from "@tauri-apps/plugin-process";
// 定义闪烁状态
let isBlinking: boolean = false;
let blinkInterval: NodeJS.Timeout | null = null;
let trayInstance: TrayIcon | any | null = null;
let originalIcon: string | any;
/**
* 在这里你可以添加一个托盘菜单,标题,工具提示,事件处理程序等
*/
const options: TrayIconOptions = {
// icon 项目根目录/src-tauri/
icon: "icons/32x32.png",
tooltip: "zero",
menuOnLeftClick: false,
action: (event: TrayIconEvent) => {
if (
event.type === "Click" &&
event.button === "Left" &&
event.buttonState === "Down"
) {
// 显示窗口
winShowFocus();
}
},
};
/**
* 窗口置顶显示
*/
async function winShowFocus() {
try {
// 获取窗体实例
const win = getCurrentWindow();
// 检查窗口是否见,如果不可见则显示出来
if (!(await win.isVisible())) {
await win.show();
} else {
// 检查是否处于最小化状态,如果处于最小化状态则解除最小化
if (await win.isMinimized()) {
await win.unminimize();
}
// 窗口置顶
await win.setFocus();
}
} catch (error) {
console.error("Error in winShowFocus:", error);
}
}
/**
* 创建托盘菜单
*/
async function createMenu() {
try {
return await Menu.new({
// items 的显示顺序是倒过来的
items: [
{
id: "show",
text: "显示窗口",
action: () => {
winShowFocus();
},
},
{
id: "quit",
text: "退出",
action: () => {
exit(0);
},
},
],
});
} catch (error) {
console.error("Error in createMenu:", error);
return null;
}
}
/**
* 创建系统托盘
*/
export async function createTray() {
try {
const menu = await createMenu();
if (menu) {
options.menu = menu;
const tray = await TrayIcon.new(options);
trayInstance = tray;
originalIcon = options.icon; // 保存原始图标
return tray;
}
} catch (error) {
console.error("Error in createTray:", error);
}
}
/**
* 开启图标闪烁
* @param icon1 图标1路径(可选,默认原始图标)
* @param icon2 图标2路径(可选,默认alt图标)
* @param interval 闪烁间隔(默认500ms)
*/
export async function startBlinking(
icon1?: string,
icon2?: string,
interval: number = 500
) {
if (!trayInstance) {
console.error("Tray not initialized");
return;
}
// 如果正在闪烁,先停止
stopBlinking();
// 设置图标路径
const targetIcon1 = icon1 || originalIcon;
const targetIcon2 = icon2 || "icons/32x32_alt.png"; // 备用图标路径
isBlinking = true;
let currentIcon = targetIcon1;
blinkInterval = setInterval(async () => {
try {
currentIcon = currentIcon === targetIcon1 ? targetIcon2 : targetIcon1;
await trayInstance!.setIcon(currentIcon);
} catch (error) {
console.error("Blinking error:", error);
stopBlinking();
}
}, interval);
}
/**
* 停止闪烁并恢复原始图标
*/
export function stopBlinking() {
if (blinkInterval) {
clearInterval(blinkInterval);
blinkInterval = null;
isBlinking = false;
// 恢复原始图标
if (trayInstance) {
trayInstance
.setIcon(originalIcon)
.catch((error) => console.error("恢复图标失败:", error));
}
}
}
/**
* 销毁托盘(自动停止闪烁)
*/
export async function destroyTray() {
try {
stopBlinking();
if (trayInstance) {
await trayInstance.destroy();
trayInstance = null;
}
} catch (error) {
console.error("Error destroying tray:", error);
}
}
3.调用示例
结合不同场景引入 hooks 函数,调用对应方法,其中 createTray函数 可以放到 main.ts 中在系统启动时创建
ts
// 场景示例:即时通讯应用
class ChatApp {
async init() {
// 应用启动时初始化托盘
await createTray();
}
onNewMessage() {
// 收到新消息时启动红色提醒闪烁
startBlinking("icons/msg_new.png", "icons/msg_alert.png");
}
onMessageRead() {
// 用户查看消息后停止闪烁
stopBlinking();
}
async shutdown() {
// 退出时清理资源
await destroyTray();
}
}
// 场景示例:下载管理器
class DownloadManager {
onDownloadProgress() {
// 下载时使用蓝色图标呼吸灯效果
startBlinking("icons/download_active.png", "icons/download_idle.png", 1000);
}
onDownloadComplete() {
// 下载完成停止闪烁并显示完成图标
stopBlinking();
trayInstance?.setIcon("icons/download_done.png");
}
}
前后端结合方式(Rust函数)
1.配置
添加自定义图标权限 src-tauri\Cargo.toml
toml
[dependencies]
tauri = { version = "2", features = ["tray-icon", "image-png"] }
添加配置 src-tauri\tauri.conf.json 自定义图标
json
"app": {
"windows": [
],
"trayIcon": {
"iconPath": "icons/icon.ico",
"iconAsTemplate": true,
"title": "时间管理器",
"tooltip": "时间管理器"
}
},
2.Rust 封装
托盘事件定义,新建 tray.rs 文件 src-tauri\src\tray.rs
rs
use tauri::{
menu::{Menu, MenuItem, Submenu},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager, Runtime,
};
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let show_i = MenuItem::with_id(app, "show", "显示", true, None::<&str>)?;
let hide_i = MenuItem::with_id(app, "hide", "隐藏", true, None::<&str>)?;
let edit_i = MenuItem::with_id(app, "edit_file", "编辑", true, None::<&str>)?;
let new_i = MenuItem::with_id(app, "new_file", "添加", true, None::<&str>)?;
let a = Submenu::with_id_and_items(app, "File", "文章", true, &[&new_i, &edit_i])?;
// 分割线
let menu = Menu::with_items(app, &[&quit_i, &show_i, &hide_i, &a])?;
// 创建系统托盘 let _ = TrayIconBuilder::with_id("icon")
let _ = TrayIconBuilder::with_id("tray")
// 添加菜单
.menu(&menu)
// 添加托盘图标
.icon(app.default_window_icon().unwrap().clone())
.title("zero")
.tooltip("zero")
.show_menu_on_left_click(false)
// 禁用鼠标左键点击图标显示托盘菜单
// .show_menu_on_left_click(false)
// 监听事件菜单
.on_menu_event(move |app, event| match event.id.as_ref() {
"quit" => {
app.exit(0);
}
"show" => {
let window = app.get_webview_window("main").unwrap();
let _ = window.show();
}
"hide" => {
let window = app.get_webview_window("main").unwrap();
let _ = window.hide();
}
"edit_file" => {
println!("edit_file");
}
"new_file" => {
println!("new_file");
}
// Add more events here
_ => {}
})
// 监听托盘图标发出的鼠标事件
.on_tray_icon_event(|tray, event| {
// 左键点击托盘图标显示窗口
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app);
Ok(())
}
lib.rs 使用,注册函数暴露给前端调用
rs
#[cfg(desktop)]
mod tray;
// 自定义函数声明
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_log::Builder::new().build())
// 添加自定义托盘
.setup(|app| {
#[cfg(all(desktop))]
{
let handle: &tauri::AppHandle = app.handle();
tray::create_tray(handle)?;
}
Ok(())
})
// Run the app
// 注册 Rust 后端函数,暴露给前端调用
.invoke_handler(tauri::generate_handler![
greet
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
3.前端调用Rust暴露函数
vue
<template>
<div>
<button class="item" @click="flashTray(true)">开启图标闪烁</button>
<button class="item" @click="flashTray(false)">关闭图标闪烁</button>
</div>
</template>
<script setup lang="ts">
import { TrayIcon } from "@tauri-apps/api/tray";
const flashTimer = ref<Boolean | any>(false);
const flashTray = async (bool: Boolean) => {
let flag = true;
if (bool) {
TrayIcon.getById("tray").then(async (res: any) => {
clearInterval(flashTimer.value);
flashTimer.value = setInterval(() => {
if (flag) {
res.setIcon(null);
} else {
// res.setIcon(defaultIcon)
// 支持把自定义图标放在默认icons文件夹,通过如下方式设置图标
// res.setIcon('icons/msg.png')
// 支持把自定义图标放在自定义文件夹tray,需要配置tauri.conf.json参数 "bundle": {"resources": ["tray"]}
res.setIcon("tray/tray.png");
}
flag = !flag;
}, 500);
});
} else {
clearInterval(flashTimer.value);
let tray: any = await TrayIcon.getById("tray");
tray.setIcon("icons/icon.png");
}
};
</script>
窗口工具栏自定义
1. 配置
配置文件开启权限 src-tauri\capabilities\default.json
json
"permissions": [
"core:window:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-toggle-maximize",
"core:window:allow-show",
"core:window:allow-set-focus",
"core:window:allow-hide",
"core:window:allow-unminimize",
"core:window:allow-set-size",
"core:window:allow-close",
]
关闭默认窗口事件 src-tauri\tauri.conf.json
json
"app": {
"windows": [
{
"decorations": false,
}
],
},
2. 自定义实现
前端调用
vue
<script setup lang="ts">
import { getCurrentWindow } from "@tauri-apps/api/window";
const appWindow = getCurrentWindow();
onMounted(() => {
windowCustomize();
});
const windowCustomize = () => {
let minimizeEle = document.getElementById("titlebar-minimize");
minimizeEle?.addEventListener("click", () => appWindow.minimize());
let maximizeEle = document.getElementById("titlebar-maximize");
maximizeEle?.addEventListener("click", () => appWindow.toggleMaximize());
let closeEle = document.getElementById("titlebar-close");
closeEle?.addEventListener("click", () => appWindow.close());
};
</script>
<template>
<div data-tauri-drag-region class="titlebar">
<div class="titlebar-button" id="titlebar-minimize">
<img src="@/assets/svg/titlebar/mdi_window-minimize.svg" alt="minimize" />
</div>
<div class="titlebar-button" id="titlebar-maximize">
<img src="@/assets/svg/titlebar/mdi_window-maximize.svg" alt="maximize" />
</div>
<div class="titlebar-button" id="titlebar-close">
<img src="@/assets/svg/titlebar/mdi_close.svg" alt="close" />
</div>
</div>
</template>
<style scoped>
.titlebar {
height: 30px;
background: #329ea3;
user-select: none;
display: flex;
justify-content: flex-end;
position: fixed;
top: 0;
left: 0;
right: 0;
}
.titlebar-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
user-select: none;
-webkit-user-select: none;
}
.titlebar-button:hover {
background: #5bbec3;
}
</style>
webview 多窗口创建
1. 配置
配置文件开启权限 src-tauri\capabilities\default.json
json
"permissions": [
"core:webview:default",
"core:webview:allow-create-webview-window",
"core:webview:allow-create-webview",
"core:webview:allow-webview-close",
"core:webview:allow-set-webview-size",
]
2. hooks 函数封装
ts
import { nextTick } from "vue";
import {
WebviewWindow,
getCurrentWebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { emit, listen } from "@tauri-apps/api/event";
export interface WindowsProps {
label: string;
url?: string;
title: string;
minWidth: number;
minHeight: number;
width: number;
height: number;
closeWindLabel?: string;
resizable: boolean;
}
export default () => {
// 窗口事件类型
type WindowEvent = "closed" | "minimized" | "maximized" | "resized";
// 创建窗口
const createWindows = async (
args: WindowsProps = {
label: "main",
title: "主窗口",
minWidth: 800,
minHeight: 600,
width: 800,
height: 600,
resizable: true,
}
) => {
if (!(await isExist(args.label))) {
const webview = new WebviewWindow(args.label, {
title: args.title,
url: args.url,
fullscreen: false,
resizable: args.resizable,
center: true,
width: args.width,
height: args.height,
minWidth: args.minWidth,
minHeight: args.minHeight,
skipTaskbar: false,
decorations: false,
transparent: false,
titleBarStyle: "overlay",
hiddenTitle: true,
visible: false,
});
// 窗口创建成功
await webview.once("tauri://created", async () => {
webview.show();
if (args.closeWindLabel) {
const win = await WebviewWindow.getByLabel(args.closeWindLabel);
win?.close();
}
});
// 窗口创建失败
await webview.once("tauri://error", async (e) => {
console.error("Window creation error:", e);
if (args.closeWindLabel) {
await showWindow(args.closeWindLabel);
}
});
// 监听窗口事件
setupWindowListeners(webview, args.label);
return webview;
} else {
showWindow(args.label);
}
};
// 设置窗口监听器
const setupWindowListeners = (webview: WebviewWindow, label: string) => {
// 关闭请求处理
webview.listen("tauri://close-requested", async (e) => {
await emit("window-event", {
label,
event: "closed",
data: { timestamp: Date.now() },
});
console.log("label :>> ", label);
const win = await WebviewWindow.getByLabel(label);
win?.close();
// const win = label ? await WebviewWindow.getByLabel(label) : await getCurrentWebviewWindow();
// win?.close();
});
// 最小化事件
webview.listen("tauri://minimize", async (e) => {
await emit("window-event", {
label,
event: "minimized",
data: { state: true },
});
});
// 最大化事件
webview.listen("tauri://maximize", async (e) => {
await emit("window-event", {
label,
event: "maximized",
data: { state: true },
});
});
// 取消最大化
webview.listen("tauri://unmaximize", async (e) => {
await emit("window-event", {
label,
event: "maximized",
data: { state: false },
});
});
};
// 窗口间通信 - 发送消息
const sendWindowMessage = async (
targetLabel: string,
event: string,
payload: any
) => {
const targetWindow = await WebviewWindow.getByLabel(targetLabel);
if (targetWindow) {
targetWindow.emit(event, payload);
}
};
// 监听窗口消息
const onWindowMessage = (event: string, callback: (payload: any) => void) => {
return listen(event, ({ payload }) => callback(payload));
};
// 窗口控制方法
const windowControls = {
minimize: async (label?: string) => {
const win = label
? await WebviewWindow.getByLabel(label)
: await getCurrentWebviewWindow();
await win?.minimize();
},
maximize: async (label?: string) => {
const win = label
? await WebviewWindow.getByLabel(label)
: await getCurrentWebviewWindow();
await win?.maximize();
},
close: async (label?: string) => {
const win = label
? await WebviewWindow.getByLabel(label)
: await getCurrentWebviewWindow();
win?.close();
},
toggleMaximize: async (label?: string) => {
const win = label
? await WebviewWindow.getByLabel(label)
: await getCurrentWebviewWindow();
const isMaximized = await win?.isMaximized();
isMaximized ? await win?.unmaximize() : await win?.maximize();
},
};
// 获取当前窗口
const nowWindow = async () => {
const win = await getCurrentWebviewWindow();
return win;
};
// 关闭窗口
const closeWindow = async (label?: string) => {
if (label) {
const win = await WebviewWindow.getByLabel(label);
win?.close();
} else {
const win = await getCurrentWebviewWindow();
win?.close();
}
};
// 显示窗口
const showWindow = async (label: string, isCreated: boolean = false) => {
const isExistsWinds = await WebviewWindow.getByLabel(label);
if (isExistsWinds) {
nextTick().then(async () => {
// 检查是否是隐藏
const hidden = await isExistsWinds.isVisible();
if (!hidden) {
await isExistsWinds.show();
}
// 如果窗口已存在,首先检查是否最小化了
const minimized = await isExistsWinds.isMinimized();
if (minimized) {
// 如果已最小化,恢复窗口
await isExistsWinds.unminimize();
}
// 如果窗口已存在,则给它焦点,使其在最前面显示
await isExistsWinds.setFocus();
});
} else {
if (!isCreated) {
return createWindows();
}
}
};
//窗口是否存在
const isExist = async (label: string) => {
const isExistsWinds = await WebviewWindow.getByLabel(label);
if (isExistsWinds) {
return true;
} else {
return false;
}
};
return {
createWindows,
sendWindowMessage,
onWindowMessage,
...windowControls,
nowWindow,
showWindow,
isExist,
closeWindow,
};
};
3. 调用
window 父级
vue
<template>
<div class="window-controls">
<n-button @click="minimizeWindow">最小化</n-button>
<n-button @click="toggleMaximizeWindow">{{
isMaximized ? "恢复" : "最大化"
}}</n-button>
<n-button @click="maximizeWindow">最大化</n-button>
<n-button @click="closeWindow">关闭</n-button>
<n-button @click="openChildWindow">打开子窗口</n-button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import useWindowManager from "@/hooks/windowManager";
const {
createWindows,
minimize,
maximize,
toggleMaximize,
close,
onWindowMessage,
} = useWindowManager();
const isMaximized = ref(false);
const openChildWindow = () => {
createWindows({
label: "child",
title: "子窗口",
url: "/child",
minWidth: 400,
minHeight: 300,
width: 600,
height: 400,
resizable: true,
});
};
// 监听子窗口消息
onWindowMessage("child-message", (payload) => {
console.log("Received from child:", payload);
});
// 窗口控制方法
const minimizeWindow = async () => {
await minimize("child"); // 最小化窗口
};
const maximizeWindow = async () => {
await maximize("child"); // 最大化窗口
};
const toggleMaximizeWindow = async () => {
await toggleMaximize("child"); // 切换最大化/还原
};
const closeWindow = async () => {
await close("child"); // 关闭窗口
};
</script>
childView.vue 子组件
vue
<template>
<div class="child">
<h1>Child Window</h1>
<n-button @click="sendToMain">Send Message to Main</n-button>
<n-button @click="close">Close</n-button>
</div>
</template>
<script setup lang="ts">
import useWindowManager from "@/hooks/windowManager";
const { sendWindowMessage, close } = useWindowManager();
// const {close} = windowControls
// 向主窗口发送消息
const sendToMain = () => {
sendWindowMessage("main", "child-message", {
timestamp: Date.now(),
content: "Hello from child!",
});
};
</script>
系统通知 notification
1.安装依赖
安装后会自动写入到 package.json 文件和 Cargo.toml 文件,支持 前端 JS 调用和 后端 Rust 调用
sh
pnpm tauri add notification
2.配置
tauri 权限配置 src-tauri\capabilities\default.json
json
{
"permissions": [
"notification:default",
"notification:allow-get-active",
"notification:allow-is-permission-granted"
]
}
3.封装hooks
src\hooks\notification.ts
ts
import {
isPermissionGranted,
requestPermission,
sendNotification,
} from "@tauri-apps/plugin-notification";
export default () => {
const checkPermission = async () => {
const permission = await isPermissionGranted();
if (!permission) {
const permission = await requestPermission();
return permission === "granted";
} else {
return true;
}
};
const sendMessage = async (title: string, message: string) => {
const permission = await checkPermission();
if (permission) {
await sendNotification({
title,
body: message,
// 这里演示,你可以作为参数传入 win11 测试没效果
attachments: [
{
id: "image-1",
url: "F:\\tv_task\\public\\tauri.png",
},
],
});
}
};
return { sendMessage };
};
4.调用示例
vue
<template>
<div>
<n-button @click="sendNot">notification 通知</n-button>
</div>
</template>
<script setup lang="ts">
import useNotification from "@/hooks/notification";
const { sendMessage } = useNotification();
const sendNot = async () => {
await sendMessage("提示", "您当前有代办的任务需要处理!");
};
</script>
日志
1.安装依赖
安装后会自动写入到 package.json 文件和 Cargo.toml 文件,支持 前端 JS 调用和 后端 Rust 调用
sh
pnpm tauri add log
2.配置
tauri 权限配置 src-tauri\capabilities\default.json
json
{
"permissions": ["log:default"]
}
3.封装hooks
src\hooks\log.ts
ts
import {
trace,
info,
debug,
error,
attachConsole,
} from "@tauri-apps/plugin-log";
// 启用 TargetKind::Webview 后,这个函数将把日志打印到浏览器控制台
const detach = await attachConsole();
export default () => {
// 将浏览器控制台与日志流分离
detach();
return {
debug,
trace,
info,
error,
};
};
4.调用示例
vue
<template>
<div>
<h1>控制台效果</h1>
<div class="console">
<div
class="console-line"
v-for="(line, index) in consoleLines"
:key="index"
:class="{
'animate__animated animate__fadeIn':
index === consoleLines.length - 1,
}"
>
{{ line }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import TauriLog from "@/hooks/log";
import { ref } from "vue";
const { info } = TauriLog();
info("我来了");
const consoleLines = ref([
"Welcome to the console!",
"This is a cool console interface.",
"You can type commands here.",
"Press Enter to execute.",
]);
</script>
程序启动监听
hooks 函数封装
src\hooks\start.ts
ts
import { invoke } from "@tauri-apps/api/core";
function sleep(seconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
async function setup() {
console.log("前端应用启动..");
await sleep(3);
console.log("前端应用启动完成");
// 调用后端应用
invoke("set_complete", { task: "frontend" });
}
export default () => {
// Effectively a JavaScript main function
window.addEventListener("DOMContentLoaded", () => {
setup();
});
};
调用日志打印
src\main.ts
ts
import start from "@/hooks/start";
start();
Http 封装
axios 请求,会在打包后存在跨域问题,所以使用 tauri 插件,进行http封装
1.安装依赖
安装后会自动写入到 package.json 文件和 Cargo.toml 文件,支持 前端 JS 调用和 后端 Rust 调用
sh
pnpm tauri add http
2.配置
tauri 权限配置 src-tauri\capabilities\default.json
json
{
"permissions": [
{
"identifier": "http:default",
"allow": [
{
"url": "http://**"
},
{
"url": "https://**"
},
{
"url": "http://*:*"
},
{
"url": "https://*:*"
}
]
}
]
}
3.封装hooks
src\utils\exception.ts
ts
export enum ErrorType {
Network = "NETWORK_ERROR",
Authentication = "AUTH_ERROR",
Validation = "VALIDATION_ERROR",
Server = "SERVER_ERROR",
Client = "CLIENT_ERROR",
Unknown = "UNKNOWN_ERROR",
}
export interface ErrorDetails {
type: ErrorType;
code?: number;
details?: Record<string, any>;
}
export class AppException extends Error {
public readonly type: ErrorType;
public readonly code?: number;
public readonly details?: Record<string, any>;
constructor(message: string, errorDetails?: Partial<ErrorDetails>) {
super(message);
this.name = "AppException";
this.type = errorDetails?.type || ErrorType.Unknown;
this.code = errorDetails?.code;
this.details = errorDetails?.details;
// Show error message to user if window.$message is available
if (window.$message) {
window.$message.error(message);
}
}
public toJSON() {
return {
name: this.name,
message: this.message,
type: this.type,
code: this.code,
details: this.details,
};
}
}
src\utils\http.ts
ts
import { fetch } from "@tauri-apps/plugin-http";
import { AppException, ErrorType } from "./exception";
/**
* @description 请求参数
* @property {"GET"|"POST"|"PUT"|"DELETE"} method 请求方法
* @property {Record<string, string>} [headers] 请求头
* @property {Record<string, any>} [query] 请求参数
* @property {any} [body] 请求体
* @property {boolean} [isBlob] 是否为Blob
* @property {boolean} [noRetry] 是否禁用重试
* @return HttpParams
*/
export type HttpParams = {
method: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>;
query?: Record<string, any>;
body?: any;
isBlob?: boolean;
retry?: RetryOptions; // 新增重试选项
noRetry?: boolean; // 新增禁用重试选项
};
/**
* @description 重试选项
*/
export type RetryOptions = {
retries?: number;
retryDelay?: (attempt: number) => number;
retryOn?: number[];
};
/**
* @description 自定义错误类,用于标识需要重试的 HTTP 错误
*/
class FetchRetryError extends Error {
status: number;
type: ErrorType;
constructor(message: string, status: number) {
super(message);
this.status = status;
this.name = "FetchRetryError";
this.type = status >= 500 ? ErrorType.Server : ErrorType.Network;
}
}
/**
* @description 等待指定的毫秒数
* @param {number} ms 毫秒数
* @returns {Promise<void>}
*/
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* @description 判断是否应进行下一次重试
* @returns {boolean} 是否继续重试
*/
function shouldRetry(
attempt: number,
maxRetries: number,
abort?: AbortController
): boolean {
return attempt + 1 < maxRetries && !abort?.signal.aborted;
}
/**
* @description HTTP 请求实现
* @template T
* @param {string} url 请求地址
* @param {HttpParams} options 请求参数
* @param {boolean} [fullResponse=false] 是否返回完整响应
* @param {AbortController} abort 中断器
* @returns {Promise<T | { data: T; resp: Response }>} 请求结果
*/
async function Http<T = any>(
url: string,
options: HttpParams,
fullResponse: boolean = false,
abort?: AbortController
): Promise<{ data: T; resp: Response } | T> {
// 打印请求信息
console.log(`🚀 发起请求 → ${options.method} ${url}`, {
body: options.body,
query: options.query,
});
// 默认重试配置
const defaultRetryOptions: RetryOptions = {
retries: options.noRetry ? 0 : 3, // 如果设置了noRetry,则不进行重试
retryDelay: (attempt) => Math.pow(2, attempt) * 1000, // 指数退避策略
retryOn: [500, 502, 503, 504],
};
// 合并默认重试配置与用户传入的重试配置
const retryOptions: RetryOptions = {
...defaultRetryOptions,
...options.retry,
};
const { retries = 3, retryDelay, retryOn } = retryOptions;
// 获取token和指纹
const token = localStorage.getItem("TOKEN");
//const fingerprint = await getEnhancedFingerprint()
// 构建请求头
const httpHeaders = new Headers(options.headers || {});
// 设置Content-Type
if (!httpHeaders.has("Content-Type") && !(options.body instanceof FormData)) {
httpHeaders.set("Content-Type", "application/json");
}
// 设置Authorization
if (token) {
httpHeaders.set("Authorization", `Bearer ${token}`);
}
// 设置浏览器指纹
//if (fingerprint) {
//httpHeaders.set('X-Device-Fingerprint', fingerprint)
//}
// 构建 fetch 请求选项
const fetchOptions: RequestInit = {
method: options.method,
headers: httpHeaders,
signal: abort?.signal,
};
// 获取代理设置
// const proxySettings = JSON.parse(localStorage.getItem('proxySettings') || '{}')
// 如果设置了代理,添加代理配置 (BETA)
// if (proxySettings.type && proxySettings.ip && proxySettings.port) {
// // 使用 Rust 后端的代理客户端
// fetchOptions.proxy = {
// url: `${proxySettings.type}://${proxySettings.ip}:${proxySettings.port}`
// }
// }
// 判断是否需要添加请求体
if (options.body) {
if (
!(
options.body instanceof FormData ||
options.body instanceof URLSearchParams
)
) {
fetchOptions.body = JSON.stringify(options.body);
} else {
fetchOptions.body = options.body; // 如果是 FormData 或 URLSearchParams 直接使用
}
}
// 添加查询参数
if (options.query) {
const queryString = new URLSearchParams(options.query).toString();
url += `?${queryString}`;
}
// 拼接 API 基础路径
//url = `${import.meta.env.VITE_SERVICE_URL}${url}`
// 定义重试函数
async function attemptFetch(
currentAttempt: number
): Promise<{ data: T; resp: Response } | T> {
try {
const response = await fetch(url, fetchOptions);
// 若响应不 OK 并且状态码属于需重试列表,则抛出 FetchRetryError
if (!response.ok) {
const errorType = getErrorType(response.status);
if (!retryOn || retryOn.includes(response.status)) {
throw new FetchRetryError(
`HTTP error! status: ${response.status}`,
response.status
);
}
// 如果是非重试状态码,则抛出带有适当错误类型的 AppException
throw new AppException(`HTTP error! status: ${response.status}`, {
type: errorType,
code: response.status,
details: { url, method: options.method },
});
}
// 解析响应数据
const responseData = options.isBlob
? await response.arrayBuffer()
: await response.json();
// 打印响应结果
console.log(`✅ 请求成功 → ${options.method} ${url}`, {
status: response.status,
data: responseData,
});
// 若有success === false,需要重试
if (responseData && responseData.success === false) {
const errorMessage = responseData.errMsg || "服务器返回错误";
window.$message?.error?.(errorMessage);
throw new AppException(errorMessage, {
type: ErrorType.Server,
code: response.status,
details: responseData,
});
}
// 若请求成功且没有业务错误
if (fullResponse) {
return { data: responseData, resp: response };
}
return responseData;
} catch (error) {
console.error(`尝试 ${currentAttempt + 1} 失败的 →`, error);
// 检查是否仍需重试
if (!shouldRetry(currentAttempt, retries, abort)) {
console.error(
`Max retries reached or aborted. Request failed → ${url}`
);
if (error instanceof FetchRetryError) {
window.$message?.error?.(error.message || "网络请求失败");
throw new AppException(error.message, {
type: error.type,
code: error.status,
details: { url, attempts: currentAttempt + 1 },
});
}
if (error instanceof AppException) {
window.$message?.error?.(error.message || "请求出错");
throw error;
}
const errorMessage = String(error) || "未知错误";
window.$message?.error?.(errorMessage);
throw new AppException(errorMessage, {
type: ErrorType.Unknown,
details: { url, attempts: currentAttempt + 1 },
});
}
// 若需继续重试
const delayMs = retryDelay ? retryDelay(currentAttempt) : 1000;
console.warn(
`Retrying request → ${url} (next attempt: ${currentAttempt + 2}, waiting ${delayMs}ms)`
);
await wait(delayMs);
return attemptFetch(currentAttempt + 1);
}
}
// 辅助函数:根据HTTP状态码确定错误类型
function getErrorType(status: number): ErrorType {
if (status >= 500) return ErrorType.Server;
if (status === 401 || status === 403) return ErrorType.Authentication;
if (status === 400 || status === 422) return ErrorType.Validation;
if (status >= 400) return ErrorType.Client;
return ErrorType.Network;
}
// 第一次执行,attempt=0
return attemptFetch(0);
}
export default Http;
src\utils\request.ts
ts
import Http, { HttpParams } from "./http.ts";
import { ServiceResponse } from "@/enums/types.ts";
const { VITE_SERVICE_URL } = import.meta.env;
const prefix = VITE_SERVICE_URL;
function getToken() {
let tempToken = "";
return {
get() {
if (tempToken) return tempToken;
const token = localStorage.getItem("TOKEN");
if (token) {
tempToken = token;
}
return tempToken;
},
clear() {
tempToken = "";
},
};
}
export const computedToken = getToken();
// fetch 请求响应拦截器
const responseInterceptor = async <T>(
url: string,
method: "GET" | "POST" | "PUT" | "DELETE",
query: any,
body: any,
abort?: AbortController
): Promise<T> => {
let httpParams: HttpParams = {
method,
};
if (method === "GET") {
httpParams = {
...httpParams,
query,
};
} else {
url = `${prefix}${url}?${new URLSearchParams(query).toString()}`;
httpParams = {
...httpParams,
body,
};
}
try {
const data = await Http(url, httpParams, true, abort);
const serviceData = (await data.data) as ServiceResponse;
//检查服务端返回是否成功,并且中断请求
if (!serviceData.success) {
window.$message.error(serviceData.errMsg);
return Promise.reject(`http error: ${serviceData.errMsg}`);
}
return Promise.resolve(serviceData.result);
} catch (err) {
return Promise.reject(`http error: ${err}`);
}
};
const get = async <T>(
url: string,
query: T,
abort?: AbortController
): Promise<T> => {
return responseInterceptor(url, "GET", query, {}, abort);
};
const post = async <T>(
url: string,
params: any,
abort?: AbortController
): Promise<T> => {
return responseInterceptor(url, "POST", {}, params, abort);
};
const put = async <T>(
url: string,
params: any,
abort?: AbortController
): Promise<T> => {
return responseInterceptor(url, "PUT", {}, params, abort);
};
const del = async <T>(
url: string,
params: any,
abort?: AbortController
): Promise<T> => {
return responseInterceptor(url, "DELETE", {}, params, abort);
};
export default {
get,
post,
put,
delete: del,
};
src\api\manage.ts
ts
import request from "@/utils/request";
export const getAction = <T>(
url: string,
params?: any,
abort?: AbortController
) => request.get<T>(url, params, abort);
export const postAction = <T>(
url: string,
params?: any,
abort?: AbortController
) => request.post<T>(url, params, abort);
export const putAction = <T>(
url: string,
params?: any,
abort?: AbortController
) => request.put<T>(url, params, abort);
export const deleteAction = <T>(
url: string,
params?: any,
abort?: AbortController
) => request.delete<T>(url, params, abort);
4.调用示例
vue
<template>
<div>
<n-button @click="postTest">测试POST</n-button>
</div>
</template>
<script setup lang="ts">
import { postAction } from "@/api/manage";
const postTest = () => {
let url = `/sys/login`;
postAction(url, {
username: "admin",
password: "tick20140513",
}).then((res) => {
text.value = res.token;
});
};
</script>