介绍:
技术栈:vue + ant design vue组件库
实现点击上传txt文件,将txt文件上传至ant design vue组件库的table表格上进行显示
- 第一个表格:用于展示文件以及对文件的显示操作
- 第二个表格:用于接收第一个表格的列数据
功能(配置项):
-
选择文件: 上传指定的File或Blob格式的文件展示在第一个表格
-
分列: 将文件按照指定的符号来进行分列,并且支持自定义符号进行分列。
-
预览行数: 可以控制文件的预览行数。
-
从第几行开始选择: 通过从第几行开始选择的配置,第一个表格的列将按照选择好的第几行开始展示在第二个表格的列。
-
下拉框: 点击第二个表格下拉框展示第一个表格的列,选择某列第一个表格的某列就会指定的行数显示在第二个表格的对应列
效果:
选择了从第1行开始选择:第二个表格下拉框选择第一行的列,就会从第一行的来展示
选择了从第2行开始选择:第二行下拉框选择第一行的列,就会从第一行的来展示
代码:
模板:
这段代码是一个用于渲染两个表格的 Vue.js 模板。以下是它的功能和意义的总结:
- 第一个
<a-table>
组件以表格形式显示来自文本文件的数据。它使用dataSource
属性提供数据,并使用columns
属性定义表格的列。禁用了分页,同时配置了指定高度的垂直滚动。表格的大小设置为小。 - 第二个
<a-table>
组件表示一个新表格。它使用newdataList
属性提供数据,并使用newcolumns
属性定义列。禁用了分页,启用了垂直滚动。新表格的大小也设置为小。locale
属性设置当表格没有数据时显示的空文本。 - 在第二个表格内部,代码使用
v-slot:headerCell
指令定义了一个插槽,用于自定义表头单元格。它检查列索引 (column.dataIndex
) 是否不等于 0(假设排除了第一列),将下拉框插入到除了第一列(行号)的其他列。
<template>
<div>
<!-- 解析一个文本文件的内容,并在表格中显示 -->
<a-table
:dataSource="dataSource"
:columns="columns"
:pagination="false"
:scroll="{ y: height }"
size="small"
></a-table>
<!-- 新表格 -->
<a-table
:dataSource="newdataList"
:columns="newcolumns"
:pagination="false"
:scroll="{ y: height }"
size="small"
:locale="{ emptyText: ' ', }" <!-- 设置当表格没有数据时显示的空文本 -->
>
<!-- 使用插槽的v-slot:headerCell,将下拉框插入到每一个表头上 -->
<template v-slot:headerCell="{ column }">
<template v-if="column.dataIndex !== 0 ">
{{ column.title }} <!-- 显示列的标题 -->
<a-select
v-model:value="selectInput[column.dataIndex - 1]"
size="small"
style="float: right;"
:options="options"
:field-names="{ label: 'title', value: 'dataIndex'}"
></a-select>
</template>
</template>
</a-table>
</div>
</template>
这段代码定义了组件的 props 属性。props 是用于父组件向子组件传递数据的一种机制。在这里,组件接受了五个 props:
file
: 用户传入的文件,类型可以是 File 对象或 Blob 对象。rows
: 用户想展示的行数,默认值为 30。splitTag
: 分割符,用户传入的字符串,用于分列,是必需的。height
: 表格的高度,默认值为 250。startRow
: 用户选择的开始行数,默认值为 “1”。
这些 props 用于组件的数据处理和显示。父组件可以通过这些 props 向子组件传递相应的值,实现根据用户的需求来制定表格。
<script>
export default {
props: {
// 用户传入的文件
file: {
type: [File, Blob]
},
// 用户想展示的行数
rows: {
type: Number,
default: 30,
},
// 分割符号,用于分列
splitTag: {
type: String,
required: true
},
// 表格高度
height: {
type: Number,
default: 250
},
// 用户选择的开始行数
startRow: {
type: String,
default: "1",
},
},
// xxxx
}
</script>
这段代码定义了组件的 data 数据。data 属性用于存储组件的状态和数据。在这里,定义了以下数据项:
selectInput
: 选择框的值,初始为空数组。value
: 一个空字符串,可能用于存储其他数据。dataSource
: 原始数据源,初始为空数组。columns
: 表格的列信息,初始为空数组。newDataSource
: 根据用户选择的开始行数得到的新数据源,初始为空数组。newdataList
: 新表格的数据源,初始为空数组。newcolumns
: 新表格的列信息,包含了14个列,每个列对象都有dataIndex
(数据索引)、title
(列标题)和width
(列宽度)等属性。options
: 下拉框的下拉列表,初始为空数组。
这些数据项在组件中进行处理和使用。根据需要对它们进行了修改和更新。
<script>
export default {
data() {
return {
selectInput: [], // 选择框
value: '',
dataSource: [], // 原始数据源
columns: [], // 表格列信息
newDataSource: [], // 新的数据源,根据用户选择的开始行数得到的数据
newdataList: [], // 新表格的数据源
newcolumns: [
{ dataIndex: 0, title: '行号', width: '60px', fixed: 'left'},
{ dataIndex: 1, title: 'one', width: '136px'},
{ dataIndex: 2, title: 'two', width: '136px'},
{ dataIndex: 3, title: 'three', width: '136px'},
{ dataIndex: 4, title: 'four', width: '136px'},
{ dataIndex: 5, title: 'five', width: '136px'},
{ dataIndex: 6, title: 'six', width: '136px'},
{ dataIndex: 7, title: 'seven', width: '136px'},
{ dataIndex: 8, title: 'eight', width: '136px'},
{ dataIndex: 9, title: 'nine', width: '136px'},
{ dataIndex: 10, title: 'ten', width: '136px'},
{ dataIndex: 11, title: 'eleven', width: '136px'},
{ dataIndex: 12, title: 'twelve', width: '136px'},
{ dataIndex: 13, title: 'thirteen', width: '136px'},
], // 新表格的列信息
options: [] // 下拉框的下拉列表
};
},
}
</script>
逻辑:
analysisTxt()
该代码用于解析用户提供的文本文件,实现了分片加载的功能。通过将文件分成小块进行读取和解析,可以避免一次性加载整个文件而导致的性能问题。代码中使用了FileReader
对象来读取文件内容,并根据偏移量和块大小获取文件块。每次读取块的内容后,将剩余部分和新读取的内容合并,然后按行进行分割。
在每次成功读取块内容后,会进行如下操作:
- 将文本内容按行分割,并保留最后一行为未完全读取的行。
- 根据用户选择的开始行数构建数据源和表格列信息。
- 将新读取的数据源添加到原有数据源中,并为每行添加行号。
- 根据用户想展示的行数对数据源进行截断,保留指定行数。
- 如果数据源不为空且列信息中没有行数列,则添加行数列。
- 更新新的数据源。
- 如果数据源长度小于用户想展示的行数并且文件未读完,则继续读取下一块内容
通过分片加载,该代码实现了逐步解析大型文本文件的能力,并且可以根据用户的选择生成对应的数据源和表格列信息。这样可以有效处理大量数据和大型文件,提高性能和用户体验。
<script>
export default {
methods: {
analysisTxt(offset = 0, leftover = "") {
if (!this.file) { // 如果文件为空,直接返回
return;
}
const reader = new FileReader(); // 创建FileReader对象,用于读取文件内容
const chunkSize = 1024 * 4; // 每次读取的块大小
const blob = this.file.slice(offset, offset + chunkSize); // 根据偏移量和块大小获取文件块(分片加载)
const hasSpace = this.splitTag.includes(' '); // 检查分割符中是否包含空格
const dynamicSplitTag = hasSpace ? new RegExp(this.splitTag.replace(/ /g, '\\s+(?=\\S)')) : new RegExp(this.splitTag); // 根据是否包含空格创建动态的分割符正则表达式,
reader.onload = (e) => { // 文件读取成功回调函数
var contents = (leftover + e.target.result).split(/\r?\n/); // 将剩余部分和新读取的内容合并,并按行分割文本
leftover = contents.pop(); // 保留最后一行为未完全读取的行
const rowData = contents
.filter(line => line.trim() !== "") // 过滤掉空行
.map(line => line.trim().split(dynamicSplitTag)); // 使用动态分割符将每行文本分割成数组
if (offset === 0) { // 如果是第一次读取,构建列信息
const maxColumns = rowData.reduce((max, row) => Math.max(max, row.length), 0); // 获取最大列数
this.columns = Array.from({ length: maxColumns }, (_, index) => ({
dataIndex: index,
title: `${index + 1} 列`
})); // 根据最大列数创建列信息数组,索引作为dataIndex,标题为`${索引+1} 列`
this.options = this.columns.slice(0); // 初始化下拉框的下拉列表为列信息数组
}
const dataSources = rowData.map((item) => { // 构建数据源
let _temp = {};
item.forEach((item2, index) => {
_temp[index] = item2;
});
return _temp;
});
// 将新读取的数据源添加到原有数据源中,并为每行添加行号
this.dataSource = [...this.dataSource, ...dataSources].map((item, index) => ({ rowNumber: index + 1, ...item }));
if (this.dataSource.length > this.rows) { // 如果数据源长度大于用户想展示的行数,则进行截断
this.dataSource = this.dataSource.slice(0, this.rows);
}
// 如果数据源不为空,且列信息中没有行数列,则添加
if (this.dataSource.length > 0 && !this.columns.find(col => col.dataIndex === 'rowNumber')) {
this.columns.unshift({
dataIndex: 'rowNumber',
title: '行号',
width: '60px'
});
}
this.updateNewDataSource(); // 更新新的数据源
// 如果数据源长度小于用户想展示的行数,并且文件未读完,则继续读取
if (this.dataSource.length < this.rows && offset + chunkSize < this.file.size) {
this.analysisTxt(offset + chunkSize, leftover);
}
};
reader.readAsText(blob, "UTF-8"); // 以文本格式读取文件块内容
},
}
}
</script>
下面该代码:解决当我们使用空格作为分列符号的时候,如果同时有多个空格则只转换一个空格来分列,解决了多个空格连在一起都会分列的问题
const hasSpace = this.splitTag.includes(' '); // 检查分割符中是否包含空格
const dynamicSplitTag = hasSpace ? new RegExp(this.splitTag.replace(/ /g, '\\s+(?=\\S)')) : newRegExp(this.splitTag); // 根据是否包含空格创建动态的分割符正则表达式,
下面两段代码解决了以下问题:
updateNewDataSource()
函数:该函数用于更新新的数据源,根据用户选择的开始行数,对原始数据进行切片,并添加行号属性。这样可以根据用户选择的开始行数生成新的数据源,用于展示在第二个表格中。deepCopy()
函数:这是一个辅助函数,用于实现对象的深拷贝。在updateNewDataSource()
函数中,我们需要对原始数据进行深拷贝,以确保新的数据源和原始数据源是完全独立的,如果不进行深拷贝,直接使用原始数据源,那么对表格数据的当用户输入的从第几行开始选择时也会更改第一个表格的行数,深拷贝保证了新的数据源是独立的,不受原始数据的影响。
重要的知识点:
- 数据源的切片和处理:通过切片和修改数据源,可以实现对表格展示的自定义控制,例如根据用户选择的开始行数生成新的数据源,并添加行号属性。
- 深拷贝:在处理对象和数组时,使用深拷贝可以创建它们的完全独立副本,避免对原始数据的修改产生意外影响。深拷贝常用于处理涉及嵌套对象或数组的情况。
<script>
// 深拷贝函数,用于创建对象的副本
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let copy;
if (Array.isArray(obj)) {
copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]);
}
} else {
copy = {};
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
copy[key] = deepCopy(obj[key]);
}
}
}
return copy;
}
export default {
methods: {
// 更新新的数据源
updateNewDataSource() {
// 使用深拷贝创建新数组
this.newDataSource = deepCopy(this.dataSource)
.slice(!this.startRow ? Number(this.startRow) : Number(this.startRow) - 1)
.map((item, index) => {
const newItem = { ...item }; // 创建新对象
newItem.rowNumber = index + 1;
return newItem;
});
}
}
}
</script>
这段代码实现了以下功能和解决了以下问题:
emitListTag()
方法:该方法用于更新列表标签,根据传入的newItem1(第一个表格的列)
和newindex(第二个表格的列)
参数,遍历newDataSource
数组,并在newdataList
对象中更新对应的属性值。通过将新的属性值添加到newdataList
中,可以实现对列表的标签扩展或修改。- 监听
selectInput
的变化:使用watch
属性监听selectInput
的变化,deep: true
表示进行深度监听。当selectInput
发生变化时,也就是当我们选择了下拉框之后,会执行handler
函数。在handler
函数中,遍历newValue
数组,调用emitListTag()
方法进行列表标签的更新。然后,通过$emit
将更新后的newValue
传递给父组件,以便在点击确定后传递给后端。
这段代码的实现主要涉及了对选择框的变化的监听以及对列表数据的更新。通过监听选择框的变化,可以实时更新列表的标签,从而实现对列表内容的扩展或修改。这样可以提供更多的交互和定制化功能,满足用户的需求。同时,通过将更新后的数据传递给父组件,可以实现与后端的数据交互。
<script>
export default {
methods: {
// 更新列表标签
emitListTag(newItem1, newindex) {
this.newDataSource.forEach((item, index) => {
const newItem = {
0: item.rowNumber, // 行号
...this.newdataList[index + 1], // 获取原有对象
[newindex + 1]: item[newItem1] // 添加新的属性值
};
this.newdataList[index + 1] = newItem; // 更新对象
});
},
},
watch: {
// 监听选择框的变化
selectInput: {
deep: true,
handler(newValue) {
newValue.forEach((item, index) => {
this.emitListTag(item, index);
});
// 将输入传递给父组件,用于点击确定后传递给后端
this.$emit('addselectInput', newValue);
}
}
}
}
</script>
完整代码
<template>
<div>
<!-- 解析txt文件内容,显示在表格中 -->
<a-table
:dataSource="dataSource"
:columns="columns"
:pagination="false"
:scroll="{ y: height }"
size="small"
></a-table>
<!-- 新表格 -->
<a-table
:dataSource="newdataList"
:columns="newcolumns"
:pagination="false"
:scroll="{ y: height }"
size="small"
:locale="{emptyText: ' ',}"
>
<template v-slot:headerCell="{ column }">
<template v-if="column.dataIndex !== 0 ">
{{ column.title }}
<a-select
v-model:value="selectInput[column.dataIndex - 1]"
size="small"
style="float: right;"
:options="options"
:field-names="{ label: 'title', value: 'dataIndex'}"
></a-select>
</template>
</template>
</a-table>
</div>
</template>
<script>
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let copy;
if (Array.isArray(obj)) {
copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]);
}
} else {
copy = {};
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
copy[key] = deepCopy(obj[key]);
}
}
}
return copy;
}
export default {
name: "AnalysisTxtContent",
data() {
return {
selectInput: [], //选择框
value: '',
dataSource: [], // 原始数据源
columns: [], // 表格列信息
newDataSource: [], // 新的数据源,根据用户选择的开始行数得到的数据
newdataList: [], // 新表格的数据源
newcolumns: [
{ dataIndex: 0, title: '行号', width: '60px', fixed: 'left'},
{ dataIndex: 1, title: 'one', width: '136px'},
{ dataIndex: 2, title: 'two', width: '136px'},
{ dataIndex: 3, title: 'three', width: '136px'},
{ dataIndex: 4, title: 'four', width: '136px'},
{ dataIndex: 5, title: 'five', width: '136px'},
{ dataIndex: 6, title: 'six', width: '136px'},
{ dataIndex: 7, title: 'seven', width: '136px'},
{ dataIndex: 8, title: 'eight', width: '136px'},
{ dataIndex: 9, title: 'nine', width: '136px'},
{ dataIndex: 10, title: 'ten', width: '136px'},
{ dataIndex: 11, title: 'eleven', width: '136px'},
{ dataIndex: 12, title: 'twelve', width: '136px'},
{ dataIndex: 13, title: 'thirteen', width: '136px'},
], // 新表格的列信息
options: []
};
},
props: {
file: { // 用户传入的文件
type: [File , Blob]
},
rows: { // 用户想展示的行数
type: Number,
default: 30,
},
splitTag: { // 分割符,用户传入,用于分列
type: String,
required: true
},
height: { // 表格高度
type:Number,
default: 250
},
startRow: { // 用户选择的开始行数
type: String,
default: "1",
},
},
methods: {
emitListTag(newItem1, newindex) {
this.newDataSource.forEach((item, index) => {
const newItem = {
0: item.rowNumber, //行号
...this.newdataList[index +1], // 获取原有对象
[newindex + 1]: item[newItem1] // 添加新的属性值
};
this.newdataList[index+1] = newItem; // 更新对象
});
},
analysisTxt(offset = 0, leftover = "") { // 解析文本文件
if (!this.file) {
return;
}
const reader = new FileReader();
const chunkSize = 1024 * 4; // 每次读取的块大小
const blob = this.file.slice(offset, offset + chunkSize);
// const splitTagRegex = new RegExp('('+this.splitTag+")+"); // 创建分割符正则
// 判断是否包含空格
const hasSpace = this.splitTag.includes(' ')
// 如果包含空格,替换为空格为 /\s+(?=\S)/
const dynamicSplitTag = hasSpace ? new RegExp(this.splitTag.replace(/ /g, '\\s+(?=\\S)')) : new RegExp(this.splitTag);
reader.onload = (e) => { // 文件读取成功回调
var contents = (leftover + e.target.result).split(/\r?\n/); // 按行分割文本
leftover = contents.pop(); // 保留最后一行为未完全读取的行
const rowData = contents
.filter(line => line.trim() !== "")
.map(line => line.trim().split(dynamicSplitTag)); // 使用 trim() 去除首尾空格
if (offset === 0) { // 如果是第一次读取,构建列信息
const maxColumns = rowData.reduce((max, row) => Math.max(max, row.length), 0);
this.columns = Array.from({length: maxColumns}, (_, index) => ({
dataIndex: index,
title: `${index + 1} 列`
}));
this.options = this.columns.slice(0)
}
const dataSources = rowData.map((item) => { // 构建数据源
let _temp = {};
item.forEach((item2, index) => {
_temp[index] = item2;
});
return _temp;
});
this.dataSource = [...this.dataSource, ...dataSources].map((item, index) => ({rowNumber: index + 1, ...item}));
if (this.dataSource.length > this.rows) { // 如果数据源长度大于用户想展示的行数,则进行截断
this.dataSource = this.dataSource.slice(0, this.rows);
}
if (this.dataSource.length > 0 && !this.columns.find(col => col.dataIndex === 'rowNumber')) { // 如果数据源不为空,且列信息中没有行数列,则添加
this.columns.unshift({
dataIndex: 'rowNumber',
title: '行号',
width: '60px'
});
}
this.updateNewDataSource(); // 更新新的数据源
};
reader.onloadend = (e) => { // 文件读取结束回调
if (e.target.readyState == FileReader.DONE) {
if (this.dataSource.length < this.rows && offset + chunkSize < this.file.size) { // 如果数据源长度小于用户想展示的行数,并且文件未读完,则继续读取
this.analysisTxt(offset + chunkSize, leftover);
}
}
};
reader.readAsText(blob, "UTF-8");
},
updateNewDataSource() {
this.newDataSource = deepCopy(this.dataSource) // 使用深拷贝创建新数组
.slice(!this.startRow ? Number(this.startRow) : Number(this.startRow) - 1)
.map((item, index) => {
const newItem = { ...item }; // 创建新对象
newItem.rowNumber = index + 1; // 修改属性值
return newItem; // 返回修改后的对象
});
}
},
watch: {
file() { // 当文件改变时,清空数据源并重新解析文本
this.dataSource = [];
this.newDataSource = [];
this.analysisTxt();
},
rows() { // 当用户想展示的行数改变时,清空数据源并重新解析文本
this.dataSource = [];
this.newDataSource = [];
this.analysisTxt();
},
splitTag() { // 当分割符改变时,清空数据源并重新解析文本
this.dataSource = [];
this.newDataSource = [];
this.analysisTxt();
},
startRow() { // 当用户选择的开始行数改变时,更新新的数据源
this.updateNewDataSource();
},
selectInput: {
deep: true,
handler(newValue) {
newValue.forEach((item,index )=> {
this.emitListTag(item,index)
})
// 将输入传递给父组件,用于点击确定传递给后端
this.$emit('addselectInput', newValue)
}
}
}
};
</script>