tauri v2 开源项目学习(一)

前言:

tauri2编程,前端部分和electron差不多,框架部分差别大,资料少,官网乱,AI又骗我
所以在gitee上,寻找tauri v2开源项目,
通过记录框架部分与rust部分的写法,对照确定编程方式
提示:不要在VSCode里自动运行Cargo,在powershell里运行Cargo build,不会卡住


1. tauri-desktop

https://gitee.com/MapleKing/tauri-desktop
展示了自制标题栏的做法

1.1 tauri.conf.json
{
  ...
  "app": {
    "windows": [
      {
        "title": "tauri-desktop",
        "minWidth": 800,
        "minHeight": 600,
        "decorations": false
      }
    ],
    "security": {
      "csp": null
    }
  },
  "bundle": {
	...
    "resources": {
      "config/*": "config/"
    }
  }
}

  • decorations 的作用:

true(默认值):
窗口会显示操作系统原生的标题栏、边框、最小化/最大化/关闭按钮等装饰。
例如:Windows 上的标准标题栏,macOS 顶部的红黄绿按钮等。
false:
隐藏原生装饰,窗口将变成一个无边框的“纯内容”区域。
通常用于自定义标题栏(用 HTML/CSS 实现)。
需要自行处理窗口拖动、最小化、关闭等功能(可通过 Tauri API 实现)。

1.2 TopPlace.vue

提供了最小,最大,关闭的3个按钮的处理函数

import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
appWindow.listen("tauri://resize", async () => {
  appWindow.isMaximized().then((res)=>{
    isMax.value = res;
  })
});
function toggleMaximize (){
  appWindow.toggleMaximize();
}
function close(){
  appWindow.close();
}
function minimize(){
  appWindow.minimize();
}
1.3 frame.ts

前端操作文件的一个案例

import { resolveResource } from '@tauri-apps/api/path';
import { readTextFile,writeTextFile} from '@tauri-apps/plugin-fs';
import { useGlobal } from '@/stores/global';
import CONFIG from '@/frame/config';
const initMenus = async function (){
  const resourcePath = await resolveResource(CONFIG.CONFIG_PATH);
  useGlobal().menus = JSON.parse(await readTextFile(resourcePath)); 
}
const initUserConfig = async function (){
    const resourcePath = await resolveResource(CONFIG.USER_CONFIG_PATH);
    //获取用户配置
    //获取用户本地配置的主题
    useGlobal().userConfig = JSON.parse(await readTextFile(resourcePath)); 
    useGlobal().theme = useGlobal().userConfig.theme;
}



const initFrame = async function (){
    return new Promise(async (resolve) => {
        await initMenus();
        await initUserConfig();
        resolve(true);
    })
}
const updateUserConfig = async function (){
    const userConfig = useGlobal().userConfig; // 获取用户配置
    const resourcePath = await resolveResource(CONFIG.USER_CONFIG_PATH);
    await writeTextFile(resourcePath, JSON.stringify(userConfig)); // 将用户配置写入文件
}

export { 
    initFrame,
    updateUserConfig
};
1.4 Cargo.toml

Cargo.toml加加入tauri-plugin-fs,关于tauri-plugin-shell并未发现作用

...
[lib]
name = "tauri_app_win_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2.0.0", features = [] }

[dependencies]
tauri = { version = "2.0.0", features = [] }
tauri-plugin-shell = "2.0.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-fs = "2"
1.5 lib.rs

初始化tauri_plugin_fs

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
1.6 package.json
{
  "name": "tauri-desktop",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "tauri": "tauri"
  },
  "dependencies": {
    "@tauri-apps/api": ">=2.0.0",
    "@tauri-apps/plugin-fs": "~2.0.0",
    "@tauri-apps/plugin-shell": ">=2.0.0",
    "pinia": "^2.2.4",
    "vue": "^3.3.4",
    "vue-router": "^4.4.5"
  },
  "devDependencies": {
    "@tauri-apps/cli": ">=2.0.0",
    "@types/node": "^22.7.4",
    "@vitejs/plugin-vue": "^5.0.5",
    "naive-ui": "^2.40.1",
    "typescript": "^5.2.2",
    "vfonts": "^0.0.3",
    "vite": "^5.3.1",
    "vue-tsc": "^2.0.22"
  }
}
1.7 capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": [
    "main"
  ],
  "permissions": [
    "core:default",
    "shell:allow-open",
    "core:window:default",
    "core:window:allow-close",
    "core:window:allow-toggle-maximize",
    "core:window:allow-minimize",
    "core:window:allow-is-maximized",
    "core:window:allow-start-dragging",
    "fs:allow-read-text-file",
    "fs:allow-resource-read-recursive",
    "fs:allow-resource-write-recursive",
    "fs:allow-resource-write",
    "fs:allow-resource-read",
    "fs:default",
    "fs:allow-create"
  ]
}

