系列文章目录
Tauri学习笔记:0
Tauri开源项目:1 - vue-pure-admin Tauri 版本
(github: tauri-pure-admin)
vue-pure-admin (opens new window)是一款开源完全免费且开箱即用的中后台管理系统模版。完全采用 ECMAScript 模块(ESM)规范来编写和组织代码,使用了最新的 Vue3、Vite、Element-Plus、TypeScript、Pinia、Tailwindcss 等主流技术开发。(Pure Admin 保姆级文档)
Vue3入门项目:2
- 【完整版】Vue 3.0 实战,开发基于 Composition API 的 Todo Web App【github源码】
#在线预览
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
关键词
HTML
CSS
JavaScript
Vite
Vue
Tauri
TypeScript
Rust
一、环境配置
npm install -g pnpm
- Tauri快速开始-Vite
- pnpm
- TypeScript
- VSCode安装插件
- Volar
- Tauri
- rust-analyzer
- VSCode按F5生成并配置
launch.json
{
"version": "0.2.0",
"configurations": [
{
"request": "launch",
"name": "pnpm",
"command": "pnpm run tauri dev",
"type": "node-terminal",
}
]
}
二、功能实现
Base工程
- 克隆tauri-pure-admin到本地;
- VSCode打开tauri-pure-admin-main文件夹;
a. 执行pnpm install
;
b. 执行pnpm browser:dev
或者pnpm dev
;
以上两步是确认相关配置是ok的,后续每一个功能的实现、测试均基于此Base工程;
- 功能1:实现读取本地数据加载到ECharts
- 上传文件功能组件:tauri-pure-admin-main\src\components\MyComponents\FileUploader.vue
<template>
<el-upload ref="upload" class="upload-demo" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:limit="1" :on-exceed="handleExceed" :auto-upload="false" style="margin-top: 5px;">
<template #trigger>
<el-button type="primary">选择文件</el-button>
</template>
<el-button class="ml-3" type="success" @click="submitUpload">
上传
</el-button>
<template #tip>
<div class="el-upload__tip text-red">
限制上传一个文件,且不超过 2MB
</div>
</template>
</el-upload>
</template>
<script setup lang="ts">
import { ref, defineEmits } from 'vue'
import { genFileId } from 'element-plus'
import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
const upload = ref<UploadInstance>()
const handleExceed: UploadProps['onExceed'] = (files) => {
upload.value!.clearFiles()
const file = files[0] as UploadRawFile
file.uid = genFileId()
upload.value!.handleStart(file)
}
const submitUpload = () => {
upload.value!.submit();
runFunction();
}
const emit = defineEmits(['function'])
function runFunction() {
emit('function')
}
</script>
- 修改主页面:tauri-pure-admin-main\src\views\welcome\index.vue
<script setup lang="ts">
import { hasAuth, getAuths } from "@/router/utils";
defineOptions({
name: "Welcome"
});
import { ECharts, EChartsOption, init } from 'echarts';
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
import FileUploader from '@/components/MyComponents/FileUploader.vue';
const count = ref(0);
var arr = ref([] as number[]);
arr.value.push(1);
arr.value.push(5);
arr.value.push(4);
arr.value.push(3);
arr.value.push(3);
arr.value.push(count.value);
const option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [50, 20, 36, 10, 10, 50]
}]
};
// 定义props
interface Props {
width?: string;
height?: string;
option?: EChartsOption;
}
const props = withDefaults(defineProps<Props>(), {
width: '100%',
height: '80%',
option: () => ({
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 50]
}]
})
});
const myChartsRef = ref<HTMLDivElement>();
let myChart: ECharts;
let timer: string | number | NodeJS.Timeout | undefined;
const initChart = (): void => {
if (myChart !== undefined) {
myChart.dispose();
}
myChart = init(myChartsRef.value as HTMLDivElement);
myChart?.setOption(props.option, true);
};
const resizeChart = (): void => {
timer = setTimeout(() => {
if (myChart) {
myChart.resize();
}
}, 500);
};
onMounted(() => {
initChart();
window.addEventListener('resize', resizeChart);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeChart);
clearTimeout(timer);
timer = 0;
});
watch(
props.option,
() => {
initChart();
},
{
deep: true
}
);
const update = () => {
count.value++;
arr.value.pop();
arr.value.push(count.value);
option.series[0].data = arr.value;
myChart.setOption(option);
};
</script>
<template>
<div>
<FileUploader @function="update" />
</div>
<div ref="myChartsRef" :style="{ height: height, width: width }" :option="option" />
</template>
- 效果图
- 功能2:动态添加按钮
- 添加按钮组件:tauri-pure-admin-main\src\components\MyComponents\AddButtons.vue
<template>
<div>
<!-- 遍历props中的buttons数组,创建一个按钮列表 -->
<el-button v-for="(button, index) in props.buttons" :key="index" @click="button.onClick">
{{ button.text }}
</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 定义Button接口,描述按钮的结构
export interface Button {
text: string; // 按钮显示的文本
onClick: () => void; // 点击按钮时执行的函数
}
// 使用defineProps定义组件接收的props
const props = defineProps({
buttons: {
// buttons属性为一个Button类型数组,是必传属性
type: Array as () => Button[],
required: true,
},
})
</script>
- 页面加载:tauri-pure-admin-main\src\components\MyComponents\AddButtons.vue
<script setup lang="ts">
import { hasAuth, getAuths } from "@/router/utils"; // 引入权限控制相关工具函数
defineOptions({
name: "Welcome" // 设置当前组件的名称
});
import { ECharts, EChartsOption, init } from 'echarts'; // 引入echarts相关模块
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'; // 引入Vue 3的响应式和生命周期函数
import FileUploader from '@/components/MyComponents/FileUploader.vue'; // 引入文件上传组件
import AddButtons from "@/components/MyComponents/AddButtons.vue"; // 引入按钮组件
import { Button } from "@/components/MyComponents/AddButtons.vue"; // 引入按钮组件中的Button类型
// 初始化echarts使用的数据和配置
const count = ref(0);
var arr = ref([] as number[]); // 初始化一个数字类型的数组
arr.value.push(1);
arr.value.push(5);
arr.value.push(4);
arr.value.push(3);
arr.value.push(3);
arr.value.push(count.value);
const option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [50, 20, 36, 10, 10, 50]
}]
};
// 定义组件的props
interface Props {
width?: string; // 组件宽度
height?: string; // 组件高度
option?: EChartsOption; // echarts配置项
}
// 使用withDefaults来设置props的默认值
const props = withDefaults(defineProps<Props>(), {
width: '100%',
height: '80%',
option: () => ({
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 50]
}]
})
});
// 初始化echarts图表的引用
const myChartsRef = ref<HTMLDivElement>();
let myChart: ECharts;
let timer: string | number | NodeJS.Timeout | undefined;
// 初始化图表和监听窗口大小改变来调整图表大小
const initChart = (): void => {
if (myChart !== undefined) {
myChart.dispose();
}
myChart = init(myChartsRef.value as HTMLDivElement);
myChart?.setOption(props.option, true);
};
const resizeChart = (): void => {
timer = setTimeout(() => {
if (myChart) {
myChart.resize();
}
}, 500);
};
// 生命周期钩子:组件挂载时初始化图表,并添加窗口大小改变的监听
onMounted(() => {
initChart();
window.addEventListener('resize', resizeChart);
});
// 生命周期钩子:组件卸载前移除监听并清理资源
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeChart);
clearTimeout(timer);
timer = 0;
});
// 监听props中的option变化,以重新初始化图表
watch(
props.option,
() => {
initChart();
},
{
deep: true
}
);
// 更新图表数据的函数
const update = () => {
count.value++;
arr.value.pop();
arr.value.push(count.value);
option.series[0].data = arr.value;
myChart.setOption(option); // 更新图表配置
};
// 初始化一组按钮,并定义其点击事件来更新图表
const buttons = ref<Button[]>([]);
var i;
for (i = 0; i < 10; i++) {
const newButton: Button = {
text: `按钮 ${i}`,
onClick: () => {
update();
},
};
buttons.value.push(newButton);
}
</script>
<template>
<div>
<AddButtons :buttons="buttons" /> <!-- 显示添加的按钮 -->
<FileUploader @function="update" /> <!-- 文件上传组件,上传后更新图表 -->
</div>
<div ref="myChartsRef" :style="{ height: height, width: width }" :option="option" /> <!-- echarts图表容器 -->
</template>
- 效果图
- 功能3:Tauri打开文件对话框
- tauri2.0.0-bete.19支持文件对话框,实现跟tauri1有区别,见官方说明;
- 要点:
a. 初始化项目时要选择--beta
版本,这才是tauri2.0.0,cargo create-tauri-app --beta
;
b. 与官方文档不同,这里与前端交互的rust函数参数要添加tauri::Window
类型参数,从而函数体可以使用window.dialog()
使用当前窗口句柄创建对话框; - 完整代码:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri_plugin_dialog::DialogExt;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(window: tauri::Window, name: &str) -> String {
window.dialog().file().pick_file(|file_path| {
// do something with the optional file path here
// the file path is `None` if the user closed the dialog
println!("{:?}", file_path);
});
format!("Hello, {}! You've been greeted from Rust!", name)
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- 效果图
- 功能4:Rust通过管道多线程传递变量
- 代码:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri_plugin_dialog::DialogExt;
// 1. 引入包
use std::sync::mpsc;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(app: tauri::AppHandle, name: &str) -> String {
// 2. 创建管道
let (tx, rx) = mpsc::channel();
app.dialog().file().pick_file(move |file_path| match file_path {
Some(file_path) => {
println!("{:?}", file_path.path);
// 3. 管道发送
let _ = tx.send(file_path.path);
},
None => println!("Canceled"),
});
// 4. 管道接受
let res = rx.recv().unwrap();
format!("Hello, {:?}! You've been greeted from Rust!", res)
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- 效果图:
- 功能5:rust打开对话框读取csv到dict
- Cargo.toml
csv = "1.1.1"
polars = { version = "0.33.2", features = ["lazy", "ndarray"] }
- csv_to_dict.rs
use polars::prelude::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
pub fn read_csv_to_dict(
file_path: &str,
) -> Result<HashMap<String, Vec<Option<f64>>>, Box<dyn std::error::Error>> {
// 创建一个文件缓冲读取器
let file = File::open(file_path)?;
let reader = BufReader::new(file);
// 使用CsvReader读取CSV文件,第一行被视为列名
let df = CsvReader::new(reader)
.has_header(true) // 指定第一行是列名
.finish()?;
let mut dict = HashMap::new();
for column in df.get_columns() {
let column_name = column.name();
let column_values = df
.column(column.name())
.unwrap()
.as_any()
.downcast_ref::<Float64Chunked>()
.unwrap()
.into_iter()
.map(|v| v)
.collect::<Vec<_>>();
dict.insert(column_name.to_string(), column_values);
}
Ok(dict)
}
- main.rs
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod csv_to_dict;
use std::sync::mpsc;
use tauri_plugin_dialog::DialogExt;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(app: tauri::AppHandle, name: &str) -> String {
let (tx, rx) = mpsc::channel();
app.dialog()
.file()
.pick_file(move |file_path| match file_path {
Some(file_path) => {
match csv_to_dict::read_csv_to_dict(file_path.path.to_str().unwrap()) {
Ok(data) => {
let _ = tx.send(data);
}
Err(err) => {
eprintln!("Error: {}", err);
}
}
}
None => println!("Canceled"),
});
let res = rx.recv().unwrap();
for key in res.keys() {
println!("{:?}", res.get(key).unwrap());
}
format!("Hello, {:?}! You've been greeted from Rust!", "123")
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- 功能6:serd
- csv_to_json.rs
use serde::{Deserialize, Serialize};
use serde_json::{Result, Value};
#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: i32,
city: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct Vecs {
names: Vec<String>,
rows: Vec<Vec<String>>,
}
pub fn print_json() -> String {
let person = Person {
name: String::from("jack"),
age: 10,
city: String::from("beijing"),
};
let vecs = Vecs {
names: vec![
String::from("name"),
String::from("age"),
String::from("city"),
],
rows: vec![
vec![
String::from("jack"),
String::from("10"),
String::from("beijing"),
],
vec![
String::from("tom"),
String::from("20"),
String::from("shanghai"),
],
],
};
let json = serde_json::to_string(&vecs).unwrap();
println!("{:#?}", json);
json
}
- main.rs
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod csv_to_json;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
let json = csv_to_json::print_json();
format!("Hello, {}! You've been greeted from Rust!", json)
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- 效果图:
- 功能6:前端vue按钮打开后端tauri对话框选择本地csv文件读取数据传给前端ts得到json对象使用;
- Cargo.toml
[package]
name = "demo"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies]
tauri = { version = "2.0.0-beta", features = [] }
tauri-plugin-shell = "2.0.0-beta"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
csv = "1.1.1"
polars = { version = "0.33.2", features = ["lazy", "ndarray"] }
tauri-plugin-dialog = "2.0.0-beta.7"
- Greet.vue
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
const greetMsg = ref("");
const name = ref("");
async function greet() {
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
greetMsg.value = await invoke("greet", { name: name.value });
console.log(greetMsg.value);
let json = JSON.parse(greetMsg.value)
console.log(json);
console.log(json["rows"][0]);
}
</script>
<template>
<form class="row" @submit.prevent="greet">
<input id="greet-input" v-model="name" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p>{{ greetMsg }}</p>
</template>
- util.rs
use polars::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::{Result, Value};
use std::fs::File;
#[derive(Serialize, Deserialize, Debug)]
struct Vecs {
names: Vec<String>,
rows: Vec<Vec<String>>,
}
pub fn read_csv_to_json(file_path: &str) -> Result<String> {
let df = CsvReader::new(File::open(file_path).unwrap())
.infer_schema(None)
.has_header(true)
.finish()
.unwrap(); // 读取CSV文件
let column_names = df
.get_column_names()
.iter()
.map(|name| name.to_string())
.collect();
let data: Vec<Vec<_>> = df
.get_columns()
.iter()
.map(|c| c.iter().map(|v| v.to_string()).collect::<Vec<_>>())
.collect();
let vecs = Vecs {
names: column_names,
rows: data,
};
let json = serde_json::to_string(&vecs).unwrap();
Ok(json)
}
- main.rs
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod util;
use std::sync::mpsc;
use tauri_plugin_dialog::DialogExt;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(app: tauri::AppHandle, name: &str) -> String {
let (tx, rx) = mpsc::channel();
app.dialog()
.file()
.pick_file(move |file_path| match file_path {
Some(file_path) => {
let _ = tx.send(file_path.path);
}
None => println!("Canceled"),
});
let path = rx.recv().unwrap();
let json = util::read_csv_to_json(path.to_str().unwrap()).unwrap();
json
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
- 效果图:
- 常用命令:
cargo creare-tauri-app --beta
//cd src-tauri
cargo add tauri-plugin-dialog
//cd ..
$env:RUST_BACKTRACE=1; pnpm tauri dev
- 功能7:tauri读取csv数据更新ts的Echarts;
- Greet.vue
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { EChartsOption, ECharts, init } from "echarts";
const greetMsg = ref("");
const name = ref("");
async function greet() {
greetMsg.value = await invoke("greet", { name: name.value });
let json = JSON.parse(greetMsg.value);
option.series[0].data = json["rows"][0].map(Number);
option.series[0].type = "line";
myChart.setOption(option);
}
interface Props {
width?: string;
height?: string;
option?: EChartsOption;
}
const props = withDefaults(defineProps<Props>(), {
width: "100%",
height: "500%",
option: () => ({
title: {
text: "ECharts 入门示例",
},
tooltip: {},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
},
yAxis: {},
series: [
{
name: "销量",
type: "bar",
data: [5, 20, 36, 10, 10, 50],
},
],
}),
});
const myChartsRef = ref<HTMLDivElement>();
let myChart: ECharts;
let timer: number | undefined;
const initChart = (): void => {
if (myChart !== undefined) {
myChart.dispose();
}
myChart = init(myChartsRef.value as HTMLDivElement);
myChart?.setOption(props.option, true);
};
const resizeChart = (): void => {
timer = setTimeout(() => {
if (myChart) {
myChart.resize();
}
}, 500);
};
onMounted(() => {
initChart();
window.addEventListener("resize", resizeChart);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resizeChart);
clearTimeout(timer);
timer = 0;
});
watch(
props.option,
() => {
initChart();
},
{
deep: true,
}
);
const option = {
title: {
text: "ECharts 入门示例",
},
tooltip: {},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
},
yAxis: {},
series: [
{
name: "销量",
type: "bar",
data: [50, 20, 36, 10, 10, 50],
},
],
};
</script>
<template>
<form @submit.prevent="greet">
<input id="greet-input" v-model="name" placeholder="选择csv数据..." />
<button type="submit">打开...</button>
</form>
<p>{{ greetMsg }}</p>
<p
ref="myChartsRef"
:style="{ height: height, width: width }"
:option="option"
/>
</template>
- 效果图: