文件展开-webkitRelativePath
发表于:2024-03-21 |

前言

今天带大家一起学习一下webkitRelativePath这个api

兼容性

这个api对于浏览器要求还是比较高的,大家可以去can i use网站看看兼容性。
我这里贴一张图
兼容性

常规使用

我写一个最普通的上传,我们只需要在input上添加webkitdirectory属性和multiple属性即可

代码

这里我用vue3的代码写了个简单的逻辑

1
2
3
4
5
6
7
8
9
10
11
<template>
<input type="file" webkitdirectory multiple @change="handleUploadChange" />
</template>
<script setup>
import { ref } from "vue";
const handleUploadChange=(event)=>{
let files = event.target.files;
for (let i = 0; i < files.length; i++) {
console.log(files[i])
}
}

文件目录

我新建了一个文件,目录结构差不多是这样的,后续我也将一直使用这个目录

测试效果

此时,效果如下

他就可以得到我们要的file格式

优化显示

代码

我此时就有了个想法,优化一下我的列表显示,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<template>
<t-space>
<t-button @click="handleUploadFile">上传文件</t-button>
<t-button @click="handleUploadFolder">上传文件夹</t-button>
</t-space>
<input type="file" style="width:0" webkitdirectory multiple @change="handleUploadChange" ref="folderRef" v-show="false" />
<input type="file" style="width:0" @change="handleUploadChange" ref="fileRef" v-show="false" />
<t-table :columns="tableColumns" :data="fileList">
<!-- size栏 -->
<template #size="{row}">
{{getFileSize(row.size)}}
</template>
<!-- 操作栏 -->
<template #action="{rowIndex}">
<t-button @click="handleDeleteFile(rowIndex)" size="small" theme="danger">删除</t-button>
</template>
</t-table>
</template>
<script setup>
import {ref} from "vue";

const folderRef = ref(null);
// 文件值改变
const handleUploadChange=(event)=>{
let files = event.target.files;
for (let i = 0; i < files.length; i++) {
Object.assign(files[i],{time:new Date().toLocaleString()})
fileList.value.push(files[i])
}
}
// 上传文件夹
const handleUploadFolder=()=>{
folderRef.value.click();
}

const fileRef = ref(null);

// 上传文件
const handleUploadFile=()=>{
fileRef.value.click();
}

// 展示的文件列表
const fileList=ref([]);

// 删除某个文件
const handleDeleteFile=(rowIndex)=>{
fileList.value.splice(rowIndex,1);
}

// 列表信息
const tableColumns=[
{
title: "文件名",
colKey: "name",
width: 200,
},
{
title: "大小",
colKey:'size',
width: 100,
},
{
title: "上传时间",
colKey:'time',
width: 200,
},
{
title: "操作",
colKey:'action',
width: 100,
},
]

// 得到文件大小
const getFileSize = (size) => {
let initNum=0;
const unit=[
'Bytes',
'KB',
'MB',
'GB',
'TB'
]
const returnValFunc=()=>{
return size.toFixed(2)+' '+unit[initNum];
}
if(size<=1024){
return returnValFunc();
}
while(size>1024){
size/=1024;
initNum++;
}
return returnValFunc();
}
</script>

这里有点细节,首先是我将input设置的宽度为0了,这样他就不会显示了,然后点击按钮的时候调用file类型input的click事件,就可以调用我们的选取文件弹窗,文件大小我就用了简单的逻辑,如果是不到1024的那就用Bytes,如果大于1024的那就先根据1024的进制来计算大小,然后那个时间我尝试过{...}的方式进行对象合并,发现会有错误,于是就使用了Object.assign来进行对象合并

效果展示

此时展示就是这样
效果展示

实现拖拽上传

禁用默认行为

修改一下table的样式,然后加个ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<t-space>
<t-button @click="handleUploadFile">上传文件</t-button>
<t-button @click="handleUploadFolder">上传文件夹</t-button>
</t-space>
<input type="file" style="width:0" webkitdirectory multiple @change="handleUploadChange" ref="folderRef" v-show="false" />
<input type="file" style="width:0" @change="handleUploadChange" ref="fileRef" v-show="false" />
<t-table
:columns="tableColumns"
:data="fileList"
@drop="handleDrop"
@dragenter="handleDragenter"
ref="tableRef"
>
<!-- 自定义没有内容的情况 -->
<template #empty>
<p v-if="dragActive">释放鼠标</p>
<p v-else>将 <span class="font-bold">文件</span> 或 <span class="font-bold">文件夹</span> 拖拽 到此处上传</p>
</template>
<!-- size栏 -->
<template #size="{row}">
{{getFileSize(row.size)}}
</template>
<!-- 操作栏 -->
<template #action="{rowIndex}">
<t-button @click="handleDeleteFile(rowIndex)" size="small" theme="danger">删除</t-button>
</template>
</t-table>
</template>