2. tauri-latest-stables-study

https://gitee.com/smartxh/tauri-latest-stables-study
简单登录界面测试,记录一些框架的写法

2.1 tauri.conf.json
  • withGlobalTauri的作用:

类型: boolean
默认值: false
自动在 window.TAURI 上挂载 Tauri 的 JavaScript API,前端可以直接通过 window.TAURI.tauri 或 window.TAURI.window 等调用 Tauri 功能。
true:
适用于传统网页开发或需要全局访问 Tauri API 的场景。
false(默认):
不自动注入全局 TAURI 对象,前端需通过 @tauri-apps/api 的 ES 模块导入方式调用 API(推荐)。
更符合现代模块化开发规范,避免全局变量污染。

此处并未这样使用

{
	"app": {
		"withGlobalTauri": true,
		"windows": [],
		"security": {
			"csp": null,
			"capabilities": ["main-capability"]
		}
	},
	"bundle": {
		"copyright": "Huan",
		"licenseFile": "./copyright/License.txt",
		...
	}
}
2.2 \capabilities\default.json

提供了main-capability的内容,和permissions的复杂的写法

{
	"$schema": "../gen/schemas/desktop-schema.json",
	"identifier": "main-capability",
	"description": "Capability for the main window",
	"platforms": ["macOS", "windows", "linux"],
	"windows": ["*"],
	"permissions": [
		"core:default",
		"opener:default",
		"core:path:default",
		"core:event:default",
		"core:window:default",
		"core:app:default",
		"core:resources:default",
		"core:menu:default",
		"core:tray:default",
		"core:window:allow-set-title",
		{
			"identifier": "fs:write-files",
			"allow": [{
				"path": "**"
			}]
		},
		{
			"identifier": "fs:allow-mkdir",
			"allow": [{
				"path": "**"
			}]
		},
		{
			"identifier": "fs:read-dirs",
			"allow": [{
				"path": "**"
			}]
		},
		{
			"identifier": "fs:read-files",
			"allow": [{
				"path": "**"
			}]
		},
		{
			"identifier": "fs:allow-copy-file",
			"allow": [{
				"path": "**"
			}]
		},
		{
			"identifier": "fs:allow-read-text-file",
			"allow": [{
				"path": "**"
			}]
		}
	]
}
2.3 tauri.windows.conf.json
  • skipTaskbar的作用:

类型: boolean
默认值: false
作用:
true: 窗口不会在操作系统的任务栏(Windows/Linux)或 Dock(macOS)中显示图标。
false: 窗口会正常出现在任务栏/Dock 中(默认行为)。
适用场景:
登录窗口、悬浮小工具、通知窗口等辅助性窗口,不希望占用任务栏空间时使用。
通常与 resizable: false 和 decorations: false 结合,实现简洁的弹出式界面。
示例效果:
用户按主窗口时,登录窗口不会在任务栏生成额外图标,避免任务栏拥挤。

  • transparent的作用:

类型: boolean
默认值: false
作用:
true: 窗口背景完全透明,仅显示内容(需前端 CSS 配合,如设置 background: transparent)。
false: 窗口背景为不透明(默认行为)。

这里使用了多窗口,通过url与路由绑定一起,这样是否能前期缓存,没有设定url是否还会有作用,需要进一步测试

"app": {
		"withGlobalTauri": true,
		"windows": [
			{
				"title": "登录",
				"label": "login",
				"url": "/login",
				"fullscreen": false,
				"resizable": false,
				"center": true,
				"width": 320,
				"height": 448,
				"skipTaskbar": true,
				"transparent": true,
				"decorations": false
			},
			{
				"title": "注册",
				"label": "register",
				"url": "/register",
				"fullscreen": false,
				"resizable": false,
				"center": true,
				"width": 320,
				"height": 448,
				"skipTaskbar": true,
				"transparent": true,
				"decorations": false
			},
			{
				"title": "变量生成",
				"label": "variableGenerate",
				"url": "/",
				"fullscreen": false,
				"resizable": false,
				"center": true,
				"width": 320,
				"height": 448,
				"skipTaskbar": false,
				"transparent": true,
				"decorations": false
			}
		],
		"security": {
			"csp": null
		}
	}
2.4 lib.rs的写法
// 导入模块
mod commands;

// 如果是移动平台,将此函数标记应用入口
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    // 创建构建器实例
    tauri::Builder::default()
        // 添加插件
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_fs::init())
        // 添加webview可访问的函数
        .invoke_handler(tauri::generate_handler![
            commands::login_api,
            commands::register_api
        ])
        // 运行应用
        .run(tauri::generate_context!())
        // 捕获错误
        .expect("error while running tauri application");
}

