添加 前端 OSS文件管理
This commit is contained in:
2
tonecn/components.d.ts
vendored
2
tonecn/components.d.ts
vendored
@@ -33,6 +33,8 @@ declare module 'vue' {
|
|||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ElText: typeof import('element-plus/es')['ElText']
|
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']
|
Footer: typeof import('./src/components/Common/Footer.vue')['default']
|
||||||
Header: typeof import('./src/components/Common/Header.vue')['default']
|
Header: typeof import('./src/components/Common/Header.vue')['default']
|
||||||
Resources: typeof import('./src/components/Console/Resources.vue')['default']
|
Resources: typeof import('./src/components/Console/Resources.vue')['default']
|
||||||
|
|||||||
@@ -22,10 +22,12 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
|
"@types/ali-oss": "^6.16.11",
|
||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^20.12.5",
|
"@types/node": "^20.12.5",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"ali-oss": "^6.21.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"element-plus": "^2.7.3",
|
"element-plus": "^2.7.3",
|
||||||
"npm": "^10.8.3",
|
"npm": "^10.8.3",
|
||||||
|
|||||||
308
tonecn/src/components/Console/FileOnline.vue
Normal file
308
tonecn/src/components/Console/FileOnline.vue
Normal 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>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<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 Resources from '../../components/Console/Resources.vue'
|
||||||
import Blogs from '../../components/Console/Blogs.vue'
|
import Blogs from '../../components/Console/Blogs.vue'
|
||||||
import Utils from '../../components/Console/Utils.vue'
|
import Utils from '../../components/Console/Utils.vue'
|
||||||
|
import FileOnline from '../../components/Console/FileOnline.vue'
|
||||||
import { shallowRef, ref, onMounted, onUnmounted } from 'vue';
|
import { shallowRef, ref, onMounted, onUnmounted } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
const tabComponent = shallowRef(Resources);
|
const tabComponent = shallowRef(Resources);
|
||||||
@@ -51,7 +52,13 @@ onUnmounted(async () => {
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
<span>博客管理</span>
|
<span>博客管理</span>
|
||||||
</el-menu-item>
|
</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>
|
<el-icon>
|
||||||
<Tools />
|
<Tools />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
|
|||||||
Reference in New Issue
Block a user