tauri2+vue+vite实现基于webview视图渲染的桌面端开发

创建应用

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>
相关推荐
狗哥哥2 小时前
Vue 3 统一面包屑导航系统:从配置地狱到单一数据源
前端·vue.js·架构
鱼鱼块3 小时前
从后端拼模板到 Vue 响应式:前端界面的三次进化
前端·vue.js·面试
UIUV3 小时前
JavaScript内存管理与闭包原理:从底层到实践的全面解析
前端·javascript·代码规范
无限大63 小时前
为什么计算机要使用二进制?——从算盘到晶体管的数字革命
前端·后端·架构
良木林3 小时前
字节前端高频面试题试析
前端
一 乐3 小时前
家政管理|基于SprinBoot+vue的家政服务管理平台(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot
fruge3 小时前
图片优化终极指南:WebP/AVIF 选型、懒加载与 CDN 配置
前端
掘金一周3 小时前
数据标注平台正式上线啦! 标注赚现金,低门槛真收益 | 掘金一周 12.10
前端·人工智能·后端
Macbethad4 小时前
工业触摸屏技术指南:选型、难点与实战解决方案
服务器·前端·数据库