2.5 command.rs的写法

通过这样的写法,分离业务逻辑,简单明了

#[tauri::command]
pub fn login_api(account: String, password: String) -> String {
    if account == "huan" && password == "123456" {
        "login_success".to_string()
    } else {
        "login_fail".to_string()
    }
}

#[tauri::command]
pub fn register_api(username: String, email: String, password: String) -> String {
    if username.len() > 3 && email.contains('@') && password.len() > 6 {
        "register_success".to_string()
    } else {
        "register_fail".to_string()
    }
}



3. tauri 串口工具

https://gitee.com/loock/tauri-serial-tool
UDP的应用案例

3.1 tauri.conf.json

没有重要内容

3.2 cargo.toml

安装了tauri-plugin-udp与tauri-plugin-network
tauri-plugin官网上有发布一些,如果去github搜素tauri-plugin,会有更多的插件支持

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [ "devtools"] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
socket2 = "0.4"
pnet = "0.28"
tauri-plugin-udp = "0.1.1"
tauri-plugin-network = "2.0.4"

https://github.com/kuyoonjo/tauri-plugin-udp
看到upd是如何应用的

3.2 default.json
{
  "windows": ["main"],
  "permissions": [
    "core:default",
    "udp:default",
    "shell:allow-open",
    "network:allow-all", 
    "network:allow-get-interfaces"
  ]
}

3.3 App.tsx

提供了udp的操作

// 引入React Hooks和其他依赖
import { useState, useEffect, useRef } from "react";
import { Input, Button, Card, Modal, Checkbox, Select } from "antd"; // Ant Design 组件
import { bind, unbind, send } from "@kuyoonjo/tauri-plugin-udp"; // UDP 插件方法
import { listen } from "@tauri-apps/api/event"; // 事件监听
import HexForm, { defaultAutoData } from "./AutoRepeat"; // 自定义表单组件与默认自动回复数据
import HexInput from "./HexInput"; // 十六进制输入组件
import dayjs from "dayjs";
import { getInterfaces } from "tauri-plugin-network-api";
import "./App.css";

function getLocalIP() {
  return getInterfaces().then((ifaces) => {
    const ips: string[] = [];
    ifaces.forEach((item) => {
      item.v4_addrs?.forEach?.((ip4) => {
        ips.push(ip4.ip);
      });
    });
    return ips;
  });
}

// 将字节数组转换为十六进制字符串
function bytesToHex(bytes: number[]) {
  return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(" ");
}

// 将十六进制字符串转换为字节数组
function hexToBytes(hexString: string) {
  const bytes = hexString.replace(/\s+/g, "").match(/.{1,2}/g); // 清理非十六进制字符,并按每两位分组
  return bytes ? bytes.map((byte) => parseInt(byte, 16)) : []; // 转换为整数数组
}

interface IMsg {
  type: "in" | "out";
  time: string;
  value: string;
}

function saveArrayAsTxtFile(array: IMsg[], filename = "output.txt") {
  // 将数组转换为字符串,每个元素占一行
  const content = array
    ?.map((item) => {
      return `${item.time} ${item.type} : ${item.value}`;
    })
    .join("\n");

  // 创建一个 Blob 对象
  const blob = new Blob([content], { type: "text/plain" });

  // 创建一个下载链接
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = filename;

  // 触发点击事件来下载文件
  document.body.appendChild(link);
  link.click();

  // 清理
  document.body.removeChild(link);
  URL.revokeObjectURL(link.href);
}

// 发送十六进制数据
const sendHex = (
  destIP: string,
  destPort: number,
  hexStr: string,
  id: string
) => {
  const bytes = hexToBytes(hexStr); // 将十六进制字符串转换为字节数组
  return send(id, `${destIP}:${destPort}`, bytes).catch((err) => {
    console.error("%c Line:63 ", "color:#7f2b82", err);
    alert(`消息发送失败: ${err}`); // 显示发送失败信息
  }); // 发送数据
};

// 获取自动回复配置
const getAutoReport = (msg: string) => {
  return (window as any).list?.find?.((item: any) => item.hex1 === msg); // 查找匹配的消息
};

function customRandomString(
  length = 8,
  charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
) {
  let result = "";
  for (let i = 0; i < length; i++) {
    result += charset.charAt(Math.floor(Math.random() * charset.length));
  }
  return result;
}

// 主应用组件
const App = () => {
  const idRef = useRef(customRandomString());
  const [ipOptions, setIps] = useState<{ label: string; value: string }[]>([
    { label: "127.0.0.1", value: "127.0.0.1" },
  ]);
  const [_, setLocalIP] = useState("127.0.0.1"); // 本地IP地址
  const [type, setType] = useState(0); // 当前状态,0:未启动, 1:已启动
  const [localPort, setLocalPort] = useState(8080); // 本地端口号
  const [destIP, setDestIP] = useState("127.0.0.1"); // 目标IP地址
  const [destPort, setDestPort] = useState(8080); // 目标端口号

  const [msg, setMsg] = useState<IMsg[]>([]); //消息列表
  const [message, setMessages] = useState<string>(""); // 待发送的消息
  const listenRef = useRef<any>(); // 保存监听函数引用
  const [isModalOpen, setIsModalOpen] = useState(false); // 控制模态框显示状态
  const timteIntervalFlag = useRef<{ flag: any; time: number }>({
    flag: null,
    time: 1000,
  }); // 定时

  const addMsg = (type: "in" | "out", hex: string) => {
    setMsg((m) => [
      { type, time: dayjs().format("HH:mm:ss.SSS"), value: hex },
      ...m,
    ]);
  };
  // 组件挂载或卸载时执行
  useEffect(() => {
    (window as any).list = defaultAutoData(); // 初始化自动回复列表

    getLocalIP().then((ips) => {
      setIps(ips?.map((v) => ({ label: v, value: v })));
    });
    // 监听UDP消息
    listen("plugin://udp", (x: any) => {
      const hex = bytesToHex(x.payload.data); // 将接收到的数据转换为十六进制字符串
      const auto = getAutoReport(hex); // 查找自动回复配置
      if (auto) {
        setTimeout(() => {
          addMsg("out", auto.hex2);
          sendHex(destIP, destPort, auto.hex2, idRef.current); // 如果有匹配的自动回复,则发送回复
        }, 100);
      }

      addMsg("in", hex);
    }).then((fn) => {
      listenRef.current = fn; // 保存监听函数引用以便后续移除
    });

    // 组件卸载时清理监听
    return () => {
      listenRef.current?.();
    };
  }, [destIP, destPort]);

  // 启动UDP服务器
  const handleStartServer = async () => {
    try {
      await bind(idRef.current, `0.0.0.0:${localPort}`); // 绑定UDP服务器到指定端口
      setType(1); // 更新状态为已启动
    } catch (error) {
      console.error(error);
      alert(`UDP 服务启动失败: ${error}`); // 显示错误信息
    }
  };

  // 关闭UDP服务器
  const handleCloseServer = async () => {
    unbind(idRef.current); // 解绑UDP服务器
    setType(0); // 更新状态为未启动
  };

  // 发送消息
  const handleSendMessage = async () => {
    if (!message) return; // 如果没有消息则不发送
    try {
      addMsg("out", message);
      sendHex(destIP, destPort, message, idRef.current); // 发送十六进制消息
    } catch (error) {
      console.error(error);
      alert(`消息发送失败: ${error}`); // 显示发送失败信息
    }
  };

  // 返回React元素
  return (
    <div>
      {/* UDP连接信息卡片 */}
      <Card title="UDP连接信息">
        <table>
          <tbody>
            <tr>
              <td>本机IP端口:</td>
              <td>
                <Select
                  disabled={!!type}
                  options={ipOptions}
                  onChange={(v) => {
                    setLocalIP(v);
                  }}
                  style={{ minWidth: "200px" }}
                ></Select><Input
                  style={{ display: "inline-block", width: 80 }}
                  type="number"
                  disabled={!!type}
                  value={localPort}
                  onChange={(e) => setLocalPort(Number(e.target.value))}
                />
              </td>
            </tr>
            <tr>
              <td>远端IP端口:</td>
              <td>
                <Input
                  style={{ display: "inline-block", width: 150 }}
                  type="ip"
                  value={destIP}
                  onChange={(e) => setDestIP(e.target.value)}
                /><Input
                  style={{ display: "inline-block", width: 80 }}
                  type="number"
                  value={destPort}
                  onChange={(e) => setDestPort(Number(e.target.value))}
                />
              </td>
            </tr>
          </tbody>
        </table>
        {type === 0 ? (
          <Button type="primary" onClick={handleStartServer}>
            打开
          </Button>
        ) : (
          <Button type="primary" onClick={handleCloseServer}>
            关闭
          </Button>
        )}
      </Card>

      {/* 数据卡片 */}
      <Card
        title="数据"
        style={{ marginTop: "10px" }}
        extra={
          <>
            <Button
              onClick={() => {
                setIsModalOpen(true);
              }}
            >
              自动回复
            </Button>

            <Button
              onClick={() => {
                saveArrayAsTxtFile(msg, `${new Date().getTime()}.txt`);
              }}
              style={{ marginLeft: "8px" }}
            >
              保存数据
            </Button>

            <Button
              onClick={() => {
                setMsg([]); // 清空接收消息
              }}
              style={{ marginLeft: "8px" }}
            >
              清空数据
            </Button>
          </>
        }
      >
        <div>
          <div
            style={{
              padding: "20px",
              height: "300px",
              overflowY: "auto",
              border: "#ccc solid 1px",
              borderRadius: "8px",
            }}
          >
            {msg?.map?.((item) => (
              <div key={`${item.time}${item.value}`}>
                <span>{item.time}</span>{" "}
                <span className={item.type}>{item.value}</span>
              </div>
            ))}{" "}
            {/* 显示接收消息 */}
          </div>
          <div style={{ display: "flex", marginTop: "20px" }}>
            <HexInput value={message} onChange={(v: any) => setMessages(v)} />{" "}
            {/* 十六进制输入框 */}
            <Button
              style={{ marginLeft: "8px" }}
              type="primary"
              onClick={handleSendMessage}
            >
              发送
            </Button>{" "}
            {/* 发送按钮 */}
          </div>
          <div
            style={{
              display: "flex",
              justifyContent: "end",
              alignItems: "center",
            }}
          >
            <Checkbox
              onChange={(e) => {
                console.log(e, timteIntervalFlag.current);
                window.clearInterval(timteIntervalFlag.current?.flag);
                if (e.target.checked) {
                  timteIntervalFlag.current.flag = setInterval(() => {
                    handleSendMessage();
                  }, timteIntervalFlag.current.time);
                }
              }}
            >
              定时发送
            </Checkbox>
            <Input
              style={{ width: "150px" }}
              defaultValue={1000}
              type="number"
              suffix={"ms/次"}
              onChange={(v) => {
                timteIntervalFlag.current.time = Number(v.target.value);
              }}
            ></Input>
          </div>
        </div>
      </Card>

      {/* 自动回复设置模态框 */}
      <Modal
        title="自动回复设置"
        open={isModalOpen}
        onOk={() => setIsModalOpen(false)}
        onCancel={() => setIsModalOpen(false)}
        width={"80vw"}
        destroyOnClose
        footer={null}
      >
        <HexForm />
      </Modal>
    </div>
  );
};

