Vue.js + Electron 项目学习————Seaweedfs文件管理系统

359 阅读6分钟

Electron + Vue 初始化

Vue3项目初始化

全局变量为vue2的情况下使vue3.0项目可以在同一电脑上运行

创建一个Vue3文件夹 在vue3文件夹执行命令

npm install @vue/cli

修改vue3文件夹中node_modules/.bin中vue vue.cmd 的文件名称为vue3 vue3.cmd

修改环境变量:将\node_modules.bin地址配置到环境变量path

创建项目

vue3 create vue3-project

手动配置项目

vue create创建项目手动配置步骤_class-style component syntax-CSDN博客

vue项目+electron 初始化

vue项目中添加 electron 模块

vue add electron-builder

启动vue_electron项目

npm run electron:serve

vue项目设计

el-tree的学习(重点)

代码解析:

<el-tree ref="tree" :data="treeData" :expand-on-click-node="true" :props="defaultProps"
                        :filter-node-method="filterNode" @node-click="handleClick" :highlight-current="true"
                        node-key="id" :default-expanded-keys="[]">
                        <span slot-scope="{ node, data }" @mouseenter="handleMouseEnter(data)"
                            @mouseleave="handleMouseLeave(data)">
                            <template>
                                <i :class="{
                'el-icon-folder': !node.expanded,
                'el-icon-folder-opened': node.expanded,
                'el-icon-s-order': data.flag
            }" style="color: orange;" />
                                <span>{{ node.label }}</span>
                                <i v-if="selectedList.find(item => item.name === node.label)" class="el-icon-check" />
                                <el-button type="text" icon="el-icon-delete" class="delete-btn"
                                    @click="deleteNode(data)" v-show="data.showButtons">删除</el-button> <!-- 删除按钮 -->
                                <el-button type="text" icon="el-icon-edit" class="edit-btn" @click="editNode(data)"
                                    v-show="data.showButtons">编辑</el-button> <!-- 编辑按钮 -->
                            </template>
                        </span>
                    </el-tree>

使用 Element UI 的 el-tree 组件来展示树形数据

  • ref="tree": 这个属性是为了在组件中使用 $refs 来引用 el-tree 组件实例

  • :data="treeData": 使用 treeData 对象作为 el-tree 组件的数据源,展示树形结构的数据。

  • :expand-on-click-node="true": 当节点被点击时,自动展开或折叠节点。

  • :props="defaultProps": 通过 defaultProps 对象设置节点的默认属性配置,包括节点的文本、子节点等。

  • :filter-node-method="filterNode": 通过 filterNode 方法进行节点过滤。

  • @node-click="handleClick": 当节点被点击时触发 handleClick 方法。

  • :highlight-current="true": 高亮当前选中节点。

  • node-key="id": 指定节点数据中表示节点唯一标识的属性为 id。

  • :default-expanded-keys="[]": 设置默认展开的节点的 key 值,这里设置为空数组表示没有默认展开的节点。

el-tree 组件的模板部分

  • <span slot-scope="{ node, data }" @mouseenter="handleMouseEnter(data)" @mouseleave="handleMouseLeave(data)">: 使用了插槽(slot)来定义每个节点的内容,在这个插槽中可以访问到 nodedata 对象,分别表示节点的数据和节点对象。

  • <i :class="{'el-icon-folder': !node.expanded, 'el-icon-folder-opened': node.expanded, 'el-icon-s-order': data.flag}" style="color: orange;" />: 这段代码根据节点的展开状态和数据的 flag 属性来动态设置图标样式。

  • <span>{{ node.label }}</span>: 显示节点的文本内容。

  • <i v-if="selectedList.find(item => item.name === node.label)" class="el-icon-check" />: 根据条件判断是否显示选中图标。

  • <el-button type="text" icon="el-icon-delete" class="delete-btn" @click="deleteNode(data)" v-show="data.showButtons">删除</el-button>: 显示一个删除按钮,并且根据 data.showButtons 来控制按钮的显示与隐藏,点击按钮时调用 deleteNode 方法。

  • <el-button type="text" icon="el-icon-edit" class="edit-btn" @click="editNode(data)" v-show="data.showButtons">编辑</el-button>: 显示一个编辑按钮,并且根据 data.showButtons 来控制按钮的显示与隐藏,点击按钮时调用 editNode 方法。

