添加 前端 OSS文件管理

This commit is contained in:
2024-10-05 01:32:06 +08:00
parent 9adcc812a3
commit 840b427a7e
4 changed files with 321 additions and 2 deletions

View File

@@ -33,6 +33,8 @@ declare module 'vue' {
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElText: typeof import('element-plus/es')['ElText']
ElUpload: typeof import('element-plus/es')['ElUpload']
FileOnline: typeof import('./src/components/Console/FileOnline.vue')['default']
Footer: typeof import('./src/components/Common/Footer.vue')['default']
Header: typeof import('./src/components/Common/Header.vue')['default']
Resources: typeof import('./src/components/Console/Resources.vue')['default']

View File

@@ -22,10 +22,12 @@
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
"@types/ali-oss": "^6.16.11",
"@types/md5": "^2.3.5",
"@types/node": "^20.12.5",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/tsconfig": "^0.5.1",
"ali-oss": "^6.21.0",
"autoprefixer": "^10.4.20",
"element-plus": "^2.7.3",
"npm": "^10.8.3",

View File

@@ -0,0 +1,308 @@
<script setup lang="ts">
import { request } from '@/lib/request';
import { Refresh, Back, House, UploadFilled } from '@element-plus/icons-vue';
import OSS, { type PutObjectOptions } from 'ali-oss'
import { ElMessage, ElMessageBox, ElSubMenu, type UploadFile, type UploadFiles } from 'element-plus';
import { ref, onMounted, reactive } from 'vue';
let OSSClient: OSS;
onMounted(async () => {
// 请求sts token初始化OSSClient
let sts_token_res: any = await request.get('console/ossToken');
if (sts_token_res.code == 0) {
// 请求成功
sts_token_res = sts_token_res.data;
OSSClient = new OSS({
region: sts_token_res.OSSRegion,
accessKeyId: sts_token_res.AccessKeyId,
accessKeySecret: sts_token_res.AccessKeySecret,
stsToken: sts_token_res.SecurityToken,
refreshSTSTokenInterval: sts_token_res.ExpirationSec * 1000,
bucket: sts_token_res.Bucket,
refreshSTSToken: async () => {
let sts_token_res: any = await request.get('console/ossToken');
sts_token_res = sts_token_res.data;
return {
accessKeyId: sts_token_res.AccessKeyId,
accessKeySecret: sts_token_res.AccessKeySecret,
stsToken: sts_token_res.SecurityToken,
}
},
})
} else {
throw new Error('获取OSS Token失败')
}
await loadFullFileList();
loadFileListShow();
})
// 当前目录
let prefix = ref('personal-web/');
// 完整的文件列表
const fileList: any[] = reactive([])
// 加载完整的文件列表
const loadFullFileList = async () => {
try {
// result.isTruncated ===> { marker: result.nextMarker } 用于后续实现翻页
const res = (await OSSClient.list({ "prefix": "personal-web", "max-keys": 900 }, {})).objects;
for (let i of res) {
// 初始化处理添加dir字段
(i as any).dir = (i.name as string).endsWith('/');
}
fileList.splice(0, fileList.length);
fileList.push(...res);
ElMessage.success('文件列表加载完成')
console.log(fileList)
} catch (error) {
ElMessage.error('文件列表加载失败')
}
}
// 总文件列表
for (let item of fileList) {
item.dir = item.name.endsWith('/')
}
// 文件列表可见数据
const fileListShow: any[] = reactive([]);
// 重置文件列表中,在列表中可见的数据
const loadFileListShow = () => {
fileListShow.splice(0, fileListShow.length);
const dirLength = prefix.value.indexOf('/') != -1 ? prefix.value.split('/').length - 1 : 0;
fileList.forEach((item: any) => {
const itemName: string = item.name;
// 加入当前目录中的目录
if (item.dir && itemName.startsWith(prefix.value) && itemName.split('/').length - 2 == dirLength)
fileListShow.push(item)
});
fileList.forEach((item: any) => {
const itemName: string = item.name;
// 加入当前目录中的文件
if (!item.dir && itemName.startsWith(prefix.value) && itemName.split('/').length - 1 == dirLength)
fileListShow.push(item)
});
}
// 文件被单击
const fileClick = (row: any, column: any, event: Event) => {
if (!row.dir || column.label !== '文件名')
return;
prefix.value = row.name;
loadFileListShow();
}
// 返回上级目录
const backtoLastDir = () => {
const prefixArr = prefix.value.split('/')
prefixArr.pop()
prefixArr.pop()
prefix.value = prefixArr.join('/') + (prefixArr.length > 0 ? '/' : '');
loadFileListShow();
}
// 文件列表中文件名格式化
const fileListTableNameFormatter = (row: any) => {
return (row.name as string).substring(prefix.value.length);
}
// 文件列表中上次修改时间格式化
const fileListTableLastModifiedFormatter = (row: any) => {
return row.dir ? "" : new Date(row.lastModified).toLocaleString()
}
// 文件列表中文件大小格式化
function fileListTableSizeFormatter(row: any) {
function formatterFileSize(num: number) {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
for (let i = 0; i < units.length; i++) {
if (num < 1000)
return num.toFixed(3) + units[i];
num /= 1024;
}
}
return row.dir ? "" : formatterFileSize(row.size)
}
// 退回到层级为index的某个目录
const Dirto = (index: number) => {
const prefixArr = prefix.value.split('/');
prefixArr.splice(index, prefixArr.length);
prefix.value = prefixArr.join('/') + (prefixArr.length > 0 ? '/' : '');
loadFileListShow();
}
// 删除选中的一些文件
const deleteChosenFile = () => {
}
// 文件选中状态被切换
const fileListSelectionChange = (newSelection: any[]) => {
console.log(newSelection)
}
// 长传文件的列表
let uploadFiles: UploadFiles;
const onUploadFileChange = (_uploadFile: UploadFile, _uploadFiles: UploadFiles) => {
uploadFiles = _uploadFiles;
}
// 上传文件
const uploadFile = async () => {
if (uploadFiles.length < 1) {
return ElMessage.error('请选择需要上传的文件')
}
// console.log(uploadFiles)
// console.log(prefix.value)
isUploading.value = true;
let uploadSuccessCount = 0;
for (let i of uploadFiles) {
if (i.status == 'success' || i.status == 'uploading')
continue;
try {
i.status = 'uploading';
// 采用分片上传方式,以获取上传进度
const name = prefix.value + i.raw!.name;
const options: any = {
// 获取分片上传进度、断点和返回值。
progress: (p: any, cpt: any, res: any) => {
// console.log(p, cpt, res);
i.percentage = Math.floor(p * 100);
if (p == 1) {
i.status = 'success';
uploadSuccessCount++;
}
},
// 设置并发上传的分片数量。
parallel: 4,
// 设置分片大小。默认值为256 KB最小值为100 KB。
partSize: 1024 * 256,
}
await OSSClient.multipartUpload(name, i.raw, options);
} catch (error) {
i.status = 'fail';
}
}
ElMessage.success(`${uploadSuccessCount} 个文件上传完成`);
isUploading.value = false;
await loadFullFileList();
loadFileListShow();
}
// 文件列表操作:删除某个文件
const fileListHandleDelete = async (row: any) => {
ElMessageBox.confirm(
'是否要删除该文件?',
'警告',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
await OSSClient.delete(row.name);
ElMessage.success('删除完成');
await loadFullFileList();
loadFileListShow();
} catch (error) {
ElMessage.error('删除失败');
}
})
}
// 文件列表操作:下载某个文件
const fileListHandleDownload = async (row: any) => {
try {
const res = await fetch(row.url);
if(!res.ok){
return ElMessage.error('请求失败')
}
ElMessage.info('开始下载')
const blob = await res.blob();
const urlObj = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = urlObj;
a.download = row.name.split('/').pop()
a.click();
a.remove();
window.URL.revokeObjectURL(urlObj);
} catch (error) {
console.log(error)
ElMessage.error('下载失败')
}
}
// 文件列表操作:重命名某个文件
const fileListHandleRename = async (row: any) => {
ElMessageBox.prompt('请输入新的文件名', '提示', {
confirmButtonText: '提交',
cancelButtonText: '取消',
inputValue: row.name
}).then(async ({ value }) => {
try {
await OSSClient.copy(value, row.name);
await OSSClient.delete(row.name);
ElMessage.success('重命名成功');
await loadFullFileList()
loadFileListShow();
} catch (error) {
ElMessage.error('重命名失败');
}
}).catch(() => {
// 已取消
})
}
const dialogUploadFileShow = ref(false);
const isUploading = ref(false);
</script>
<template>
<div class="container w-full">
<div class="flex items-center p-[10px]">
<el-button class="mr-[-15px]" @click="() => { loadFullFileList(); loadFileListShow(); }" circle link>
<el-icon>
<Refresh />
</el-icon>
</el-button>
<el-button @click="backtoLastDir" circle link>
<el-icon>
<Back />
</el-icon>
</el-button>
<el-text>当前目录</el-text>
<el-icon @click="Dirto(0)" class="cursor-pointer hover:text-[#222] mr-[5px]">
<House />
</el-icon>
<span v-for="i, key of prefix.split('/')" class="text-[#666]">
<span v-if="i != ''" class="mx-[3px]">/</span>
<span @click="Dirto(key + 1)" class="cursor-pointer hover:text-[#222]">{{ i }}</span>
</span>
<span class="mx-[3px] text-[#666]">/</span>
</div>
</div>
<div class="container w-full">
<el-button-group class="pl-[10px]">
<el-button @click="dialogUploadFileShow = true">上传</el-button>
<el-button>下载</el-button>
<el-button>编辑</el-button>
<el-button type="danger" :disabled="false" @click="deleteChosenFile">删除</el-button>
</el-button-group>
</div>
<el-table :data="fileListShow" @cell-click="fileClick" @selection-change="fileListSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="文件名" :formatter="fileListTableNameFormatter" />
<el-table-column prop="lastModified" :formatter="fileListTableLastModifiedFormatter" label="最后修改时间" />
<el-table-column prop="size" :formatter="fileListTableSizeFormatter" label="文件大小" />
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button v-if="!scope.row.dir" size="small"
@click.prevent="fileListHandleDownload(scope.row)">下载</el-button>
<!-- <el-button v-if="!scope.row.dir" size="small"
@click.prevent="fileListHandleRename(scope.row)">重命名</el-button> -->
<el-button v-if="!scope.row.dir" size="small" type="danger"
@click.prevent="fileListHandleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 文件上传对话框 -->
<el-dialog v-if="dialogUploadFileShow" v-model="dialogUploadFileShow" :multiple="true" title="上传文件"
class="min-w-[300px]">
<el-upload drag multiple :auto-upload="false" :on-change="onUploadFileChange" :disabled="isUploading">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖动文件到此处或 <em>单击选择</em>
</div>
<template #tip>
<div class="el-upload__tip">任何小于5GB的文件</div>
</template>
</el-upload>
<el-button @click="uploadFile" :loading="isUploading" class="mt-[10px]">开始上传</el-button>
</el-dialog>
</template>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { Menu as IconMenu, Document, Back, Tools } from '@element-plus/icons-vue'
import { Menu as IconMenu, Document, Back, Tools, Files } from '@element-plus/icons-vue'
import Resources from '../../components/Console/Resources.vue'
import Blogs from '../../components/Console/Blogs.vue'
import Utils from '../../components/Console/Utils.vue'
import FileOnline from '../../components/Console/FileOnline.vue'
import { shallowRef, ref, onMounted, onUnmounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
const tabComponent = shallowRef(Resources);
@@ -51,7 +52,13 @@ onUnmounted(async () => {
</el-icon>
<span>博客管理</span>
</el-menu-item>
<el-menu-item index="3" @click="tabComponent = Utils">
<el-menu-item index="3" @click="tabComponent = FileOnline">
<el-icon>
<Files />
</el-icon>
<span>文件管理</span>
</el-menu-item>
<el-menu-item index="4" @click="tabComponent = Utils">
<el-icon>
<Tools />
</el-icon>