export default App;


4. tauri-template

https://gitee.com/ZeroOpens/tauri-template/tree/master
一个带有更新等功能的框架

4.1 tauri.conf.json
...
"plugins": {
    "updater": {
      "pubkey": "myapp.key.pub",
      "endpoints": [
        "https://github.com/user/repo/releases/latest/download/latest.json"
      ]
    }
  }
4.2 cargo.toml
[dependencies]
# 启用 devtools 功能
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-http = "2"

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2"

[profile.dev]
incremental = true # 以较小的步骤编译您的二进制文件。

[profile.release]
codegen-units = 1 # 允许 LLVM 执行更好的优化。
lto = true # 启用链接时优化。
opt-level = "s" # 优先考虑小的二进制文件大小。如果您更喜欢速度,请使用 `3`。
panic = "abort" # 通过禁用 panic 处理程序来提高性能。
strip = true # 确保移除调试符号。

4.3 useTauri.ts

获得Tauri的版本号等

import { ref, onMounted } from 'vue';
import { getTauriVersion, getName, getVersion } from '@tauri-apps/api/app';

export default function () {
  let tauriVersion = ref('')
  let appName = ref('')
  let appVersion = ref('')

  onMounted( async () => {
    tauriVersion.value = await getTauriVersion();  // 获取tauri版本
    appName.value = await getName();  // 获取应用程序名称
    appVersion.value = await getVersion();  // 获取应用程序版本
  })

  // 导出
  return {tauriVersion, appName, appVersion}
}
4.4 update.vue

更新代码,通过运行check,获得update对象,
通过判断是否需要更新,一路下来,完成程序更新
最后通过relaunch重启程序

<template>
  <div class="update-container">
    <div class="update-section">
      <button @click="checkUpdate" :disabled="isUpdating">检查更新</button>
      <div v-if="updateMessage" class="update-message">{{ updateMessage }}</div>
      <div v-if="showProgress" class="progress-container">
        <div class="progress-bar" :style="{ width: progressPercentage + '%' }"></div>
        <div class="progress-text">{{ progressPercentage }}%</div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
  defineOptions({name: 'Update'})
  import { ref } from 'vue';
  import { check } from '@tauri-apps/plugin-updater';
  import { relaunch } from '@tauri-apps/plugin-process';

  const updateMessage = ref('');
  const isUpdating = ref(false);
  const showProgress = ref(false);
  const progressPercentage = ref(0);

  // 更新功能
  const checkUpdate = async () => {
    try {
      isUpdating.value = true;
      updateMessage.value = '正在检查更新...';
      
      const update = await check();
      
      if (!update) {
        updateMessage.value = '您已经在使用最新版本';
        setTimeout(() => {
          updateMessage.value = '';
          isUpdating.value = false;
        }, 3000);
        return;
      }
      
      updateMessage.value = `发现新版本 ${update.version},更新说明: ${update.body}`;
      showProgress.value = true;
      
      let downloaded = 0;
      let contentLength = 0;
      
      await update.downloadAndInstall((event) => {
        switch (event.event) {
          case 'Started':
            contentLength = event.data.contentLength!;
            updateMessage.value = `开始下载更新,总大小: ${(contentLength / 1024 / 1024).toFixed(2)}MB`;
            break;
          case 'Progress':
            downloaded += event.data.chunkLength;
            progressPercentage.value = Math.floor((downloaded / contentLength) * 100);
            break;
          case 'Finished':
            updateMessage.value = '下载完成,准备安装...';
            break;
        }
      });

      updateMessage.value = '更新已安装,即将重启应用...';
      setTimeout(async () => {
        await relaunch();
      }, 2000);
    } catch (error) {
      console.error('Update error:', error);
      updateMessage.value = `更新失败: ${error}`;
      isUpdating.value = false;
      showProgress.value = false;
    }
  }
</script>

5. tauri-vue3

https://gitee.com/funtry/tauri-vue3
一个托盘图标的演示

5.1 tauri.conf.json

security的设置方式有所不同

  • dangerousDisableAssetCspModification 的作用:

false(默认值):
Tauri 会自动修改 CSP(内容安全策略),添加必要的安全规则(如允许加载本地资源、Tauri API 调用等),确保应用正常运行。
例如,自动添加 asset: 协议、ws:(WebSocket)等白名单规则。
推荐保持默认值,除非你有特殊需求。
true:
禁用 Tauri 对 CSP 的自动修改,完全使用开发者配置的 csp 规则。
这是一个 危险选项(前缀 dangerous 已标明),可能导致应用功能异常(如资源加载失败、Tauri API 不可用)。
仅适用于需要 完全自定义 CSP 的高级场景(如严格安全策略需求)。

...
  "app": {
    "windows": [
      {
        "title": "tauri-app-vue3",
        "width": 1330,
        "height": 730
      }
    ],
    "security": {
      "csp": "default-src 'self'",
      "dangerousDisableAssetCspModification": false
    }
  },
5.2 tauriApi.rs

调用函数、关闭、获得窗口基本操作


import { getCurrentWindow } from '@tauri-apps/api/window';
import { exit } from '@tauri-apps/plugin-process';
import { invoke } from '@tauri-apps/api/core';
/**
 * 
 * @param name 
 * @returns 
 */
export const greet = async (name: string): Promise<string> => {
    return await invoke("greet", { name });
}

export const getCurrentWindowInstance = async () => {
    return await getCurrentWindow();
}
/**
 * 退出应用
 */
export const  exitApp = async () => {
    await exit();
}
5.3 tray.rs

托盘图标操作

/**
 * @description: 托盘菜单
 * @return {*}
 */
// 获取当前窗口
import { getCurrentWindow } from '@tauri-apps/api/window';
// 导入系统托盘
import { TrayIcon, TrayIconOptions, TrayIconEvent } from '@tauri-apps/api/tray';
// 托盘菜单
import { Menu } from '@tauri-apps/api/menu';

import { exitApp } from './tools/tauriApi';

/**
 * 在这里你可以添加一个托盘菜单,标题,工具提示,事件处理程序等
 */
const options: TrayIconOptions = {
    // icon 的相对路径基于:项目根目录/src-tauri/,其他 tauri api 相对路径大抵都是这个套路
    icon: "icons/32x32.png",
    // 托盘提示,悬浮在托盘图标上可以显示 tauri-app
    tooltip: 'tauri-app',
    // 是否在左键点击时显示托盘菜单,默认为 true。当然不能为 true 啦,程序进入后台不得左键点击图标显示窗口啊。
    menuOnLeftClick: false,
    // 托盘图标上事件的处理程序。
    action: (event: TrayIconEvent) => {
        // 左键点击事件
        if (event.type === 'Click' && event.button === "Left" && event.buttonState === 'Down') {
            console.log('单击事件');
            // 显示窗口
            winShowFocus();
        }
    }
}

/**
 * 窗口置顶显示
 */
async function winShowFocus() {
    // 获取窗体实例
    const win = getCurrentWindow();
    // 检查窗口是否见,如果不可见则显示出来
    if (!(await win.isVisible())) {
        win.show();
    } else {
        // 检查是否处于最小化状态,如果处于最小化状态则解除最小化
        if (await win.isMinimized()) {
            await win.unminimize();
        }
        // 窗口置顶
        await win.setFocus();
    }
}

/**
 * 创建托盘菜单
 */