目录数据的可视化

相关方法

1.鼠标移入和移出事件

        handleMouseEnter(data) {
            console.log(data, '110')
            this.$set(data, 'showButtons', true)
        },
        handleMouseLeave(data) {
            this.$set(data, 'showButtons', false)
        },

其中,this.$set 是vue提供的一个方法,用于在 Vue 实例中动态添加响应式属性。在 Vue 中,如果直接给对象添加新属性,Vue 无法检测到这个属性的变化,因此无法实现响应式更新。而使用 this.$set 方法可以解决这个问题,它会确保添加的属性是响应式的,这样当属性发生变化时,相关的界面会自动更新。

具体来说,this.$set(obj, 'propertyName', value) 方法接收三个参数:

  • 需要添加属性的对象 obj
  • 属性名 propertyName
  • 属性值 value

调用这个方法后,Vue 将会确保 obj.propertyName 是响应式的。

在你提供的代码中,handleMouseEnter 和 handleMouseLeave 方法使用了 this.$set 来给 data 对象动态添加了名为 showButtons 的属性,从而实现了当鼠标移入节点时显示按钮,移出节点时隐藏按钮的效果,并且确保了这个属性是响应式的,能够触发界面的自动更新

2.关键词筛选

:filter-node-method="filterNode"实现对目录树的筛选

设置v-model="searchVal"来绑定关键词

<div style="display: flex; justify-content: center; margin: 1rem;">
    <el-input v-model="searchVal" style="width: 20rem" placeholder="输入名称搜索" />
</div>

利用watch监视searchVal数据的变化情况,当数据发生变化时开启树节点的过滤方法

    watch: {
        // 监听输入的关键词,调用 Tree 实例的filter方法
        searchVal(val) {
            this.$refs.tree.filter(val);
        }
    },

在 Vue.js 中,watch 选项用于监听数据的变化,并在数据发生变化时执行相应的操作。

与computed相比,watch用于观察和响应数据的变化,computed用于基于响应式数据生成派生数据。

实际项目的关键词筛选(进阶版)

需求分析:需要设置两个筛选条件,一是用户根据选择器选择文件类别,二是输入关键字。进行联合查找

html写法:

<el-select v-model="searchVal" clearable placeholder="请选择文件类型" style="margin-right: 1rem;">
    <!-- clearable属性为可清空单选 -->
                        <el-option label="3Dtiles" value="tileset.json"></el-option>
                        <el-option label="Terrain" value="layer.json"></el-option>
                        <el-option label="Shapefile" value="shp"></el-option>
                        <el-option label="Geotif" value="tif"></el-option>
                        <el-option label="GeoTIF" value="TIF"></el-option>
                        <el-option label="GLB" value="glb"></el-option>
                        <el-option label="GeoJSON" value="json"></el-option>
                        <el-option label="CZML" value="czml"></el-option>
                        <el-option label="KML" value="kml"></el-option>
                        <el-option label="PDF" value="pdf"></el-option>
                        <el-option label="XLS" value="xls"></el-option>
                        <el-option label="XLSX" value="xlsx"></el-option>
                        <el-option label="GDB" value="gdb"></el-option>
                        <el-option label="LAS" value="las"></el-option>
                        <el-option label="LAZ" value="laz"></el-option>
                        <el-option label="CSV" value="csv"></el-option>
                        <!-- 添加其他文件类型选项 -->
</el-select>
<el-input v-model="filterText" style="width: 20rem" placeholder="输入关键词搜索" />