js中添加默认行为禁用,避免拖拽的时候文件进行下载

1
2
3
4
5
6
7
8
9
10
const tableRef=ref(null);
// 阻止默认行为
const handlePreventDefault=()=>{
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event=>{
tableRef.value.$el.addEventListener(event,preventDefault,false);
})
}
onMounted(()=>{
handlePreventDefault();
})

处理普通文件

这里我将for循环添加文件的代码单独封装成了一个方法handleFileAdd,这样在选中内容和拖拽内容的时候都可以进行同一段代码

1
2
3
4
5
6
7
8
9
10
11
12
// 是否在拖拽
const dragActive=ref(false);
// 鼠标放下
const handleDrop=(e)=>{
dragActive.value=false;
let files = e.dataTransfer.files;
handleFileAdd(files);
}
// 鼠标进入
const handleDragenter=()=>{
dragActive.value=true;
}

此时普通文件拖拽的效果

处理文件夹拖拽

直接使用的bug

此时如果我们直接拖拽文件夹,可以看到效果,emm,完全没用
直接拖拽文件夹

使用webkitGetAsEntry判断拖拽类型

这里,我使用了这个api来判断拖拽的类型
地址:https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransferItem/webkitGetAsEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 处理拖拽文件(夹)添加
const handleFileFolderAdd=(items)=>{
for(let j=0;j<items.length;j++){
const item=items[j];
const fileEntry=item.webkitGetAsEntry()
// 如果是文件夹类型
if(fileEntry.isDirectory){

}
// 如果是文件类型
else if(fileEntry.isFile){

}
}
}
// 鼠标放下
const handleDrop=(e)=>{
dragActive.value=false;
let items = e.dataTransfer.items;
handleFileFolderAdd(items);
}

处理文件类型的

我们可以通过文档的file方法,知道这个fileEntry可以进行文件格式转化
地址:https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileEntry

1
2
3
4
5
6
// 如果是文件类型
else if(fileEntry.isFile){
fileEntry.file((file)=>{
handleFileAdd([file]);
})
}

因此我们处理文件就可以这样简单的完成,效果如下

处理文件夹类型的

我们可以通过createReaderreadEntries来实现文件夹的读取:
地址:https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry/createReader
地址:https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 获取文件夹下文件的entries
const readEntriesAsync = (reader) =>
new Promise((resolve) => {
reader.readEntries((results) => {
const cacheRes = results.map((item) => ({
entry: item,
}));
resolve(cacheRes);
});
});

// 处理拖拽文件(夹)添加
const handleFileFolderAdd=(items)=>{
for(let j=0;j<items.length;j++){
const item=items[j];
const fileEntry=item.webkitGetAsEntry();
// 如果是文件夹类型
if(fileEntry.isDirectory){
// 文件夹类型,递归处理
const reader = fileEntry.createReader();
const readEntries = async (reader) => {
const results = await readEntriesAsync(reader);
for(let i=0;i<results.length;i++){
const curItem=results[i];
const curFileEntry=curItem.entry;
if(curFileEntry.isFile){
curFileEntry.file((file)=>{
handleFileAdd([file]);
})
}
else{
readEntries(curFileEntry.createReader());
}
}
};
readEntries(reader);
}
// 如果是文件类型
else if(fileEntry.isFile){
fileEntry.file((file)=>{
handleFileAdd([file]);
})
}
}
}