async function createMenu() {
    return await Menu.new({
        // items 的显示顺序是倒过来的
        items: [
            {
                icon: 'icons/32x32.png',
                id: 'show',
                text: '显示窗口',
                action: () => {
                    winShowFocus();
                }
            },
            {
                // 菜单 id
                id: 'quit',
                // 菜单文本
                text: '退出',
                //  菜单项点击事件
                action: async() => {
                    try {
                        // 退出程序
                        exitApp();
                    } catch (error) {
                        console.log(error);
                    }
                }
            }
        ]
    })
}

/**
 * 创建系统托盘
 */
export async function createTray() {
    // 获取 menu
    options.menu = await createMenu();
    await TrayIcon.new(options);
}

6. tauri-win-learn

https://gitee.com/mingjianyeying/tauri-win-learn
功能比较多框架
tauri v2 开源项目学习(一)_第1张图片

6.1 tauri.conf.json

无特别

6.2 cargo.toml

加入比较多的插件

[dependencies]
tauri = { version = "2.0.0-beta.3", features = ["wry", "tray-icon", "image-ico"] }
tauri-plugin-opener = "2.0.0-beta.3"
tauri-plugin-sql = { version = "2.0.0-beta.3", features = ["mysql"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "mysql", "macros"], default-features = false }
tokio = { version = "1", features = ["full"] }
tauri-plugin-os = "2.0.0-beta.3"
tauri-plugin-dialog = "2.0.0-beta.3"
tauri-plugin-http = "2.0.0-beta.3"
tauri-plugin-notification = "2.0.0-beta.3"
tauri-plugin-log = "2"
tauri-plugin-system-info = "2.0.9"
6.3 default.json
"permissions": [
    "core:default",
    "opener:default",
    "sql:default",
    "sql:allow-execute",
    "os:default",
    "dialog:default",
    {
      "identifier": "http:default",
      "allow": [
        {
          "url": "https://api.github.com/users/tauri-apps"
        }
      ]
    },
    "notification:default",
    "log:default"
  ]
6.4 main.rs

可以通过mod sysInfo嵌入,也可以通过use tauri_app_lib::{database, tray};嵌入

use serde::Serialize;
use tauri_app_lib::{database, tray};
// 导入sysInfo模块
mod sysInfo;
...
// 注册命令
.invoke_handler(tauri::generate_handler![
    get_os_info,
    database::get_users,
    database::add_user,
    database::update_user,
    database::delete_user,
    sysInfo::get_detailed_system_info,
    sysInfo::get_simplified_system_info
])
6.5 lib.rs
// 引入并导出子模块
pub mod database;
pub mod tray;

// 导出常用结构体,使消费者更容易使用
pub use database::{Config, DatabaseConfig};

// 注意:系统信息命令直接使用tauri_plugin_system_info插件提供的命令
6.6 rust其他功能
  • tray.rs :托盘
  • sysInfo.rs :获取系统信息
  • database.rs :数据库操作
6.7 src\pages\FileOperationPage.jsx

对话框操作

import { open, save, message, ask, confirm } from '@tauri-apps/plugin-dialog';
...
 const file = await open({
        multiple: false,
        directory: false,
      });
6.8 src\pages\HttpCasePage.jsx

http请求

import { fetch } from '@tauri-apps/plugin-http';
...
const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Accept': 'application/json'
        }
      });
6.9 src\pages\LogCasePage.jsx

log日志

import { trace, info, error, debug, warn, attachConsole } from '@tauri-apps/plugin-log';
...
import { appLogDir } from '@tauri-apps/api/path';
...

6.10 src\pages\NotificationCasePage.jsx

通知

import { 
  isPermissionGranted,
  requestPermission,
  sendNotification,
  createChannel,
  Importance,
  Visibility
} from '@tauri-apps/plugin-notification';

7. tffmpeg

https://gitee.com/zwssd1980/tffmpeg
tauri 运行ffmpeg

7.1 lib.rs

这个项目有关于Command一些操作细节可参考

let mut ffmpeg_cmd = Command::new("ffmpeg/bin/ffmpeg.exe")
            .arg("-loglevel")
            .arg("debug")
            .arg("-y")
            .arg("-hide_banner")
            .arg("-i")
            .arg(&input_path_clone)
            .args(options_clone.split_whitespace())
            .arg(&output_file_clone)
            .stdin(Stdio::null())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .creation_flags(0x08000000)
            .spawn()
            .map_err(|e| e.to_string())?;
7.2 App.jsx

前端有些ffmpeg信息归纳

const options2 = [
  { value: "1", label: "音视频质量不变" },
  { value: "2", label: "WEB视频流媒体" },
  { value: "3", label: "H264压缩" },
  { value: "4", label: "H264压缩-Intel加速" },
  { value: "5", label: "H264压缩-AMD加速" },
  { value: "6", label: "H264压缩-NV加速" },
  { value: "7", label: "H265压缩" },
  { value: "8", label: "H265压缩-AMD加速" },
  { value: "9", label: "H265压缩-AMD加速" },
  { value: "10", label: "H265压缩-NV加速" },
  { value: "11", label: "快速时间录制" },
  { value: "12", label: "快速时间放大" },
  { value: "13", label: "设置高质量比例" },
  { value: "14", label: "视频0.5倍速 + 光流法补帧到60帧" },
  { value: "15", label: "裁切视频画面" },
  { value: "16", label: "视频旋转度数" },
  { value: "17", label: "水平翻转画面" },
  { value: "18", label: "垂直翻转画面" },
  { value: "19", label: "设定至指定分辨率并且自动填充黑边" },
  { value: "20", label: "转码到mp3" },
  { value: "21", label: "音频两倍速" },
  { value: "22", label: "音频倒放" },
  { value: "23", label: "声音响度标准化" },
  { value: "24", label: "音量大小调节" },
  { value: "25", label: "静音第一个声道" },
  { value: "26", label: "静音所有声道" },
  { value: "27", label: "交换左右声道" },
  { value: "28", label: "gif(15fps,480p)" },
  { value: "29", label: "从视频区间每秒提取n张照片" },
  { value: "30", label: "截取指定数量的帧保存为图片" },
  { value: "31", label: "视频或音乐添加封面图片" },
];

const audioOptions2 = [
  { value: "32", label: "转码到mp3" },
  { value: "33", label: "音频两倍速" },
  { value: "34", label: "音频倒放" },
  { value: "35", label: "声音响度标准化" },
  { value: "36", label: "音量大小调节" },
  { value: "37", label: "静音第一个声道" },
  { value: "38", label: "静音所有声道" },
  { value: "39", label: "交换左右声道" },
];

const picOptions2 = [
  { value: "40", label: "JPEG压缩质量(1-31,越大压缩率越高)" },
  { value: "41", label: "PNG无损压缩(zlib 1-9)" },
  { value: "42", label: "WebP压缩(质量0-100)" },
];

8. tts-tauri

https://gitee.com/lieranhuasha/tts-tauri
一个tts的demo

8.1 cargo.toml
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4.40"
sha2 = "0.10.8"
uuid = {version = "1.16.0", features = ["v4"] }
url = "2.5.4"
regex = "1.11.1"
hex = "0.4.3"
tokio = {version = "1.44.2", features = ["full"] }
tokio-tungstenite = {version = "0.26.2", features = ["native-tls"] }
futures-util = "0.3.31"
reqwest = "0.12.15"
base64 = "0.22.1"
tauri-plugin-dialog = "2"
tauri-plugin-shell = "2"
8.2 lib.rs

通过utils里的代码,提供了请求接口的rust的写法,值得参考

pub mod utils;
use utils::api::{get_exe_path, get_voices_list, start_tts};
8.3 api.rs

关于接口请求的代码写法

let response = get(url)
        .await
        .map_err(|e| CustomResult::error(Some(e.to_string()), None))?;

    if response.status().is_success() {
        let body = response
            .text()
            .await
            .map_err(|e| CustomResult::error(Some(e.to_string()), None))?;
        let json: serde_json::Value =
            from_str(body.as_str()).map_err(|e| CustomResult::error(Some(e.to_string()), None))?;
        return Ok(CustomResult::success(None, Some(json)));
    }

    return Err(CustomResult::error(
        Some(response.status().to_string()),
        None,
    ));
}
8.4 src\pages\SetPage.vue

这里会打开默认浏览器

    import { open } from '@tauri-apps/plugin-dialog';
    import { open as openShell } from '@tauri-apps/plugin-shell';
    ...
    const openBrowser = async (url)=>{
        await openShell(url);
    }
8.5 src\utils\sqlite.js

数据库也可以在前端操作,sqlite操作

import Database from '@tauri-apps/plugin-sql';
...
function connect(){
    return new Promise(async (resolve, reject) => {
        if(isConnect){
            resolve();
        }else{
            try {
                db = await Database.load('sqlite:database.db');
                // 初始化数据库
                for (let i = 0; i < databseTable.length; i++) {
                    // 初始化数据库,如果出错,则立即中止,并退出程序
                    await init(databseTable[i].name, databseTable[i].sql).catch((err) => {
                        reject(err);
                        return;
                    });
                }
                // 初始化成功,连接成功
                isConnect = true;
                resolve();

            } catch (error) {
                reject(error);
            }
        }
    })
}

tauri2开始,通过tauri-plugin的方式,把更多的编程,通过js语言来编写

你可能感兴趣的:(tauri v2 开源项目学习(一))