系列文章目录
- Tauri学习笔记:0
Tauri学习笔记:3 - 读取CSV数据动态添加字段绘制曲线
前言
基于tauri-pure-admin二次开发,实现rust后端读取csv数据返回给vue前端,根据csv字段动态生成checkbox,单击或多选checkbox绘制一条或多条曲线。
一、使用技术
1.框架
- Tauri 是一款应用构建工具包,让您能够为使用 Web 技术的所有主流桌面操作系统构建软件。
Tauri - vue-pure-admin (opens new window)是一款开源完全免费且开箱即用的中后台管理系统模版。
Pure Admin - Apache ECharts 一个基于 JavaScript 的开源可视化图表库。
Echarts - Element Plus 基于 Vue 3,面向设计师和开发者的组件库。
Element Plus
2.语言
- Html
- Css
- Typescript
- Rust
3.环境配置
- node
- npm
- pnpm
- rust
- vscode
- git
二、开发步骤
1.克隆tauri-pure-admin项目
代码如下:
git clone https://github.com/pure-admin/tauri-pure-admin.git
2.安装依赖
代码如下:
pnpm install
3.启动、打包
代码如下:
# 桌面端
pnpm dev
# 浏览器端
pnpm browser:dev
# 桌面端
pnpm build
# 浏览器端
pnpm browser:build
4.新建页面菜单
代码如下:src\router\modules\functions.ts
import Redirect from "@/layout/redirect.vue";
// 最简代码,也就是这些字段必须有
export default {
path: "/functions",
Redirect: "/functions/function1",
meta: {
icon: "ri:beer-fill",
title: "功能模块"
},
children: [
{
path: "/functions/function1",
name: "function1",
component: () => import("@/views/functions/function1.vue"),
meta: {
title: "功能1:数据表格(Vue版)"
}
},
{
path: "/functions/function2",
name: "function2",
component: () => import("@/views/functions/function2.vue"),
meta: {
title: "功能2:数据表格(Rust版)"
}
}
]
};
代码如下:src\router\modules\help.ts
export default {
path: "/help",
redirect: "/help/help1",
meta: {
icon: "ri:24-hours-fill",
// showLink: false,
title: "使用文档",
rank: 9
},
children: [
{
path: "/help/help1",
name: "help1",
component: () => import("@/views/help/help1.vue"),
meta: {
title: "使用技术"
}
},
{
path: "/help/help2",
name: "help2",
component: () => import("@/views/help/help2.vue"),
meta: {
title: "版本控制"
}
},
{
path: "/help/help3",
name: "help3",
component: () => import("@/views/help/help3.vue"),
meta: {
title: "软件说明"
}
}
]
} satisfies RouteConfigsTable;
代码如下:src\views\functions\function1.vue
<script setup lang="ts">
defineOptions({
// name 作为一种规范最好必须写上并且和路由的name保持一致
name: "function1"
});
import { ref, onMounted, onUnmounted } from "vue";
import Papa from "papaparse";
interface CSVRow {
[key: string]: string;
}
// 定义ref
const csvData = ref<CSVRow[]>([]);
const headers = ref<string[]>([]);
// 文件输入的ref
const fileInputRef = ref<HTMLInputElement>(null);
// 处理文件上传
const handleFileUpload = async (event: Event) => {
const fileInput = fileInputRef.value;
if (!fileInput || !fileInput.files) return;
const file: File = fileInput.files[0];
if (file.type !== "text/csv") return;
try {
const parsed = await new Promise<{
data: CSVRow[];
meta: { fields: string[] };
}>((resolve, reject) =>
Papa.parse(file, {
header: true,
complete: results => resolve(results),
error: reject
})
);
csvData.value = parsed.data;
headers.value = parsed.meta.fields;
console.log(csvData.value);
console.log(headers.value);
} catch (error) {
console.error("Error parsing CSV:", error);
}
};
// 在组件挂载后添加事件监听器
onMounted(() => {
fileInputRef.value.addEventListener("change", handleFileUpload);
});
// 在组件卸载时移除事件监听器
onUnmounted(() => {
fileInputRef.value.removeEventListener("change", handleFileUpload);
});
</script>
<template>
<div>
<input type="file" ref="fileInputRef" accept=".csv" />
<el-table
stripe
highlight-current-row
scrollbar-always-on
height="500px"
:data="csvData"
style="width: 100%"
>
<el-table-column
v-for="header in headers"
:key="header"
:prop="header"
:label="header"
></el-table-column>
</el-table>
</div>
</template>
代码如下:src\views\functions\function2.vue
<script setup lang="ts">
import { open } from "@tauri-apps/api/dialog";
import { onBeforeUnmount, onMounted, ref, watch, reactive } from "vue";
import { invoke } from "@tauri-apps/api/tauri";
import { EChartsOption, ECharts, init } from "echarts";
import AddCheckboxs from "@/components/ReMy/AddCheckboxs.vue";
import { Checkbox } from "@/components/ReMy/AddCheckboxs.vue";
import { json } from "stream/consumers";
import { message } from "@/utils/message";
defineOptions({
name: "function2"
});
window.alert = function (name) {
var iframe = document.createElement("IFRAME");
iframe.style.display = "none";
iframe.setAttribute("src", "data:text/plain,");
document.documentElement.appendChild(iframe);
window.frames[0].window.alert(name);
iframe.parentNode.removeChild(iframe);
};
const handleclick = async () => {
alert(
"注意:\r\n1. CSV 文件必须为UTF-8编码(若非UTF-8编码可打开文件以UTF-8编码另存即可),否则可能出现中文乱码、闪退问题!\r\n2. CSV 文件非数值数据会替换为0!"
);
const selected = await open({
filters: [
{
name: "UTF-8编码CSV文件",
extensions: ["csv"]
}
]
});
if (selected === null) {
alert("请选择文件");
} else {
greet(selected);
}
};
const greetMsg = ref("");
let jsonRef;
async function greet(selected) {
let data: string = await invoke("greet_table", { name: selected });
jsonRef = JSON.parse(data);
let json = jsonRef;
greetMsg.value = json["path"];
tableData.value = json["data"];
tableHeaders.value = json["headers"];
console.log(tableHeaders.value);
console.log(tableData.value);
}
interface CSVRow {
[key: string]: string;
}
const tableHeaders = ref<string[]>([]);
const tableData = ref<CSVRow[]>([]);
</script>
<template>
<div>
<!-- 必须用一个div根节点页面跳转才不会空白 -->
<el-button type="success" @click="handleclick">加载数据</el-button>
<p>{{ greetMsg }}</p>
<el-table :data="tableData" lazy style="width: 100%" height="500px">
<el-table-column
v-for="header in tableHeaders"
:key="header"
:prop="header"
:label="header"
></el-table-column>
</el-table>
</div>
</template>
代码如下:src\views\help\help1.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
import noAccess from "@/assets/status/403.svg?component";
defineOptions({
name: "help1"
});
</script>
<template>
<div>
<el-page-header title="使用文档" content="使用技术" />
<el-card class="box-card">
<div>
Tauri 是一款应用构建工具包,让您能够为使用 Web 技术的所有主流桌面操作系统构建软件。
<el-link type="primary" href="https://tauri.app/zh-cn/v1/guides/getting-started/prerequisites" target="_blank"
>Tauri</el-link
>
</div>
<div>
vue-pure-admin (opens new window)是一款开源完全免费且开箱即用的中后台管理系统模版。
<el-link type="primary" href="https://pure-admin.github.io/pure-admin-doc/pages/introduction/" target="_blank"
>Pure Admin</el-link
>
</div>
<div>
Apache ECharts 一个基于 JavaScript 的开源可视化图表库。
<el-link type="primary" href="https://echarts.apache.org/examples/zh/index.html#chart-type-line" target="_blank"
>Echarts</el-link
>
</div>
<div>
Element Plus 基于 Vue 3,面向设计师和开发者的组件库。
<el-link type="primary" href="https://element-plus.org" target="_blank"
>Element Plus</el-link
>
</div>
</el-card>
</div>
</template>
<style scoped>
.el-link {
margin-right: 8px;
font-size: large;
}
.el-link .el-icon--right.el-icon {
vertical-align: text-bottom;
}
</style>
代码如下:src\views\help\help2.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
import noAccess from "@/assets/status/403.svg?component";
defineOptions({
name: "help2"
});
</script>
<template>
<div>
<el-page-header title="使用文档" content="版本控制" />
<el-card class="box-card">
<p>
v1.00 - 20240523
<p>
【首页】:vue前端打开文件夹读取csv文件路径给rust后端读取数据后返回json给vue前端显示;要求读取的csv文件为utf-8格式,否则中文处理会乱码或闪退;
</p>
<p>
【功能模块 - 功能1:数据表格】:vue前端打开文件夹读取csv到table显示;vue前端处理大数据量可能会有性能问题(可能是v-for渲染问题);
</p>
</p>
</el-card>
</div>
</template>
代码如下:src\views\help\help3.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
import noAccess from "@/assets/status/403.svg?component";
defineOptions({
name: "help3"
});
</script>
<template>
<div>
<el-page-header title="使用文档" content="软件说明" />
<el-card class="box-card">
<p>
作者
<p>
【软件框架】:【使用文档 - 使用技术】相关作者;
</p>
<p>
【二次开发】:dengchaohai;
</p>
</p>
</el-card>
</div>
</template>
代码如下:src-tauri\src\util.rs
use polars::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::{Result, Value};
use std::fs::File;
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug)]
struct Vecs {
path: String,
names: Vec<String>,
rows: Vec<Vec<String>>,
}
pub fn is_valid_float(input: String) -> String {
match input.parse::<f64>() {
Ok(_) => input.to_string(),
Err(_) => 0.to_string(),
}
}
pub fn read_csv_to_json(file_path: &str) -> Result<String> {
let df = CsvReader::new(File::open(file_path).expect("File not found"))
.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| is_valid_float(v.to_string()))
.collect::<Vec<_>>()
})
.collect();
let vecs = Vecs {
path: file_path.to_string(),
names: column_names,
rows: data,
};
let json = serde_json::to_string(&vecs).expect("Failed to serialize to JSON");
Ok(json)
}
代码如下:src-tauri\src\functions.rs
use polars::prelude::*;
use serde::de::value;
use serde::{Deserialize, Serialize};
use serde_json::{Result, Value};
use std::collections::HashMap;
use std::fs::File;
#[derive(Serialize, Deserialize, Debug)]
struct Json {
path: String,
headers: Vec<String>,
data: Vec<HashMap<String, String>>,
}
pub fn csv_to_json(file_path: &str) -> Result<String> {
let mut data: Vec<HashMap<String, String>> = Vec::new();
let file = File::open(file_path).expect("File not found");
let df = CsvReader::new(file)
.infer_schema(None)
.has_header(true)
.finish()
.expect("Failed to read CSV file");
let column_names: Vec<String> = df
.get_column_names()
.iter()
.map(|name| name.to_string())
.collect();
let num_rows = df.get_columns()[0].len();
for i in 0..num_rows {
let row = df.get_row(i).expect("Failed to get row").0;
let mut row_data: HashMap<String, String> = HashMap::new();
for i in 0..column_names.len() {
row_data.insert(column_names[i].clone(), row[i].to_string());
}
data.push(row_data);
}
let json = Json {
path: file_path.to_string(),
headers: column_names,
data: data,
};
let result = serde_json::to_string(&json).expect("Failed to serialize to JSON");
// println!("{}", result);
Ok(result)
}
代码如下:src-tauri\src\main.rs
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod functions;
mod util;
use encoding::{self, all::GB18030, DecoderTrap, EncoderTrap, Encoding};
use tauri::{CustomMenuItem, Menu, Submenu};
#[tauri::command]
fn greet(name: &str) -> String {
let json = util::read_csv_to_json(name);
json.unwrap()
}
#[tauri::command]
fn greet_table(name: &str) -> String {
let json = functions::csv_to_json(name);
json.unwrap()
}
fn main() {
let context = tauri::generate_context!();
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, greet_table])
.menu(Menu::new().add_submenu(Submenu::new(
"文件",
Menu::new().add_item(CustomMenuItem::new("close", "退出").accelerator("cmdOrControl+Q")),
)))
.on_menu_event(|event| match event.menu_item_id() {
"close" => {
event.window().close().unwrap();
}
_ => {}
})
.run(context)
.expect("error while running tauri application");
}
代码如下:src-tauri\Cargo.toml
或者通过pnpm add 包名
方式添加js包,cargo添加rust包则cd src-tauri
然后cargo add 包名
[package]
name = "tauri-pure-admin"
version = "5.6.0"
description = "tauri-pure-admin"
authors = ["pure-admin"]
license = "MIT"
repository = "https://github.com/pure-admin/tauri-pure-admin"
default-run = "tauri-pure-admin"
edition = "2021"
rust-version = "1.59"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.2.1", features = [] }
[dependencies]
csv = "1.1.1"
polars = { version = "0.33.2", features = ["lazy", "ndarray"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2.4", features = ["dialog-all", "shell-open"] }
encoding = "0.2"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = ["custom-protocol"]
# this feature is used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
代码如下:src\views\welcome\index.vue
<script setup lang="ts">
import { open } from "@tauri-apps/api/dialog";
import { onBeforeUnmount, onMounted, ref, watch, reactive } from "vue";
import { invoke } from "@tauri-apps/api/tauri";
import { EChartsOption, ECharts, init } from "echarts";
import AddCheckboxs from "@/components/ReMy/AddCheckboxs.vue";
import { Checkbox } from "@/components/ReMy/AddCheckboxs.vue";
import { json } from "stream/consumers";
import { message } from "@/utils/message";
defineOptions({
name: "Welcome"
});
window.alert = function (name) {
var iframe = document.createElement("IFRAME");
iframe.style.display = "none";
iframe.setAttribute("src", "data:text/plain,");
document.documentElement.appendChild(iframe);
window.frames[0].window.alert(name);
iframe.parentNode.removeChild(iframe);
};
// 初始化一组按钮,并定义其点击事件来更新图表
const checkboxs = ref<Checkbox[]>([]);
const handleclick = async () => {
alert(
"注意:\r\n1. CSV 文件必须为UTF-8编码(若非UTF-8编码可打开文件以UTF-8编码另存即可),否则可能出现中文乱码、闪退问题!\r\n2. CSV 文件非数值数据会替换为0!"
);
const selected = await open({
filters: [
{
name: "UTF-8编码CSV文件",
extensions: ["csv"]
}
]
});
if (selected === null) {
alert("请选择文件");
} else {
greet(selected);
}
};
const greetMsg = ref("");
let jsonRef;
async function greet(selected) {
let data: string = await invoke("greet", { name: selected });
jsonRef = JSON.parse(data);
let json = jsonRef;
var i;
for (i = 0; i < json["names"].length; i++) {
statedict[json["names"][i]] = false;
const cb: Checkbox = {
label: json["names"][i],
isChecked: false,
index: i,
handleCheckboxChange: ($event, label, isChecked) => {
let k: string = label;
let v: string = isChecked;
statedict[k] = v;
updatedict();
}
};
checkboxs.value.push(cb);
}
greetMsg.value = json["path"];
}
const statedict = reactive({});
type seriesDict = {
name: string;
type: string;
data: [];
};
function createArrayWithFill(length: number): string[] {
const arr = new Array(length).fill(0);
for (let i = 0; i < length; i++) {
arr[i] = (i + 1).toString();
}
return arr;
}
function updatedict() {
var series: seriesDict[] = [];
for (const [key, value] of Object.entries(statedict)) {
let k: string = key;
let v: boolean = value as boolean;
if (v) {
let index = jsonRef["names"].indexOf(k);
let seriesItem: seriesDict = {
name: k,
type: "line",
data: jsonRef["rows"][index]
};
series.push(seriesItem);
// console.log(`${k}: ${v}`);
}
}
option.xAxis.data = createArrayWithFill(series[0].data.length);
option.xAxis.name = "序号";
option.series = series;
option.legend.data = jsonRef["names"];
myChart.setOption(option, true);
}
interface Props {
width?: string;
height?: string;
option?: EChartsOption;
}
const props = withDefaults(defineProps<Props>(), {
width: "100%",
height: "85%",
option: () => ({
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross"
}
},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
name: "产品",
nameLocation: "middle",
nameGap: 30
},
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);
};
const screenWidth = ref(0);
const screenHeight = ref(0);
onMounted(() => {
// 获取屏幕分辨率
screenWidth.value = window.screen.width;
screenHeight.value = window.screen.height;
initChart();
window.addEventListener("resize", resizeChart);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resizeChart);
clearTimeout(timer);
timer = 0;
});
watch(
props.option,
() => {
initChart();
},
{
deep: true
}
);
const option = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross"
}
},
legend: {
data: []
},
dataZoom: {
type: "inside",
preventDefaultMouseMove: false
},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
name: "产品",
nameLocation: "middle",
nameGap: 30
},
yAxis: {
type: "value",
containLabel: true
},
series: [
{
name: "销量",
type: "bar",
data: [50, 20, 36, 10, 10, 50]
}
]
};
</script>
<template>
<div class="cssdiv">
<!-- 必须用一个div根节点页面跳转才不会空白 -->
<el-button type="success" @click="handleclick">加载数据</el-button>
<p>{{ greetMsg }}</p>
<AddCheckboxs :checkBoxs="checkboxs" />
<p
ref="myChartsRef"
:style="{ height: height, width: width }"
:option="option"
/>
</div>
</template>
<style>
.cssdiv {
height: 90%;
}
</style>
代码如下:src-tauri\tauri.conf.json
注意allowlist里面要启用dialog
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeDevCommand": "pnpm browser:dev",
"beforeBuildCommand": "pnpm browser:build",
"devPath": "http://localhost:8080",
"distDir": "../dist"
},
"package": {
"productName": "tauri-pure-admin",
"version": "../package.json"
},
"tauri": {
"windows": [
{
"fullscreen": false,
"maximized": true,
"height": 768,
"resizable": true,
"title": "数据处理中心",
"width": 1024
}
],
"bundle": {
"active": true,
"targets": ["dmg", "deb", "appimage", "msi"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"copyright": "Copyright © 2020-present, pure-admin",
"category": "DeveloperTool",
"identifier": "com.tauri.pure",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
},
"deb": {
"depends": []
},
"externalBin": [],
"longDescription": "",
"resources": [],
"shortDescription": ""
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"allowlist": {
"shell": {
"open": true
},
"dialog": {
"all": true,
"ask": true,
"confirm": true,
"message": true,
"open": true,
"save": true
}
}
}
}
三、效果展示
总结
以上就是今天要讲的内容,本文仅仅简单介绍了tauri-pure-admin的二次开发,而vue3提供了大量能使我们快速便捷地处理数据的函数和方法。