watch写法:

    watch: {
        // 监听输入的关键词,调用 Tree 实例的filter方法
        searchVal(val) {
            if(val!=''){
                this.$refs.tree.filter(val);
            }else{
                this.init()//如果为空则刷新目录树
            }
        },
        filterText(val) {
            const keywords = `${val}.${this.searchVal}`;
            this.$refs.tree.filter(keywords);
        }
    },

拼接一个${val}.${this.searchVal}的字符串作为关键词字符串组

filterNode方法写法:

filterNode(value, data) {
   if (!value) return true; // 如果没有关键词,则返回 true,表示显示所有节点
      const valArray = value.split('.'); // 将两个关键词分割成数组
      // 检查数据节点的 name 是否同时包含两个关键词
      return valArray.every(keyword => data.name.indexOf(keyword) !== -1);
        },
  • every() 是数组方法,用于检查数组中的每个元素是否均满足指定的条件。

  • keyword => data.name.indexOf(keyword) !== -1 是一个箭头函数,它接受数组中的每个元素作为参数,并返回一个布尔值,表示关键字是否存在于 data.name 属性中。具体操作如下:

  1. keyword 是箭头函数的参数,表示数组中的当前元素(即关键字)。
  2. data.name.indexOf(keyword) 用于查找关键字在 data.name 字符串中的位置。如果关键字存在,则返回其在字符串中的索引;如果不存在,则返回 -1。
  3. !== -1 用于检查 indexOf() 返回的索引值是否不等于 -1,即关键字是否存在于字符串中。

数组的其他方法总结

  • forEach(callback[, thisArg]):对数组中的每个元素执行提供的函数一次。没有返回值
            data.forEach(item => {
                if (item.parentId === parentId) {
                    let newItem = { ...item }; // 使用对象解构创建一个新对象,避免直接修改原始数据
                    newItem.children = this.buildTree(data, item.id);
                    result.push(newItem);
                }
            });
  • map(callback[, thisArg]):创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

  • filter(callback[, thisArg]):创建一个新数组,其中包含所有通过提供的函数实现的测试的元素。

  • some(callback[, thisArg]):检测数组中是否至少有一个元素满足提供的函数的条件。

  • find(callback[, thisArg]):返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。

3.目录树加载

    init(){
        axios.get(`/api/geoserver/catalog/getAll`)
        .then(res => {
            if (res.data.code === 200) {
                const { BigInt } = window;
                // 遍历 result 数组中的对象,将 数字很大的id 属性值转换为字符串
                const parsedResult = res.data.result.map(item => {
                    return {
                        ...item,
                        id:  BigInt(item.id).toString(),
                        parentId:BigInt(item.parentId).toString(),
                    };
                });
                console.log('转换结果',parsedResult)
                this.treeData = this.buildTree(parsedResult, '-1')
                console.log('目录树:',this.treeData)
                const currentData = this.findNodeById(this.treeData, this.parentId);
                console.log('当前节点', currentData)
                if (currentData) {
                    this.childrenList = currentData.children.map(child => child);
                } else {
                    console.log('当前节点为根节点');
                    this.childrenList = this.treeData;
                }
                this.tableData = this.childrenList;
                this.tableData.forEach(item => {
                    item.author = 'admin';
                });
                console.log("更新tableData:", this.tableData);
                this.getPageInfo();
                //  默认点击第一个节点
        //         this.$nextTick().then(() => {
        //             const firstNode = document.querySelector('.el-tree-node')
        //             firstNode.click();
        //   })
        
            } else {
                // 失败的提示
                this.$message.error(res.data.message);
            }
        })
    },
    //查找当前节点的逻辑改为递归方式,以便能够获取所有层级的子目录
    findNodeById(data, id) {
        for (const node of data) {
            if (node.id === id) {
                return node;
            } else if (node.children && node.children.length > 0) {
                const foundNode = this.findNodeById(node.children, id);
                if (foundNode) {
                    return foundNode;
                }
            }
        }
        return null;
    }
     buildTree(data, parentId) {
        let result = [];
        data.forEach(item => {
            if (item.parentId === parentId) {
                let newItem = { ...item }; // 使用对象解构创建一个新对象,避免直接修改原始数据
                newItem.children = this.buildTree(data, item.id);
                result.push(newItem);
            }
        });
        return result;
    },   
    