效果

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
<template>
<t-space>
<t-button @click="handleUploadFile">上传文件</t-button>
<t-button @click="handleUploadFolder">上传文件夹</t-button>
</t-space>
<input type="file" style="width:0" webkitdirectory multiple @change="handleUploadChange" ref="folderRef" v-show="false" />
<input type="file" style="width:0" @change="handleUploadChange" ref="fileRef" v-show="false" />
<t-table
:columns="tableColumns"
:data="fileList"
@drop="handleDrop"
@dragenter="handleDragenter"
ref="tableRef"
>
<!-- 自定义没有内容的情况 -->
<template #empty>
<p v-if="dragActive">释放鼠标</p>
<p v-else>将 <span class="font-bold">文件</span> 或 <span class="font-bold">文件夹</span> 拖拽 到此处上传</p>
</template>
<!-- size栏 -->
<template #size="{row}">
{{getFileSize(row.size)}}
</template>
<!-- 操作栏 -->
<template #action="{rowIndex}">
<t-button @click="handleDeleteFile(rowIndex)" size="small" theme="danger">删除</t-button>
</template>
</t-table>
</template>
<script setup>
import {onMounted, ref} from "vue";

const folderRef = ref(null);
// 获取文件夹下文件的entries
const readEntriesAsync = (reader) =>
new Promise((resolve) => {
reader.readEntries((results) => {
const cacheRes = results.map((item) => ({
entry: item,
}));
resolve(cacheRes);
});
});

// 处理拖拽文件(夹)添加
const handleFileFolderAdd=(items)=>{
for(let j=0;j<items.length;j++){
const item=items[j];
const fileEntry=item.webkitGetAsEntry();
// 如果是文件夹类型
if(fileEntry.isDirectory){
// 文件夹类型,递归处理
const reader = fileEntry.createReader();
const readEntries = async (reader) => {
const results = await readEntriesAsync(reader);
for(let i=0;i<results.length;i++){
const curItem=results[i];
const curFileEntry=curItem.entry;
if(curFileEntry.isFile){
curFileEntry.file((file)=>{
handleFileAdd([file]);
})
}
else{
readEntries(curFileEntry.createReader());
}
}
};
readEntries(reader);
}
// 如果是文件类型
else if(fileEntry.isFile){
fileEntry.file((file)=>{
handleFileAdd([file]);
})
}
}
}
// 处理文件添加
const handleFileAdd=(files)=>{
for(let i=0;i<files.length;i++){
const file=files[i];
Object.assign(file,{time:new Date().toLocaleString()})
fileList.value.push(file)
}
console.log(fileList.value,'fileList')
}
// 文件值改变
const handleUploadChange=(event)=>{
let files = event.target.files;
handleFileAdd(files);
}
// 上传文件夹
const handleUploadFolder=()=>{
folderRef.value.click();
}

const fileRef = ref(null);

// 上传文件
const handleUploadFile=()=>{
fileRef.value.click();
}

// 展示的文件列表
const fileList=ref([]);

// 删除某个文件
const handleDeleteFile=(rowIndex)=>{
fileList.value.splice(rowIndex,1);
}

// 列表信息
const tableColumns=[
{
title: "文件名",
colKey: "name",
width: 200,
},
{
title: "大小",
colKey:'size',
width: 100,
},
{
title: "上传时间",
colKey:'time',
width: 200,
},
{
title: "操作",
colKey:'action',
width: 100,
},
]

// 得到文件大小
const getFileSize = (size) => {
let initNum=0;
const unit=[
'Bytes',
'KB',
'MB',
'GB',
'TB'
]
const returnValFunc=()=>{
return size.toFixed(2)+' '+unit[initNum];
}
if(size<=1000){
return returnValFunc();
}
size/=1000;
while(size>1024){
size/=1024;
initNum++;
}
return returnValFunc();
}


const tableRef=ref(null);
// 阻止默认行为
const handlePreventDefault=()=>{
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event=>{
tableRef.value.$el.addEventListener(event,preventDefault,false);
})
}
onMounted(()=>{
handlePreventDefault();
})

const preventDefault=(e)=>{
e.preventDefault();
e.stopPropagation();
}

// 是否在拖拽
const dragActive=ref(false);
// 鼠标放下
const handleDrop=(e)=>{
dragActive.value=false;
let items = e.dataTransfer.items;
handleFileFolderAdd(items);
}
// 鼠标进入
const handleDragenter=()=>{
dragActive.value=true;
}
</script>
<style scoped>
.font-bold{
font-weight: bold;
}
</style>

结语

这样一个简单的上传文件夹展开内容的功能就实现了,大家也可以根据这个逻辑继续完善,比如实现树形展示,文件夹一层嵌套一层展示,这样功能就高级多了。更多内容,敬请期待~

上一篇:
【Python学习】01-一篇文章帮你搞定Python基础
下一篇:
【JAVA学习】06-跟着黑马程序员课程敲全栈项目(三)