项目目标
主要的目的是学习tauri。
流程
1、搭建项目
2、简单的在项目使用leaflet
3、打包
准备项目
环境准备
废话不多说,直接开始
需要有准备能运行Rust的环境和Node,对于Rust可以参考下面这位大佬的文章,Node不必细说。
Rust 和 Cargo 安装指南-CSDN博客https://blog.csdn.net/qq_44154915/article/details/139365116
建立项目
进入tauri2
官网https://tauri.app/start/
笔者将使用pnpm建立项目,项目名称为ttvvl
如下
使用VSCode或者WebStorm打开项目
安装依赖
pnpm install
运行
pnpm tauri dev
然后就在compiling
这笔者感到疑惑,可能是第一次运行,搞了许久
运行结果
没有问题。
看一下文件夹的属性
6个G,有点大。
如果以
pnpm run dev
点击按钮,会出现错误
观察上面的代码,我们可以发现,这个是invoke好像是个方法。第一个参数是个字符串greet
第二个参数是个对象,属性是name。
同时找到src-tauri/src/lib.rs中的代码,如下
fn 是Rust的关键字,相当与定义了一个函数,函数名叫greet,参数name
format!是个格式化字符串的函数
其他不是很懂,但我们可以把这段代码给deepseek,问一问
解答如下
从这里我们可以得到关键信息——被这个宏标记的函数能被前端调用。
因为Rust函数的函数名叫greet,而invoke的第一个参数是字符串greet,我们可以很容易猜测
二者必然有关系,我们可以检验一下
修改函数名为greets,运行
发现报错了
我们可以看到下面还有一个函数
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
可以问一问deepseek
原来下面那个函数是入口函数,invoke_handler是注册器,因此,把中括号里面的greet改成greets
应该可以运行了,(每次都要编译,有点慢)。
如果点击按钮,下面应该不会出现字
事实确实如此。
那么推断很有可能是正确的,那么我们把字符串greet改成greets,就可以出现字了
事实确实如此,笔者明白了。
再次观察,Rust函数greets返回值是String,
而返回格式化后的字符串,对于App.vue中的TypeScript代码,打印出greetMsg.value的值
在运行后的的tauri中,可以打开开发者工具,打印结果如下。
既然如此,感觉明白了,invoke作为Rust与TypeScript交互的关键函数。
安装leaflet有关依赖
pnpm install leaflet leaflet-geoman-free
因为使用typescipt,还需要安装类型定义包
pnpm install --save @types/leaflet.pm @types/leaflet
显示地图
这也是很麻烦的事情。
安装vue-router
pnpm install vue-router
新建一些目录和文件,如下
在router目录的index.ts中
import {createRouter, createWebHashHistory, Router, RouteRecordRaw, RouterOptions} from "vue-router";
const routes:RouteRecordRaw[]=[
{
path:"/",
redirect:"/map",
children:[
{
path:"map",
component:()=>import("../views/Map.vue")
}]
}
]
const options:RouterOptions={
history:createWebHashHistory(),
routes
}
const router:Router=createRouter(options)
export default router
暂时先这么写
main.ts的内容
import { createApp } from "vue";
import App from "@/App.vue";
import router from "@/router";
import * as L from "leaflet";
import "leaflet/dist/leaflet.css";
import "@geoman-io/leaflet-geoman-free";
import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
const app=createApp(App);
app.use(router);
app.config.globalProperties.$L = L;
app.mount("#app");
将L作为Vue的全局属性,并且引入插件leaflet-geoman-free及相关样式
关于@符号,不多解释。
在constant/data.ts中
export const MAP_URL= "https://tile-a.openstreetmap.fr/hot/{z}/{x}/{y}.png";
这个是地图瓦片的URL。通过它就能访问地图。
在utils/Map.ts中
import {MAP_URL} from "@/constant/data.ts";
import L from 'leaflet';
const GetMap = (
control: any=null, //控件
language:any = 'zh' //语言
): L.Map => {
const options: L.MapOptions = {
center: [30.6667, 104.0667], //中心点
minZoom: 0,
maxZoom: 30,
zoom: 10,
zoomControl: true,
doubleClickZoom: true,
attributionControl: true,
dragging: true,
boxZoom: true,
scrollWheelZoom: true,
zoomSnap: 0.5,
};
const map = L.map('map', options);
L.tileLayer(MAP_URL, {
attribution: '',
}).addTo(map);
map.pm.addControls(control);
map.pm.setLang(language);
return map;
};
export default GetMap;
如果不写
import L from 'leaflet'
Vue: 'L' refers to a UMD global, but the current file is a module. Consider adding an import instead.
在views/Map.vue中
<script setup lang="ts">
import GetMap from "@/utils/Map.ts";
import { onMounted } from 'vue';
let map: any = null
onMounted(() => {
map = GetMap();
map.on('click', (e: any) => {
console.log(e);
});
})
</script>
<template>
<div id="map"></div>
</template>
<style scoped>
#map {
height: 100vh;
width: 200vh;
}
</style>
运行,结果如下
地图定位到成都市。
显示控件
可以查看leaflet-geoman-free文档
Introduction | Documentation for Leaflet-Geomanhttps://geoman.io/docs/leaflet在constant/data.ts中
export const MAP_CONTROL={
}
可以写一些设置,但有默认选项,就不改了。
在views/Map.vue中导入MAP_CONTROL,作为GetMap的参数
map = GetMap(MAP_CONTROL);
,使用控件,运行结果如下
搞点小操作1——计算两点之间的距离
操作过程
点两个点,通过invoke函数发送经纬度到rust函数,返回结果
计算距离函数
使用Haversine 公式
haversine公式计算两经纬度点距离-CSDN博客https://blog.csdn.net/spatial_coder/article/details/116605509使用Rust实现,src-tauri/src/lib.rs的代码如下
const EARTH_RADIUS_KM: f64 = 6371.0; // 地球半径,单位:公里
#[tauri::command]
// 计算两个经纬度点之间的距离(单位:公里)
fn haversine_distance(start:[f64;2],end:[f64;2]) -> f64 {
// 将经纬度从度数转换为弧度
let lat1_rad = start[0].to_radians();
let lon1_rad = start[1].to_radians();
let lat2_rad = end[0].to_radians();
let lon2_rad = end[1].to_radians();
// 经纬度差值
let dlat = lat2_rad - lat1_rad;
let dlon = lon2_rad - lon1_rad;
// Haversine 公式
let a = (dlat / 2.0).sin().powi(2) + lat1_rad.cos() * lat2_rad.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
// 计算距离
EARTH_RADIUS_KM * c
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![haversine_distance])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
以公里作为单位
对两个点marker的思考
点了两个点marker,我们通过leaflet和有关插件的某些事件可以获得这两个点的属性,
我们可以修改这些属性。
那应该然后修改?
可以尝试使用tauri的多窗口。
marker位于父窗口,发送信号到子窗口,在子窗口进行修改,返回父窗口。
父子窗口以及发送信号
关于窗口的配置
src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main","marker-name"],
"permissions": [
"core:default",
"opener:default",
"core:window:allow-hide",
"core:window:allow-show"
]
}
在子窗口添加一个路由
src/router/index.ts
{
path:"/name",
component:()=>import("../views/name.vue")
}
src-tauri/tauri.conf.json
"windows": [
{
"title": "ttvvl",
"label": "main",
"width": 800,
"height": 600
},
{
"title": "点位窗口",
"width": 400,
"height": 300,
"label": "marker-name",
"resizable": true,
"parent": "main",
"visible": false,
"url":"#/name",
"decorations": false,
"center": true
}
],
参数的具体含义,可以参考官网或者deekssek
{
"windows": [
{
"label": "main", // 窗口的唯一标识符
"title": "My Tauri App", // 窗口标题
"width": 800, // 窗口宽度
"height": 600, // 窗口高度
"x": null, // 窗口初始水平位置(null 表示由系统决定)
"y": null, // 窗口初始垂直位置(null 表示由系统决定)
"minWidth": null, // 窗口最小宽度
"minHeight": null, // 窗口最小高度
"maxWidth": null, // 窗口最大宽度
"maxHeight": null, // 窗口最大高度
"resizable": true, // 是否允许调整窗口大小
"fullscreen": false, // 是否全屏
"focus": true, // 窗口是否在创建时获得焦点
"visible": true, // 窗口是否可见
"decorations": true, // 是否显示窗口装饰(标题栏、边框等)
"alwaysOnTop": false, // 窗口是否始终置顶
"maximized": false, // 窗口是否最大化
"transparent": false, // 窗口背景是否透明
"center": true, // 窗口是否居中
"theme": "system", // 窗口主题("system"、"light"、"dark")
"url": "index.html", // 窗口加载的页面路径
"fileDropEnabled": true, // 是否启用文件拖放功能
"skipTaskbar": false, // 是否在任务栏中显示窗口
"shadow": true, // 是否显示窗口阴影
"acceptFirstMouse": false, // 是否接受首次鼠标点击事件
"tabbingIdentifier": null, // macOS 标签页标识符
"titleBarStyle": "visible", // macOS 标题栏样式("visible"、"transparent"、"overlay")
"hiddenTitle": false // 是否隐藏标题栏标题
}
]
}
父窗口
src/views/Map.vue
<script setup lang="ts">
//@ts-nocheck
import GetMap from "@/utils/Map.ts";
import {MAP_CONTROL} from "@/constant/data.ts";
import {invoke} from "@tauri-apps/api/core";
import {emit,listen} from "@tauri-apps/api/event";
import {onMounted, ref} from 'vue';
import WindowManager from '@/utils/window';
const windowManager = new WindowManager();
let startName=ref<String>('起点');
let endName=ref<String>('终点');
let markerWindow,mainWindow,markerIndex=null;
let map: any = null
let markerList: L.Marker[] = [];
let distance=ref<number>(0);
listen('message-to-main-window', (event)=>{ //监听消息
let payload:any=event.payload
if(payload.close){
windowManager.closeWindow('marker-name')
mainWindow.show()
}
if(markerIndex===null){
return
}
if(markerIndex===0){
startName.value='起点'+payload.tooltipContent
}else{
endName.value='终点'+payload.tooltipContent
}
markerList[markerIndex].bindTooltip(payload.tooltipContent)
});
async function sendMsg(tooltipContent:String,nowClickMarkerId:Number) { //发送消息
markerWindow=await windowManager.getWindowByLabel('marker-name') //子窗口
mainWindow=await windowManager.getWindowByLabel('main') //父窗口
if(markerWindow) {
mainWindow.hide()
markerWindow.show();
markerIndex=markerList.findIndex(
(marker:L.Marker)=>marker._leaflet_id===nowClickMarkerId
)
emit('message-to-second-window', { //发送消息 参数
lat: markerList[0].getLatLng().lat,
lng: markerList[0].getLatLng().lng,
markerIndex:markerIndex,
tooltipContent:tooltipContent
}
)
}
}
async function get_distance(){
let start=markerList[0].getLatLng(); //开始点
let end=markerList[1].getLatLng(); //终点
let startArray=[start.lat,start.lng];
let endArray=[end.lat,end.lng];
distance.value = await invoke('haversine_distance', { //发送给rust tauri
start: startArray,
end: endArray
});
}
onMounted(() => {
// openWindow()
map = GetMap(MAP_CONTROL);
map.on('pm:create', (e: any) => { // map create事件
let marker:L.Marker = e.marker;
marker.on('click',(e)=>{ // marker的点击事件
let nowClickMarkerId=e.target._leaflet_id // 获取marker的id
sendMsg(e.target.getTooltip(),nowClickMarkerId) // 发送消息
})
markerList.push(marker);
let length = markerList.length;
if(length==2){ // 起始点 和终点
get_distance() //获取距离
}else if(length>2){
markerList[0].remove(); //移除第一个点
markerList.shift();
get_distance() // 获取距离
}
})
})
</script>
<template>
<div id="map">
<div id="distance-text" v-if="markerList.length>=2">从{{startName}}到{{endName}}的距离为</div>
<div>
<button type="button" id="show" >{{distance}}</button>
</div>
<div id="unit">
公里
</div>
</div>
</template>
<style scoped>
#map {
margin: 0 auto;
width: 100%;
height: 900px;
}
#show {
max-width: 200px;
max-height: 50px;
padding: 10px;
background-color: blue;
color: white;
text-align: center;
position: absolute;
top: 100px;
right: 10px;
z-index: 999;
border-radius: 20px;
}
#distance-text {
max-width: 200px;
max-height: 50px;
padding: 10px;
background-color: #af8433;
color: white;
text-align: center;
position: absolute;
top: 10px;
right: 10px;
z-index: 999;
border-radius: 20px;
}
#unit {
max-width: 200px;
max-height: 50px;
padding: 10px;
background-color: #af8433;
color: white;
text-align: center;
position: absolute;
top: 150px;
right: 10px;
z-index: 999;
border-radius: 20px;
}
</style>
子窗口
src/views/Name.vue
<script setup>
import {listen,emit} from "@tauri-apps/api/event";
import {onMounted, ref} from "vue";
let lat = ref(0);
let lng = ref(0);
let markerIndex = ref(0); // 0 替换成起点 1 替换成终点
let inputValue = ref('');
let oldTooltip=null
const handleClicked = () => {
emit('message-to-main-window', {
tooltipContent:inputValue.value,
close: true,
});
}
const handleCancel = () => {
emit('message-to-main-window', {
tooltipContent:oldTooltip,
close: true,
});
}
onMounted(
() => {
listen('message-to-second-window', (event) => {
let payload = event.payload;
lat.value = payload.lat;
lng.value = payload.lng;
markerIndex.value = payload.markerIndex;
inputValue.value=payload.tooltipContent;
oldTooltip=payload.tooltipContent
});
}
)
</script>
<template>
<div data-tauri-drag-region class="titlebar" >
<h1>{{markerIndex===0?'起点':'终点'}}</h1>
<div v-if="lat>0">纬度:{{lat }}</div>
<div v-if="lng>0">经度:{{lng }}</div>
<div>名称
<br/>
<input v-model="inputValue"></input>
</div>
<br/>
<button type="button" @click="handleClicked" class="sure">确定</button>
<button type="button" @click="handleCancel" class="cancel">取消</button>
</div>
</template>
<style >
.sure{
padding: 10px ;
margin-right: 10vw;
background-color: #5174ff;
}
.cancel{
padding: 10px ;
margin-right: 10vw;
background-color: #ffb43e;
}
</style>
窗口管理
src/utils/window.ts
import { getAllWebviewWindows } from '@tauri-apps/api/webviewWindow';
export class WindowManager {
async getWindowByLabel(label: string) {
const windows = await getAllWebviewWindows();
return windows.find((win) => win.label === label);
}
async closeWindow(label: string) {
const window = await this.getWindowByLabel(label);
if (window) {
window.hide();
}
}
}
export default WindowManager;
运行结果
运行结果倒是没问题,代码写得不行,算了。不管那些。
打包
本地打包
打包成exe命令
pnpm tauri build
因为笔者不是第一次打包,如果是第一次打包,需要下载一些东西,笔者就不展示了,可参考下面这位大佬的过程。
从零开始的 Tauri 开发 & 打包成 exe 【Windows 平台】_tauri 打包-CSDN博客https://blog.csdn.net/u010263423/article/details/136006546对于leaflet打包,会出现一个bug,解决过程可看这篇文章
解决Vue+Vite打包后Leaflet的marker图标不显示的问题_leaflet l.marker 没有-CSDN博客https://blog.csdn.net/qq_63401240/article/details/139972362?spm=1001.2014.3001.5502总之。打包结果
打包运行结果
可以。
上传到github并打包
在github上通过工作流打包
工作流代码参考下面这位大佬
tauri使用github的action自动发布release,让别人也可以看到下载链接_tauri github action-CSDN博客https://blog.csdn.net/weixin_44786530/article/details/140904091在大佬的基础上做出小的修改
修改了工件的版本,其他改动不大
运行结果如下
发现ubuntu出错了。笔者没有深究。
笔者再次打包,只选择了window系统,结果如下
项目地址
qe-present/ttvvlhttps://github.com/qe-present/ttvvl
总结
简单地使用了tauri,使用了信号通信,多窗口,打包。