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提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值