代码解析:

  1. 使用BigInt对象将返回数据中的 id 和 parentId 转换为字符串类型,以防止数字过大导致精度丢失。接着,将处理后的数据 parsedResult 传入 buildTree 函数,构建出树形结构的数据,并将结果存储在 treeData 中。
  2. 使用 findNodeById 函数来根据 parentId 查找当前节点,并将其存储在 currentData 变量中。如果找到了当前节点,则将其子节点存储在 childrenList 中,否则将整个树形结构数据存储在 childrenList 中
  3. 构建树形数据结构的函数 buildTree,它接收一个数组 data 和一个父节点的 parentId 作为参数,然后递归地将数组中的数据转换为树形结构,创建一个新的对象 newItem,通过对象解构 ...item 将当前元素的属性复制到新对象中,以避免直接修改原始数据

效果展示

1711716400207.png

样式修改

.el-tree-node__content {
    height: 2.5rem;
}

扩大树状节点的宽度

4.添加目录

1.检查是否在已存在节点上新建

handleNewDirectroy() {
            if (this.clickNode) {
                console.log("新建目录", this.clickNode)
                this.directroyForm.name="";
                if (this.clickNode.flag == true) {
                    this.$message.error('该文件节点不能新建目录');
                } else {
                    this.newDialogVisible = true;
                }
            } else {
                this.newDialogVisible = true;
            }
        },

2.调用新建目录/文件的接口

        handleNew(flag) {
            if (this.clickNode) {
                this.parentId = this.clickNode.id
            } else {
                this.parentId = '-1'
            }
            axios.get('/api/geoserver/catalog/save', {
                params: {
                    name: this.directroyForm.name,
                    flag: flag,
                    parentId: this.parentId
                }
            })
                .then(async(res) => {
                    if (res.data.code === 200) {
                        console.log(res.data.result.id)
                        await this.init()
                        if(flag==0){
                            this.$message.success("目录新建成功");
                        }else if(flag==1){
                            console.log("文件上传成功")
                        }
                        
                    } else {
                        // 失败的提示
                        this.$message.error(res.data.message);
                    }
                })
                .finally(() => {
                    this.newDialogVisible = false;
                });
        },

await this.init()表示每次新建目录后都要刷新目录树一遍

5.上传文件

1.检查是否在目录节点上上传文件

        handleLoadData() {
            if (this.clickNode) {
                console.log("上传文件", this.clickNode)
                if (this.clickNode.flag == true) {
                    this.$message.error('该节点不能上传文件');
                } else {
                    this.handleLoadFile()
                }
            } 
            else {
                this.$message.error('请选择一个目录上传文件');
            }
        },

2.electron的文件传递功能

需要引入依赖

const { remote } = require('electron');
const { dialog } = remote;
const fs = remote.require('fs');

Electron 是一个用于构建跨平台桌面应用程序的框架,它使用了 Node.js 和 Chromium 技术

remote 对象允许渲染进程(如网页)访问主进程中的模块,从而执行一些原本只能在主进程中执行的操作。

dialog 对象允许渲染进程显示原生操作系统对话框,例如打开文件对话框、保存文件对话框等。

const fs = remote.require('fs');

这一行使用 remote.require() 方法从主进程中加载 fs 模块。 fs 模块是 Node.js 中用于处理文件系统的模块

注意: 在渲染进程中直接使用 require('fs') 是不被允许的,因为渲染进程无法直接访问文件系统等敏感资源。通过 remote.require 方法,可以安全地在渲染进程中加载主进程的模块,以此来访问文件系统等功能

handleLoadFile(){
            dialog.showOpenDialog({
                title: '上传文件', // 将标题改为“上传文件”
                buttonLabel: '上传', // 将按钮改为“上传”
                properties: ['openFile','multiSelections'] // 允许多选文件
            }).then(async result => {
                if (!result.canceled) {
                    const filePaths = result.filePaths; // 注意这里是一个数组,包含选中的所有文件路径
                    await this.readFileAndUpload(filePaths); 
                    if(this.uploadSuccess==true){
                        for (const filePath of filePaths) {
                        this.directroyForm.name = filePath.substring(filePath.lastIndexOf('\\') + 1); // 这里可能需要修改,取决于你想如何处理多个文件的文件名
                        this.handleNew(1);
                        console.log("文件路径",filePath)
                    }
                    }
                }
            });
        },

properties: ['openFile','multiSelections']设置打开文件对话框的属性,允许用户选择多个文件并且可以打开文件

const filePaths = result.filePaths;获取用户选择的文件路径,将其存储在 filePaths 数组中。

3.利用post请求的form-data方式读取文件传递给后端

async readFileAndUpload(filePaths) {
            try {
                const formData = new FormData();
                formData.append('name',this.clickNode.name);
                for (const filePath of filePaths) {
                    // 使用字符串操作函数截取文件名
                    this.fileName = filePath.substring(filePath.lastIndexOf('\\') + 1);
                    console.log(this.fileName)
                    // 读取文件内容
                    const fileType=this.getFileType(this.fileName)
                    if(fileType!=""){
                        this.fileType=fileType;
                      }
                    const fileData = fs.readFileSync(filePath);
                    console.log(fileData)
                    // 创建 Blob 对象
                    const blob = new Blob([fileData]);
                    console.log(blob)
                    formData.append('file',blob,this.fileName);
                }
                formData.append('fileType',this.fileType);
                formData.append('userDescription','');
                console.log(formData.get("fileType"))
                console.log(formData.get("file"))
                const response = await axios.post('/api/geoserver/seaweed/uploadOneStep', formData, {
                    headers: {
                        "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryn8D9asOnAnEU4Js0"
                    }
                });
                // 检查响应状态码
                if (response.status === 200) {
                    const responseData = response.data; 
                    console.log('请求结果', responseData);
                    if(response.data.success==true){
                        this.uploadSuccess=true;
                        this.$message.success("文件上传成功");
                    }else{
                        this.uploadSuccess=false;
                        this.$message.error("文件上传失败");
                    }
                } else {
                    this.uploadSuccess=false;
                    console.error('请求失败:', response.status);
                    this.$message.error("文件上传失败");
                }
            } catch (error) {
                this.uploadSuccess=false;
                console.error('发生错误:', error);
                this.$message.error("文件上传失败");
            }
        },

代码解析:

  • 只有post请求才可以实现本地文件的实际传输,文件是以二进制的blob类型进行传输的

  • const formData = new FormData();:创建一个 FormData 对象,用于将文件数据以表单形式发送到服务器

  • const fileData = fs.readFileSync(filePath):使用 Node.js 的 fs 模块同步地读取文件内容,并将内容存储在 fileData 变量中。

  • const blob = new Blob([fileData]);:使用文件数据创建一个 Blob 对象,Blob 是表示二进制数据的一种标准化形式。

  • formData.append('file', blob, this.fileName);:向表单数据中添加一个名为 'file' 的字段,值为前面创建的 Blob 对象,并指定文件名为 this.fileName。

  • const response = await axios.post('/api/geoserver/seaweed/uploadOneStep', formData, { ... });:使用 axios 库发送 POST 请求将表单数据上传到服务器的指定接口地址 /api/geoserver/seaweed/uploadOneStep。请求使用 multipart/form-data 格式,并附带自定义的请求头。