文件上传组件迁移
@ -10,13 +10,18 @@
|
|||||||
"prettier": "prettier --write ."
|
"prettier": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.0.10",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@headlessui/vue": "^1.7.12",
|
||||||
|
"@heroicons/vue": "^2.0.17",
|
||||||
|
"@soerenmartius/vue3-clipboard": "^0.1.2",
|
||||||
"@tinymce/tinymce-vue": "^5.1.1",
|
"@tinymce/tinymce-vue": "^5.1.1",
|
||||||
"@types/js-cookie": "^3.0.2",
|
"@types/js-cookie": "^3.0.2",
|
||||||
"@vueuse/core": "^9.1.1",
|
"@vueuse/core": "^9.1.1",
|
||||||
|
"@vueuse/integrations": "^9.13.0",
|
||||||
"@wangeditor/editor": "^5.0.0",
|
"@wangeditor/editor": "^5.0.0",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
|
"beautify-qrcode": "^1.0.3",
|
||||||
"better-scroll": "^2.4.2",
|
"better-scroll": "^2.4.2",
|
||||||
"default-passive-events": "^2.0.0",
|
"default-passive-events": "^2.0.0",
|
||||||
"docx-preview": "^0.3.0",
|
"docx-preview": "^0.3.0",
|
||||||
@ -28,6 +33,7 @@
|
|||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
|
"minimatch": "^5.1.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"path-to-regexp": "^6.2.0",
|
"path-to-regexp": "^6.2.0",
|
||||||
@ -35,6 +41,9 @@
|
|||||||
"screenfull": "^6.0.0",
|
"screenfull": "^6.0.0",
|
||||||
"sortablejs": "^1.14.0",
|
"sortablejs": "^1.14.0",
|
||||||
"tinymce": "^7.0.0",
|
"tinymce": "^7.0.0",
|
||||||
|
"ua-browser": "^0.1.5",
|
||||||
|
"v-contextmenu": "^3.0.0",
|
||||||
|
"v3-img-preview-enhance": "^1.1.18",
|
||||||
"vue": "^3.2.40",
|
"vue": "^3.2.40",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^9.1.9",
|
"vue-i18n": "^9.1.9",
|
||||||
|
BIN
web/public/image/beijing.jpg
Normal file
After Width: | Height: | Size: 743 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 915 B |
@ -7,7 +7,7 @@ window.webConfig = {
|
|||||||
"content1": "杭州明眸慧眼科技有限公司",
|
"content1": "杭州明眸慧眼科技有限公司",
|
||||||
"content2": "浙江省杭州市余杭区祥茂路166号华滋科欣设计创意园4号楼",
|
"content2": "浙江省杭州市余杭区祥茂路166号华滋科欣设计创意园4号楼",
|
||||||
"content3": "400-6588695",
|
"content3": "400-6588695",
|
||||||
"bgImg": "loginbg.jpg",
|
"bgImg": "beijing.jpg",
|
||||||
"loginLogo": "uplogo.png",
|
"loginLogo": "uplogo.png",
|
||||||
"headerLogo": "logo.png",
|
"headerLogo": "logo.png",
|
||||||
"ItutionIds": "a35ce60b10e425df77f401ebf1af80ac",
|
"ItutionIds": "a35ce60b10e425df77f401ebf1af80ac",
|
||||||
|
@ -38,3 +38,11 @@ export function deleteNodesById(queryParams:any) {
|
|||||||
params:queryParams
|
params:queryParams
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
//获取表格内容
|
||||||
|
export function getFilesPage(queryParams:any) {
|
||||||
|
return request({
|
||||||
|
url: '/specialDocument/sd_files/page',
|
||||||
|
method: 'get',
|
||||||
|
params:queryParams
|
||||||
|
});
|
||||||
|
}
|
54
web/src/api/file-operator.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// import axios from "~/http/request"
|
||||||
|
import request from '@/utils/request';
|
||||||
|
import useFilePwd from "@/components/file/file/useFilePwd";
|
||||||
|
let { getPathPwd } = useFilePwd();
|
||||||
|
|
||||||
|
// 新建文件夹
|
||||||
|
export function newFolderReq(data){
|
||||||
|
data.password = getPathPwd(data.path);
|
||||||
|
return request({
|
||||||
|
url: `/api/file/operator/mkdir`,
|
||||||
|
method: "post",
|
||||||
|
data:data
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
// 批量删除文件/文件夹
|
||||||
|
export function batchDeleteReq(data){
|
||||||
|
return request({
|
||||||
|
url: `/api/file/operator/delete/batch`,
|
||||||
|
method: "post",
|
||||||
|
data:data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 重命名文件
|
||||||
|
export function renameFileReq(data){
|
||||||
|
data.password = getPathPwd(data.path);
|
||||||
|
return request({
|
||||||
|
url: `/api/file/operator/rename/file`,
|
||||||
|
method: "post",
|
||||||
|
data:data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重命名文件夹
|
||||||
|
export function renameFolderReq(data){
|
||||||
|
data.password = getPathPwd(data.path);
|
||||||
|
return request({
|
||||||
|
url: `/api/file/operator/rename/folder`,
|
||||||
|
method: "post",
|
||||||
|
data:data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件上传链接
|
||||||
|
export function uploadFileReq(data){
|
||||||
|
data.password = getPathPwd(data.path);
|
||||||
|
return request({
|
||||||
|
url: `/api/file/operator/upload/file`,
|
||||||
|
method: "post",
|
||||||
|
data:data
|
||||||
|
})
|
||||||
|
}
|
20
web/src/api/header.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// import axios from "~/http/request"
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
export function resetPasswordReq(data){
|
||||||
|
return request({
|
||||||
|
url: "/api/site/reset-password",
|
||||||
|
method: "get",
|
||||||
|
params:data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取已启用的存储源列表
|
||||||
|
export function getSourceListReq(data){
|
||||||
|
return request({
|
||||||
|
url: "/api/storage/list",
|
||||||
|
method: "get",
|
||||||
|
params:data
|
||||||
|
})
|
||||||
|
}
|
53
web/src/api/home.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// import axios from '~/http/request';
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
// 获取存储源文件列表
|
||||||
|
export function loadFileListReq(data) {
|
||||||
|
return request({
|
||||||
|
url: `/api/storage/files`,
|
||||||
|
method: 'post',
|
||||||
|
data:data,
|
||||||
|
config: {
|
||||||
|
showDefaultMsg: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取存储源文件详情
|
||||||
|
export function loadFileItemReq(data) {
|
||||||
|
return request({
|
||||||
|
url: `/api/storage/file/item`,
|
||||||
|
method: 'post',
|
||||||
|
data:data,
|
||||||
|
config: {
|
||||||
|
responseIntercept: false,
|
||||||
|
showDefaultMsg: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取全局站点设置
|
||||||
|
export const loadGlobalSiteConfigReq = () => {
|
||||||
|
return request({
|
||||||
|
url: `/api/site/config/global`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取存储源设置
|
||||||
|
export function loadStorageConfigReq(data) {
|
||||||
|
return request({
|
||||||
|
url: `/api/site/config/storage`,
|
||||||
|
method: 'post',
|
||||||
|
data:data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量生成直/短链
|
||||||
|
export function batchGenerateShortLinkReq(data) {
|
||||||
|
return request({
|
||||||
|
url: `/api/short-link/batch/generate`,
|
||||||
|
method: 'post',
|
||||||
|
data:data
|
||||||
|
});
|
||||||
|
};
|
BIN
web/src/assets/images/delete.png
Normal file
After Width: | Height: | Size: 324 B |
BIN
web/src/assets/images/qv.png
Normal file
After Width: | Height: | Size: 175 B |
BIN
web/src/assets/images/shangging.png
Normal file
After Width: | Height: | Size: 359 B |
BIN
web/src/assets/images/shua.png
Normal file
After Width: | Height: | Size: 347 B |
BIN
web/src/assets/images/wendang.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
web/src/assets/login/90.jfif
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
web/src/assets/login/beijing.jpg
Normal file
After Width: | Height: | Size: 743 KiB |
31
web/src/components/file/SvgIcon.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<svg class="icon" aria-hidden="true" :color="color">
|
||||||
|
<use :xlink:href="symbolId" :fill="color" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent, computed } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'SvgIcon',
|
||||||
|
props: {
|
||||||
|
prefix: {
|
||||||
|
type: String,
|
||||||
|
default: 'icon',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#333',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
|
||||||
|
return { symbolId }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
164
web/src/components/file/ZUpload.vue
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<div class="zfile-file-upload-body">
|
||||||
|
<el-dialog v-if="visible" v-model="visible" :destroy-on-close="true" @close="closeDialog"
|
||||||
|
:title="uploadMode === 'file' ? '上传文件' : '上传文件夹'" draggable top="5vh" width="70%">
|
||||||
|
<el-upload drag :http-request="beforeUpload" ref="uploadRef" :show-file-list="false" multiple >
|
||||||
|
<img style="margin: auto;" src="@/assets/images/wendang.png" alt="">
|
||||||
|
<div class="el-upload__text text-gray-400">
|
||||||
|
<span v-show="uploadMode === 'file'">
|
||||||
|
拖拽文件到这里或<em> 点击上传</em>, 上传至 <em>{{ currentPath }}</em>
|
||||||
|
</span>
|
||||||
|
<span v-show="uploadMode === 'folder'">
|
||||||
|
点击选择文件夹上传, 上传至 <em>{{ currentPath }}</em>
|
||||||
|
<br>
|
||||||
|
<span class="text-gray-400">(此处不支持拖拽文件夹,只支持点击选择文件夹)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<div class="mt-5 space-y-2.5">
|
||||||
|
<div class="flex flex-row w-full relative rounded-lg" v-for="item in uploadProgressInfoSorted"
|
||||||
|
:key="item.index">
|
||||||
|
|
||||||
|
<div class="mr-2 p-1.5">
|
||||||
|
<svg-icon class="text-5xl mt-1 py-1.5 sm:py-1"
|
||||||
|
:name="'file-type-' + common.getFileIconName(item)">
|
||||||
|
</svg-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-1.5 py-2.5 sm:py-3 w-full flex flex-col justify-between">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="flex sm:space-x-5 flex-col sm:flex-row">
|
||||||
|
<div class="font-medium text-sm max-w-[80%] line-clamp-1">{{ item.name }}</div>
|
||||||
|
<div class="text-gray-400 text-xs leading-5 line-clamp-1 active:line-clamp-none">
|
||||||
|
<span class="mr-4 box animate__animated animate__fadeIn"> {{
|
||||||
|
common.fileSizeFormat(item.size) }} </span>
|
||||||
|
<span v-if="item.status === 'uploading' && !item.msg"
|
||||||
|
class="text-blue-500 box animate__animated animate__fadeIn">{{ item.speed }} /
|
||||||
|
秒</span>
|
||||||
|
<span v-if="item.status === 'uploading' && item.msg"
|
||||||
|
class="text-blue-500 box animate__animated animate__fadeIn">{{ item.msg
|
||||||
|
}}</span>
|
||||||
|
<img v-else-if="item.status === 'finished'" style="display: inline-block;" src="@/assets/images/shangging.png" alt="">
|
||||||
|
<!-- <svg-icon v-else-if="item.status === 'finished'" name="check"
|
||||||
|
class="inline text-green-500 box animate__animated animate__fadeIn" /> -->
|
||||||
|
<span v-else-if="item.status === 'waiting'"
|
||||||
|
class="text-yellow-500 box animate__animated animate__fadeIn">{{ item.msg
|
||||||
|
}}</span>
|
||||||
|
<span v-else-if="item.status === 'error'"
|
||||||
|
class="text-red-500 box animate__animated animate__fadeIn">{{ item.msg }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div v-if="item.status === 'uploading'">
|
||||||
|
<span class="text-gray-500 text-xs mr-2">{{ common.fileSizeFormat(item.loaded) }} /
|
||||||
|
{{ common.fileSizeFormat(item.size) }}</span>
|
||||||
|
<img src="@/assets/images/qv.png" @click="cancelUpload(item)" alt="" class=" relative inline text-gray-500 mr-1 text-lg cursor-pointer rounded-full hover:bg-gray-200 box animate__animated animate__fadeIn">
|
||||||
|
<!-- <svg-icon name="tool-close2"
|
||||||
|
class="top-0.5 relative inline text-gray-500 mr-1 text-lg cursor-pointer rounded-full hover:bg-gray-200 box animate__animated animate__fadeIn" /> -->
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.status === 'finished'">
|
||||||
|
<img src="@/assets/images/delete.png" @click="removeUploadFileByIndex(item.index)" alt=""
|
||||||
|
class="inline text-red-400 mr-1 text-base cursor-pointer rounded-full hover:bg-gray-200 box animate__animated animate__fadeIn">
|
||||||
|
<!-- <svg-icon @click="removeUploadFileByIndex(item.index)" name="delete"
|
||||||
|
class="inline text-red-400 mr-1 text-base cursor-pointer rounded-full hover:bg-gray-200 box animate__animated animate__fadeIn" /> -->
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.status === 'error'">
|
||||||
|
<img src="@/assets/images/shua.png" @click="retryUpload(item)" class="inline text-red-500 mr-1 text-base cursor-pointer rounded-full hover:bg-gray-200 box animate__animated animate__fadeIn" alt="">
|
||||||
|
<!-- <svg-icon @click="retryUpload(item)" name="refresh"
|
||||||
|
class="inline text-red-500 mr-1 text-base cursor-pointer rounded-full hover:bg-gray-200 box animate__animated animate__fadeIn" /> -->
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.status === 'waiting'">
|
||||||
|
<img src="@/assets/images/delete.png" @click="removeUploadFileByIndex(item.index)" alt=""
|
||||||
|
class="inline text-red-400 mr-1 text-base cursor-pointer rounded-full hover:bg-gray-200 box animate__animated animate__fadeIn">
|
||||||
|
<!-- <svg-icon @click="removeUploadFileByIndex(item.index)" name="delete"
|
||||||
|
class="inline text-red-400 mr-1 text-base cursor-pointer rounded-full hover:bg-gray-200 box animate__animated animate__fadeIn" /> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-progress v-if="item.status === 'finished'" :show-text="false"
|
||||||
|
:percentage="item.progress" status="success"></el-progress>
|
||||||
|
<el-progress v-else-if="item.status === 'uploading'" :show-text="false"
|
||||||
|
:percentage="item.progress"></el-progress>
|
||||||
|
<el-progress v-else-if="item.status === 'error'" :show-text="false" :percentage="100"
|
||||||
|
status="exception"></el-progress>
|
||||||
|
<el-progress v-else-if="item.status === 'waiting'" :show-text="false"
|
||||||
|
:percentage="0"></el-progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 文件拖拽提示框-->
|
||||||
|
<div ref="dropBoxRef" id="dropBox" class="drop-view" v-if="storageConfigStore.permission.upload"
|
||||||
|
v-show="dropState">
|
||||||
|
<div class="drop-sub">
|
||||||
|
<span>上传文件至 {{ currentPath }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import common from "@/components/file/common";
|
||||||
|
import { ref, onMounted,defineEmits } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
let router = useRouter();
|
||||||
|
let route = useRoute();
|
||||||
|
|
||||||
|
import useFileSelect from "@/components/file/file/useFileSelect";
|
||||||
|
let { currentPath } = useFileSelect();
|
||||||
|
|
||||||
|
import useFileUpload from "@/components/file/file/useFileUpload";
|
||||||
|
const { visible, uploadMode, cancelUpload, beforeUpload, uploadProgressInfoSorted,
|
||||||
|
dropState, listenDropFile,
|
||||||
|
clearALlFinishedUploadFile, removeUploadFileByIndex, retryUpload } = useFileUpload();
|
||||||
|
|
||||||
|
import useStorageConfigStore from "@/components/file/stores/storage-config";
|
||||||
|
let storageConfigStore = useStorageConfigStore();
|
||||||
|
|
||||||
|
import useFileDataStore from "@/components/file/stores/file-data";
|
||||||
|
let fileDataStore = useFileDataStore();
|
||||||
|
|
||||||
|
// 如果有上传完成的文件,关闭对话框时调用 close 方法刷新文件列表
|
||||||
|
const emit = defineEmits([])
|
||||||
|
const closeDialog = () => {
|
||||||
|
let deleteCount = clearALlFinishedUploadFile();
|
||||||
|
if (deleteCount > 0) {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听拖拽上传
|
||||||
|
const dropBoxRef = ref();
|
||||||
|
onMounted(() => {
|
||||||
|
listenDropFile();
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.zfile-file-upload-body {
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
@apply border-dashed border-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-view {
|
||||||
|
@apply fixed w-full h-full z-10 bg-opacity-20 bg-black left-0 bottom-0 flex justify-center items-center flex-row;
|
||||||
|
|
||||||
|
.drop-sub {
|
||||||
|
@apply flex justify-center items-center h-5/6 w-5/6 border-dashed border-2 border-gray-400 rounded-2xl text-gray-500 font-bold text-2xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
147
web/src/components/file/common.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// 文件分类
|
||||||
|
const fileTypeMap = {
|
||||||
|
image: ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'webp', 'ico'],
|
||||||
|
video: ['mp4', 'webm', 'm3u8', 'rmvb', 'avi', 'swf', '3gp', 'mkv', 'flv'],
|
||||||
|
audio: ['mp3', 'wav', 'wma', 'ogg', 'aac', 'flac', 'm4a'],
|
||||||
|
text: ['scss', 'sass', 'kt', 'gitignore', 'bat', 'properties', 'yml', 'css', 'js', 'md', 'xml', 'txt', 'py', 'go', 'html', 'less', 'php', 'rb', 'rust', 'script', 'java', 'sh', 'sql'],
|
||||||
|
executable: ['exe', 'dll', 'com', 'vbs'],
|
||||||
|
archive: ['7z', 'zip', 'rar', 'tar', 'gz'],
|
||||||
|
pdf: ['pdf'],
|
||||||
|
office: ['doc', 'docx', 'csv', 'xls', 'xlsx', "ppt", 'pptx'],
|
||||||
|
three3d: ['dae', 'fbx', 'gltf', 'glb', 'obj', 'ply', 'stl'],
|
||||||
|
document: ['txt', 'pages', 'epub', 'numbers', 'keynote']
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可预览的文件类型
|
||||||
|
const previewFileType = ['image', 'video', 'audio', 'text', 'office', 'pdf', 'three3d'];
|
||||||
|
|
||||||
|
import config from '/package.json'
|
||||||
|
|
||||||
|
// 自动对 /src/assets/icons 目录下的文件图标进行显示
|
||||||
|
const iconFileType = [];
|
||||||
|
import ids from 'virtual:svg-icons-names'
|
||||||
|
ids.forEach(id => {
|
||||||
|
iconFileType.push(id.replace(/^icon-file-type-/, ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
let common = {
|
||||||
|
responseCode: {
|
||||||
|
SUCCESS: 0,
|
||||||
|
FAIL: -1,
|
||||||
|
REQUIRED_PASSWORD: 405,
|
||||||
|
INVALID_PASSWORD: 406
|
||||||
|
},
|
||||||
|
storageType: {
|
||||||
|
s3Type: ['s3', 'tencent', 'aliyun', 'qiniu', 'minio', 'huawei', 'doge-cloud'],
|
||||||
|
proxyType: ['local', 'webdav', 'ftp', 'sftp', 'google-drive'],
|
||||||
|
micrsoftType: ['sharepoint', 'sharepoint-china', 'onedrive', 'onedrive-china']
|
||||||
|
},
|
||||||
|
version: config.version,
|
||||||
|
constant: {
|
||||||
|
fileTypeMap,
|
||||||
|
iconFileType,
|
||||||
|
previewFileType
|
||||||
|
},
|
||||||
|
openPage: (url) => {
|
||||||
|
window.open(url);
|
||||||
|
},
|
||||||
|
fileSizeFormat: (bytes) => {
|
||||||
|
if (bytes === 0) return '-';
|
||||||
|
if (bytes === -1) return '未知';
|
||||||
|
let k = 1024;
|
||||||
|
let sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
let i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||||
|
},
|
||||||
|
fileSizeFilter: (row, column, bytes) => {
|
||||||
|
if (row.type === "BACK") return '';
|
||||||
|
if (row.type === "FOLDER" && !row.size) return '-';
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
if (bytes === -1) return '未知';
|
||||||
|
let k = 1024;
|
||||||
|
let sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
let i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return common.fileSizeFormat(bytes);
|
||||||
|
},
|
||||||
|
getFileIconName(file) {
|
||||||
|
let iconName;
|
||||||
|
if (file.type === 'BACK' || file.type === 'FOLDER' || file.type === 'ROOT') {
|
||||||
|
return file.type.toLowerCase();
|
||||||
|
} else {
|
||||||
|
let fileSuffix = this.getFileSuffix(file.name);
|
||||||
|
let fileType = this.getFileType(file.name);
|
||||||
|
|
||||||
|
if (iconFileType.indexOf(fileSuffix) !== -1) {
|
||||||
|
iconName = fileSuffix;
|
||||||
|
} else if (fileType) {
|
||||||
|
iconName = fileType;
|
||||||
|
} else {
|
||||||
|
iconName = 'file';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iconName;
|
||||||
|
},
|
||||||
|
getFileSuffix(name) {
|
||||||
|
let lastIndex = name.lastIndexOf('.');
|
||||||
|
if (lastIndex === -1) {
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
|
return name.substring(lastIndex + 1).toLowerCase();
|
||||||
|
},
|
||||||
|
getFileName(name) {
|
||||||
|
let lastIndex = name.lastIndexOf('.');
|
||||||
|
if (lastIndex === -1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return name.substring(0, lastIndex);
|
||||||
|
},
|
||||||
|
getFileType(name) {
|
||||||
|
let fileType;
|
||||||
|
for (let key in fileTypeMap) {
|
||||||
|
let suffix = this.getFileSuffix(name);
|
||||||
|
if (fileTypeMap[key].indexOf(suffix) !== -1) {
|
||||||
|
fileType = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileType;
|
||||||
|
},
|
||||||
|
removeDuplicateSeparator(path) {
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
if (path.indexOf("http://") === 0) {
|
||||||
|
result = "http://";
|
||||||
|
} else if (path.indexOf("https://") === 0) {
|
||||||
|
result = "https://";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = result.length; i < path.length - 1; i++) {
|
||||||
|
let current = path.charAt(i);
|
||||||
|
let next = path.charAt(i + 1);
|
||||||
|
if (!(current === '/' && next === '/')) {
|
||||||
|
result += current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += path.charAt(path.length - 1);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
isMobile() {
|
||||||
|
let flag = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)
|
||||||
|
return flag || window.innerWidth < 768;
|
||||||
|
},
|
||||||
|
dateFormat:function(time) {
|
||||||
|
if (!time) {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
let date = new Date(time);
|
||||||
|
let year = date.getFullYear();
|
||||||
|
let month = date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1;
|
||||||
|
let day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
|
||||||
|
let hours = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
|
||||||
|
let minutes = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
|
||||||
|
let seconds = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
|
||||||
|
return year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + seconds;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default common;
|
70
web/src/components/file/file/useFileContextMenu.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import "v-contextmenu/dist/themes/default.css";
|
||||||
|
|
||||||
|
import useFileSelect from "@/components/file/file/useFileSelect";
|
||||||
|
const { selectRows, clearSelection, toggleRowSelection } = useFileSelect();
|
||||||
|
|
||||||
|
import useRouterData from "@/components/file/useRouterData";
|
||||||
|
let { storageKey } = useRouterData();
|
||||||
|
|
||||||
|
import useFileDataStore from "@/components/file/stores/file-data";
|
||||||
|
let fileDataStore = useFileDataStore();
|
||||||
|
import { ref } from 'vue';
|
||||||
|
const contextMenuTargetFile = ref(false);
|
||||||
|
const contextMenuTargetBlank = ref(false);
|
||||||
|
|
||||||
|
let contextmenuRef;
|
||||||
|
|
||||||
|
export default function useFileContextMenu() {
|
||||||
|
|
||||||
|
const showFileMenu = (row, column, event) => {
|
||||||
|
if (!storageKey.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果右键的不是空白区域,则不显示菜单
|
||||||
|
if (row instanceof Event) {
|
||||||
|
event = row;
|
||||||
|
let parentDom = document.querySelector(".zfile-index-body-wrapper");
|
||||||
|
let ignoreDom = document.querySelector(".el-dialog");
|
||||||
|
if (!parentDom.contains(event.target) || ignoreDom?.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contextMenuTargetBlank.value = true;
|
||||||
|
} else {
|
||||||
|
if (row.type === 'BACK') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileDataStore.updateCurrentRightClickRow(row);
|
||||||
|
|
||||||
|
if (!selectRows.value.includes(row)) {
|
||||||
|
clearSelection();
|
||||||
|
toggleRowSelection(row, true);
|
||||||
|
}
|
||||||
|
contextMenuTargetFile.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
contextmenuRef.show({
|
||||||
|
top: event.clientY,
|
||||||
|
left: event.clientX
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onclick = () => {
|
||||||
|
contextmenuRef.hide();
|
||||||
|
contextMenuTargetBlank.value = false;
|
||||||
|
contextMenuTargetFile.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
contextmenuRef.$el.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initContextMenu = (ref) => {
|
||||||
|
contextmenuRef = ref.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initContextMenu, showFileMenu, contextMenuTargetFile, contextMenuTargetBlank
|
||||||
|
}
|
||||||
|
}
|
282
web/src/components/file/file/useFileData.js
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
// import { ref,reactive,useTitle,computed } from 'vue';
|
||||||
|
import { ref,reactive,computed} from "vue";
|
||||||
|
import MessageBox from "@/components/file/messageBox/messageBox";
|
||||||
|
import path from "path-browserify";
|
||||||
|
import {removeDuplicateSlashes} from "fast-glob/out/managers/patterns";
|
||||||
|
|
||||||
|
import {loadFileListReq, loadStorageConfigReq} from "@/api/home";
|
||||||
|
|
||||||
|
import common from "@/components/file/common";
|
||||||
|
import useCommon from "@/components/file/useCommon";
|
||||||
|
const { encodeAllIgnoreSlashes } = useCommon();
|
||||||
|
|
||||||
|
import useRouterData from "@/components/file/useRouterData";
|
||||||
|
let { routerRef, fullpath, storageKey, currentPath } = useRouterData()
|
||||||
|
|
||||||
|
import useFilePwd from "@/components/file/file/useFilePwd";
|
||||||
|
let { getPathPwd, putPathPwd } = useFilePwd();
|
||||||
|
|
||||||
|
import useHeaderStorageList from "@/components/header/useHeaderStorageList";
|
||||||
|
const { storageListAsFileList } = useHeaderStorageList();
|
||||||
|
|
||||||
|
import useFileDataStore from "@/components/file/stores/file-data";
|
||||||
|
let fileDataStore = useFileDataStore();
|
||||||
|
|
||||||
|
import useStorageConfigStore from "@/components/file/stores/storage-config";
|
||||||
|
let storageConfigStore = useStorageConfigStore();
|
||||||
|
import { useTitle } from '@vueuse/core';
|
||||||
|
const title = useTitle(storageConfigStore.globalConfig.siteName);
|
||||||
|
|
||||||
|
import useGlobalConfigStore from "@/components/file/stores/global-config";
|
||||||
|
let globalConfigStore = useGlobalConfigStore();
|
||||||
|
|
||||||
|
// 引入文件预览组件
|
||||||
|
import useFilePreview from '@/components/file/file/useFilePreview';
|
||||||
|
const { openAudio, openImage, openOffice, openPdf, openText, openVideo, open3d } = useFilePreview();
|
||||||
|
|
||||||
|
// 文件操作相关 useFileOperator
|
||||||
|
import useFileOperator from '@/components/file/file/useFileOperator';
|
||||||
|
|
||||||
|
// ------------- loading start ------------
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
// 是否是已经调用了首次 loading
|
||||||
|
const firstLoading = ref(false);
|
||||||
|
let skeletonData = reactive([]);
|
||||||
|
if (skeletonData.length === 0) {
|
||||||
|
// 动态配置骨架屏行数
|
||||||
|
for (let i = 0; i < globalConfigStore.zfileConfig.skeleton.size; i++) {
|
||||||
|
skeletonData.push({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ------------- loading end ------------
|
||||||
|
|
||||||
|
|
||||||
|
// 文件列表查询条件
|
||||||
|
let searchParam = reactive({
|
||||||
|
path: '',
|
||||||
|
password: '',
|
||||||
|
orderBy: '',
|
||||||
|
orderDirection: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const initStorageConfig = ref(false);
|
||||||
|
import useFileSelect from "@/components/file/file/useFileSelect";
|
||||||
|
let { selectRows, clearSelection } = useFileSelect();
|
||||||
|
export default function useFileData() {
|
||||||
|
|
||||||
|
// 排序并重新加载数据
|
||||||
|
const sortChangeMethod = ({prop, order}) => {
|
||||||
|
searchParam.orderBy = prop;
|
||||||
|
searchParam.orderDirection = order === "descending" ? "desc" : "asc";
|
||||||
|
loadFile();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadFile = (initParam) => {
|
||||||
|
// 未指定 storageKey 时, 不执行任何操作.
|
||||||
|
if (!storageKey.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
searchParam.path = currentPath.value;
|
||||||
|
|
||||||
|
let param = initParam || {};
|
||||||
|
param.storageKey = storageKey.value;
|
||||||
|
param.path = currentPath.value;
|
||||||
|
param.password = param.password || getPathPwd();
|
||||||
|
param.orderBy = searchParam.orderBy || storageConfigStore.globalConfig.defaultSortField;
|
||||||
|
param.orderDirection = searchParam.orderDirection || storageConfigStore.globalConfig.defaultSortOrder;
|
||||||
|
|
||||||
|
let requestStorageId = storageKey.value;
|
||||||
|
loadFileListReq(param).then((response) => {
|
||||||
|
|
||||||
|
let passwordPattern = response.data.passwordPattern;
|
||||||
|
|
||||||
|
if (initParam?.rememberPassword) {
|
||||||
|
putPathPwd(passwordPattern, param.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果请求的 storageKey 和当前的 storageKey 不一致
|
||||||
|
// 则表示再加载数据期间,修改了 storageKey, 为了防止数据错乱, 取消本次渲染.
|
||||||
|
if (requestStorageId !== storageKey.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileList = response.data.files;
|
||||||
|
|
||||||
|
// 如果不是根路径, 则构建 back 上级路径的数据.
|
||||||
|
let searchPath = searchParam.path;
|
||||||
|
if (searchPath !== '' && searchPath !== '/') {
|
||||||
|
let parentPathName = path.basename(path.resolve(currentPath.value, "../"));
|
||||||
|
fileList.unshift({
|
||||||
|
name: parentPathName ? parentPathName : '/',
|
||||||
|
path: path.resolve(searchPath, '../'),
|
||||||
|
type: 'BACK'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fileDataStore.updateFileList(fileList);
|
||||||
|
loading.value = false;
|
||||||
|
firstLoading.value = true;
|
||||||
|
selectRows.value = [];
|
||||||
|
|
||||||
|
// 修改标题
|
||||||
|
if (fullpath.value) {
|
||||||
|
title.value = storageConfigStore.globalConfig.siteName + ' | ' + fullpath.value[fullpath.value.length - 1];
|
||||||
|
} else {
|
||||||
|
title.value = storageConfigStore.globalConfig.siteName + ' | 首页';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFileConfig(param);
|
||||||
|
}).catch((error) => {
|
||||||
|
let data = error.response.data;
|
||||||
|
// 如果需要密码或密码错误进行提示, 并弹出输入密码的框.
|
||||||
|
if (data.code === common.responseCode.INVALID_PASSWORD) {
|
||||||
|
ElMessage.warning('密码错误,请重新输入!');
|
||||||
|
popPassword();
|
||||||
|
} else if (data.code === common.responseCode.REQUIRED_PASSWORD) {
|
||||||
|
popPassword();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(data.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载存储器设置
|
||||||
|
const loadFileConfig = (loadFileParam) => {
|
||||||
|
|
||||||
|
let param = {
|
||||||
|
storageKey: storageKey.value,
|
||||||
|
path: currentPath.value,
|
||||||
|
password: loadFileParam.password
|
||||||
|
}
|
||||||
|
loadStorageConfigReq(param).then((res) => {
|
||||||
|
storageConfigStore.updateFolderConfig(res.data);
|
||||||
|
|
||||||
|
// 如果切换了存储器 ID, 则
|
||||||
|
if (storageKey.value !== fileDataStore.oldStorageKey) {
|
||||||
|
fileDataStore.updateOldStorageKey(storageKey.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}).finally(() => {
|
||||||
|
initStorageConfig.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 点击文件时,判断是文件夹则进入文件夹,是文件则进行预览
|
||||||
|
const openRow = (row) => {
|
||||||
|
if (!row.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileDataStore.updateCurrentClickRow(row);
|
||||||
|
// 如果是文件且格式支持预览, 则进行预览, 格式不支持预览, 则直接进行下载 (ftp 模式不支持预览, 全部是下载)
|
||||||
|
if (row.type === 'FILE') {
|
||||||
|
const { batchDownloadFile } = useFileOperator();
|
||||||
|
|
||||||
|
// 获取文件类型
|
||||||
|
let fileType = row.fileType;
|
||||||
|
|
||||||
|
|
||||||
|
switch (fileType) {
|
||||||
|
case 'video': openVideo(); break;
|
||||||
|
case 'image': openImage(row); break;
|
||||||
|
case 'text': openText(); break;
|
||||||
|
case 'audio': openAudio(row); break;
|
||||||
|
case 'office': openOffice(row); break;
|
||||||
|
case 'pdf': openPdf(row); break;
|
||||||
|
case 'three3d': open3d(row); break;
|
||||||
|
default: batchDownloadFile(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelection();
|
||||||
|
} else {
|
||||||
|
if (row.type === 'ROOT') {
|
||||||
|
routerRef.value.push(row.path);
|
||||||
|
} else if (row.type === 'BACK') {
|
||||||
|
let fullPath = removeDuplicateSlashes('/' + storageKey.value + '/' + row.path);
|
||||||
|
fullPath = encodeAllIgnoreSlashes(fullPath);
|
||||||
|
routerRef.value.push(fullPath);
|
||||||
|
} else {
|
||||||
|
let fullPath = removeDuplicateSlashes('/' + storageKey.value + '/' + row.path + '/' + row.name);
|
||||||
|
fullPath = encodeAllIgnoreSlashes(fullPath);
|
||||||
|
routerRef.value.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------- loading start ------------
|
||||||
|
|
||||||
|
// 是否启用骨架屏 loading
|
||||||
|
let skeletonLoading = computed(() => {
|
||||||
|
let skeletonEnable = globalConfigStore.zfileConfig.skeleton.enable;
|
||||||
|
let skeletonShow = globalConfigStore.zfileConfig.skeleton.show;
|
||||||
|
|
||||||
|
// 如果启用了骨架屏
|
||||||
|
if (skeletonEnable) {
|
||||||
|
// 如果骨架屏模式为是 '始终加载'
|
||||||
|
if (skeletonShow === 'always') {
|
||||||
|
return loading.value;
|
||||||
|
} else { // 如果骨架屏模式为是仅 '首次加载'
|
||||||
|
// 已经首次加载后, 则不使用骨架屏
|
||||||
|
return firstLoading.value ? false : loading.value;
|
||||||
|
}
|
||||||
|
} else { // 如果未启用骨架屏, 则直接返回 false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 是否显示普通 loading
|
||||||
|
let basicLoading = computed(() => {
|
||||||
|
return skeletonLoading.value ? false : loading.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------------- loading end ------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ------------- folder password start ------------
|
||||||
|
|
||||||
|
|
||||||
|
// 显示密码输入框
|
||||||
|
let popPassword = () => {
|
||||||
|
// 如果输入了密码, 则写入到 sessionStorage 缓存中, 并重新调用加载文件.
|
||||||
|
MessageBox.prompt('此文件夹已加密,请输入密码:', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputType: 'password',
|
||||||
|
checkbox: true,
|
||||||
|
defaultChecked: storageConfigStore.globalConfig.defaultSavePwd,
|
||||||
|
inputDefault: getPathPwd(),
|
||||||
|
checkboxLabel: '记住密码',
|
||||||
|
inputValidator(val) {
|
||||||
|
return !!val
|
||||||
|
},
|
||||||
|
inputErrorMessage: '密码不能为空.'
|
||||||
|
}).then(({value, checkbox}) => {
|
||||||
|
loadFile({password: value, rememberPassword: checkbox});
|
||||||
|
}).catch(() => {
|
||||||
|
if ((searchParam.path === '/' || searchParam.path === '') && storageConfigStore.globalConfig.rootShowStorage === true) {
|
||||||
|
fileDataStore.updateFileList(storageListAsFileList.value);
|
||||||
|
routerRef.value.push("/");
|
||||||
|
title.value = storageConfigStore.globalConfig.siteName + ' | 首页';
|
||||||
|
loading.value = false;
|
||||||
|
} else {
|
||||||
|
let parentPath = path.resolve(searchParam.path, '../');
|
||||||
|
routerRef.value.push("/" + storageKey.value + parentPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------- folder password end ------------
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadFile, openRow, searchParam, sortChangeMethod,
|
||||||
|
skeletonLoading, skeletonData, basicLoading, loading,
|
||||||
|
initStorageConfig, loadFileConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
157
web/src/components/file/file/useFileLink.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import common from "@/components/file/common";
|
||||||
|
import { batchGenerateShortLinkReq } from "@/api/home";
|
||||||
|
|
||||||
|
import { toClipboard } from '@soerenmartius/vue3-clipboard'
|
||||||
|
import { encodeData, rendererRect, rendererRound,
|
||||||
|
rendererDSJ, rendererLine, rendererFuncB } from 'beautify-qrcode';
|
||||||
|
|
||||||
|
const generateLinkLoading = ref(false);
|
||||||
|
const generateLinkDialogVisible = ref(false);
|
||||||
|
const generateLinkFormData = reactive({
|
||||||
|
expireTime: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
import useStorageConfigStore from "@/components/file/stores/storage-config";
|
||||||
|
let storageConfigStore = useStorageConfigStore();
|
||||||
|
|
||||||
|
import useRouterData from "@/components/file/useRouterData";
|
||||||
|
let { storageKey } = useRouterData()
|
||||||
|
|
||||||
|
const dataList = ref([]);
|
||||||
|
let data = computed(() => {
|
||||||
|
return dataList.value.length > 0 ? dataList.value[0] : null;
|
||||||
|
});
|
||||||
|
let generateLinkResultDialogVisible = ref(false);
|
||||||
|
let generateLinkResultLoading = ref(false);
|
||||||
|
|
||||||
|
const linkDialogVisible = computed(() => {
|
||||||
|
return generateLinkDialogVisible.value && generateLinkResultDialogVisible.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function useFileLink() {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 打开生成链接弹窗
|
||||||
|
*/
|
||||||
|
const openGenerateLinkDialog = () => {
|
||||||
|
generateLinkDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭生成链接弹窗
|
||||||
|
*/
|
||||||
|
const closeGenerateLinkDialog = () => {
|
||||||
|
generateLinkDialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交生成链接表单
|
||||||
|
*/
|
||||||
|
const submitGenerateLinkForm = () => {
|
||||||
|
closeGenerateLinkDialog();
|
||||||
|
openGenerateLinkResultDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开直链生成结果弹窗
|
||||||
|
*/
|
||||||
|
const openGenerateLinkResultDialog = () => {
|
||||||
|
generateLinkResultDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制直链
|
||||||
|
*/
|
||||||
|
let copyText = (text) => {
|
||||||
|
toClipboard(text).then(() => {
|
||||||
|
ElMessage.success('复制成功');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* svg 转为 src data uri
|
||||||
|
* @param svgText svg 文本
|
||||||
|
* @returns {string} src data uri
|
||||||
|
*/
|
||||||
|
const svgToDataUri = (svgText) => {
|
||||||
|
let xmlElement = document.createElement("xml")
|
||||||
|
xmlElement.innerHTML = svgText;
|
||||||
|
|
||||||
|
// 增加 svg 底色, 防止复制后是透明.
|
||||||
|
let rectElement = document.createElement("rect")
|
||||||
|
rectElement.setAttribute('width', '100%');
|
||||||
|
rectElement.setAttribute('height', '100%');
|
||||||
|
rectElement.style.fill = '#ffffff';
|
||||||
|
|
||||||
|
xmlElement.children[0].prepend(rectElement);
|
||||||
|
|
||||||
|
return 'data:image/svg+xml;utf8,' + encodeURIComponent(xmlElement.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成直链
|
||||||
|
*
|
||||||
|
* @param files 要生成的直链列表
|
||||||
|
*/
|
||||||
|
const generateALlLink = (files) => {
|
||||||
|
generateLinkResultLoading.value = true;
|
||||||
|
|
||||||
|
let param = {
|
||||||
|
storageKey: storageKey.value,
|
||||||
|
paths: [],
|
||||||
|
expireTime: generateLinkFormData.expireTime
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach((row) => {
|
||||||
|
let pathAndName = common.removeDuplicateSeparator("/" + row.path + "/" + row.name);
|
||||||
|
param.paths.push(pathAndName);
|
||||||
|
})
|
||||||
|
|
||||||
|
batchGenerateShortLinkReq(param).then((res) => {
|
||||||
|
let size = res.data.length;
|
||||||
|
res.data.forEach((item, index) => {
|
||||||
|
// 生成二维码
|
||||||
|
const qrcode = encodeData({
|
||||||
|
text: storageConfigStore.permission.shortLink ? item.shortLink : item.pathLink,
|
||||||
|
correctLevel: 2,
|
||||||
|
isSpace: false
|
||||||
|
});
|
||||||
|
if (size === 1) {
|
||||||
|
item.row = {
|
||||||
|
name: files[index].name,
|
||||||
|
size: common.fileSizeFormat(files[index].size),
|
||||||
|
time: files[index].time
|
||||||
|
};
|
||||||
|
item.qrcode = {
|
||||||
|
a1: svgToDataUri(rendererRect(qrcode)),
|
||||||
|
a2: svgToDataUri(rendererRound(qrcode)),
|
||||||
|
sp1: svgToDataUri(rendererDSJ(qrcode)),
|
||||||
|
aa1: svgToDataUri(rendererLine(qrcode)),
|
||||||
|
ab2: svgToDataUri(rendererFuncB(qrcode)),
|
||||||
|
}
|
||||||
|
item.currentImg = item.qrcode.a1;
|
||||||
|
} else {
|
||||||
|
item.name = files[index].name
|
||||||
|
}
|
||||||
|
dataList.value.push(item);
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
generateLinkResultLoading.value = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateLinkDialogVisible,
|
||||||
|
openGenerateLinkDialog,
|
||||||
|
closeGenerateLinkDialog,
|
||||||
|
generateLinkFormData,
|
||||||
|
generateLinkLoading,
|
||||||
|
submitGenerateLinkForm,
|
||||||
|
|
||||||
|
linkDialogVisible,
|
||||||
|
generateLinkResultDialogVisible, generateLinkResultLoading, openGenerateLinkResultDialog, copyText, data, dataList, generateALlLink
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
291
web/src/components/file/file/useFileOperator.js
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
import {
|
||||||
|
batchDeleteReq,
|
||||||
|
newFolderReq,
|
||||||
|
renameFileReq,
|
||||||
|
renameFolderReq,
|
||||||
|
} from "@/api/file-operator";
|
||||||
|
|
||||||
|
import useFileDataStore from "@/components/file/stores/file-data";
|
||||||
|
let fileDataStore = useFileDataStore();
|
||||||
|
|
||||||
|
import useStorageConfigStore from "@/components/file/stores/storage-config";
|
||||||
|
let storageConfigStore = useStorageConfigStore();
|
||||||
|
|
||||||
|
import useFileData from "@/components/file/file/useFileData";
|
||||||
|
import useRouterData from "@/components/file/useRouterData";
|
||||||
|
let { storageKey, currentPath } = useRouterData();
|
||||||
|
|
||||||
|
import useFileSelect from "@/components/file/file/useFileSelect";
|
||||||
|
let { selectRows, selectRow, selectFolders, selectFiles } = useFileSelect();
|
||||||
|
|
||||||
|
import useFilePwd from "@/components/file/file/useFilePwd";
|
||||||
|
let { getPathPwd } = useFilePwd();
|
||||||
|
|
||||||
|
// 检测浏览器类型
|
||||||
|
import uaBrowser from 'ua-browser'
|
||||||
|
import { ElLoading } from "element-plus";
|
||||||
|
const browserInfo = uaBrowser();
|
||||||
|
|
||||||
|
export default function useFileOperator() {
|
||||||
|
|
||||||
|
const { loadFile } = useFileData();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量下载已选择的所有文件
|
||||||
|
* @param {Object} row 已选择的文件
|
||||||
|
*/
|
||||||
|
const batchDownloadFile = (row) => {
|
||||||
|
if (!selectRows.value && selectRows.value.length === 0) {
|
||||||
|
ElMessage.warning("请至少选择一个文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirmMsg;
|
||||||
|
|
||||||
|
const checkBrowser = () => {
|
||||||
|
let result = {
|
||||||
|
isChrome: false,
|
||||||
|
tips: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentBrowser = browserInfo.browser;
|
||||||
|
let currentBrowserVersion = browserInfo.version;
|
||||||
|
|
||||||
|
if (currentBrowser === 'Chrome') {
|
||||||
|
result.isChrome = true;
|
||||||
|
} else {
|
||||||
|
result.tips = `<br><span class="text-gray-500 text-xs">检测到当前浏览器为 <b>${currentBrowser}-${currentBrowserVersion}</b>, 可能不支持此功能,建议使用谷歌浏览器!</span>`;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkBrowserResult = checkBrowser();
|
||||||
|
|
||||||
|
if (row?.name) {
|
||||||
|
confirmMsg = `是否确认下载文件 <span class="text-blue-500">${row.name}</span> ?`;
|
||||||
|
} else if (selectRows.value.length === 1) {
|
||||||
|
confirmMsg = `是否确认下载文件 <span class="text-blue-500">${selectRows.value[0].name}</span> ?`;
|
||||||
|
row = selectRows.value[0];
|
||||||
|
} else if (selectRows.value.length > 1) {
|
||||||
|
confirmMsg = `是否确认批量下载 ${selectRows.value.length} 个文件?`;
|
||||||
|
if (!checkBrowserResult.isChrome) {
|
||||||
|
confirmMsg += checkBrowserResult.tips;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessageBox.confirm(confirmMsg, '提示', {
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'info',
|
||||||
|
callback: (action) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
// 单个文件下载, 直接下载
|
||||||
|
if (row?.name) {
|
||||||
|
console.log('进行指定文件下载, 文件:', row);
|
||||||
|
downloadFileUseWindowOpenMode(row.url)
|
||||||
|
} else {
|
||||||
|
// 批量下载
|
||||||
|
selectRows.value.forEach((item) => {
|
||||||
|
if (item.type === 'FILE') {
|
||||||
|
console.log('批量选中文件下载, 文件:', item);
|
||||||
|
downloadFileUseIframeMode(item.url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 windows.open 模式下载文件
|
||||||
|
*
|
||||||
|
* @param url 下载文件 url
|
||||||
|
*/
|
||||||
|
const downloadFileUseWindowOpenMode = (url) => {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 iframe 模式下载文件
|
||||||
|
*
|
||||||
|
* @param url 下载文件 url
|
||||||
|
*/
|
||||||
|
const downloadFileUseIframeMode = (url) => {
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
iframe.style.display = "none"; // 防止影响页面
|
||||||
|
iframe.style.height = 0; // 防止影响页面
|
||||||
|
iframe.src = url;
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
setTimeout(()=>{
|
||||||
|
iframe.remove();
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建文件夹
|
||||||
|
const newFolder = () => {
|
||||||
|
ElMessageBox.prompt(`在 <b>${currentPath.value}</b> 下创建文件夹,请输入要创建的文件夹名称`, '提示', {
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
draggable: true,
|
||||||
|
inputValidator(val) {
|
||||||
|
if (!val) {
|
||||||
|
return '文件夹名称不能为空';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val.includes("/")) {
|
||||||
|
return '文件夹名称不能包含 /';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}).then(({value}) => {
|
||||||
|
let param = {
|
||||||
|
storageKey: storageKey.value,
|
||||||
|
path: currentPath.value,
|
||||||
|
name: value
|
||||||
|
}
|
||||||
|
newFolderReq(param).then(() => {
|
||||||
|
ElMessage.success('创建成功');
|
||||||
|
}).finally(() => {
|
||||||
|
loadFile();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重命名文件夹
|
||||||
|
const rename = () => {
|
||||||
|
let row = selectRow.value;
|
||||||
|
if (row === null) {
|
||||||
|
ElMessage.warning('请先选中一个文件或文件夹!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessageBox.prompt(`将 <b>${row.name}</b> 修改为:`, '提示', {
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputValue: row.name,
|
||||||
|
inputValidator(val) {
|
||||||
|
return !!val
|
||||||
|
},
|
||||||
|
inputErrorMessage: '模板名称不能为空.'
|
||||||
|
}).then(({value}) => {
|
||||||
|
let param = {
|
||||||
|
storageKey: storageKey.value,
|
||||||
|
path: row.path,
|
||||||
|
name: row.name,
|
||||||
|
newName: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
let reqMethod;
|
||||||
|
if (row.type === 'FILE') {
|
||||||
|
reqMethod = renameFileReq;
|
||||||
|
} else if (row.type === 'FOLDER') {
|
||||||
|
reqMethod = renameFolderReq;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameLoadingInstance = ElLoading.service({
|
||||||
|
fullscreen: true,
|
||||||
|
text: '重命名中...',
|
||||||
|
background: 'rgba(255, 255, 255, 0.6)'
|
||||||
|
})
|
||||||
|
|
||||||
|
reqMethod(param).then(() => {
|
||||||
|
ElMessage.success('重命名成功');
|
||||||
|
}).finally(() => {
|
||||||
|
renameLoadingInstance.close();
|
||||||
|
loadFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动至重命名
|
||||||
|
const moveTo = () => {
|
||||||
|
ElMessage.warning("暂未实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制至
|
||||||
|
const copyTo = () => {
|
||||||
|
ElMessage.warning("暂未实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 删除相关 start
|
||||||
|
const batchDelete = () => {
|
||||||
|
if (!storageConfigStore.permission.delete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectRows.value.length === 0) {
|
||||||
|
ElMessage.warning('请先至少选中一个文件或文件夹!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleteConfirmMsg = selectRows.value.length === 1 ? '是否确认删除 ' : '是否确认批量删除 ';
|
||||||
|
|
||||||
|
let notSupportDeleteNotEmptyFolderType = ['s3', 'tencent', 'aliyun', 'qiniu', 'minio', 'huawei', 'upyun'];
|
||||||
|
|
||||||
|
if (selectFolders.value.length > 0) {
|
||||||
|
deleteConfirmMsg += (' ' + selectFolders.value.length + ' 个文件夹');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectFolders.value.length > 0 && selectFiles.value.length > 0) {
|
||||||
|
deleteConfirmMsg += ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectFiles.value.length > 0) {
|
||||||
|
deleteConfirmMsg += (selectFiles.value.length + ' 个文件');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectFolders.value.length > 0 && notSupportDeleteNotEmptyFolderType.includes(fileDataStore.currentStorageSource.type.key)) {
|
||||||
|
deleteConfirmMsg += (' (不支持删除非空文件夹)');
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteConfirmMsg += "?"
|
||||||
|
|
||||||
|
ElMessageBox.confirm(deleteConfirmMsg, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
draggable: true,
|
||||||
|
callback: action => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
let param = {
|
||||||
|
storageKey: storageKey.value,
|
||||||
|
deleteItems: []
|
||||||
|
};
|
||||||
|
|
||||||
|
selectRows.value.forEach((item) => {
|
||||||
|
param.deleteItems.push({
|
||||||
|
path: item.path,
|
||||||
|
name: item.name,
|
||||||
|
type: item.type,
|
||||||
|
password: getPathPwd(item.path)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打开全屏 loading
|
||||||
|
const loadingInstance = ElLoading.service({
|
||||||
|
text: '删除中...',
|
||||||
|
background: 'rgba(0, 0, 0, .3)'
|
||||||
|
})
|
||||||
|
|
||||||
|
batchDeleteReq(param).then((res) => {
|
||||||
|
ElMessage.success(res.msg);
|
||||||
|
loadFile();
|
||||||
|
}).finally(() => {
|
||||||
|
loadingInstance.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 删除相关 end
|
||||||
|
|
||||||
|
return {
|
||||||
|
batchDownloadFile, rename, newFolder, moveTo, copyTo,
|
||||||
|
batchDelete
|
||||||
|
}
|
||||||
|
}
|
80
web/src/components/file/file/useFilePreview.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { v3ImgPreviewFn } from 'v3-img-preview-enhance'
|
||||||
|
import { ref,computed} from "vue";
|
||||||
|
|
||||||
|
// 基础依赖引入
|
||||||
|
import useGlobalConfigStore from "@/components/file/stores/global-config";
|
||||||
|
let globalConfigStore = useGlobalConfigStore();
|
||||||
|
|
||||||
|
import useFileDataStore from "@/components/file/stores/file-data";
|
||||||
|
let fileDataStore = useFileDataStore();
|
||||||
|
|
||||||
|
// 视频预览, 打开 dialog.
|
||||||
|
export let dialogVideoVisible = ref(false);
|
||||||
|
// 文本预览
|
||||||
|
export let dialogTextVisible = ref(false);
|
||||||
|
// office 预览
|
||||||
|
export let dialogOfficeVisible = ref(false);
|
||||||
|
// pdf 预览
|
||||||
|
export let dialogPdfVisible = ref(false);
|
||||||
|
// 3d 预览
|
||||||
|
export let dialog3dVisible = ref(false);
|
||||||
|
|
||||||
|
export default function useFilePreview() {
|
||||||
|
|
||||||
|
const openVideo = () => {
|
||||||
|
dialogVideoVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAudio = () => {
|
||||||
|
fileDataStore.updateAudioList(fileDataStore.filterFileByType('audio'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const openImage = (row) => {
|
||||||
|
// 过滤当前页面中所有图片,并记录当前打开的文件的索引位置
|
||||||
|
let images = [];
|
||||||
|
let currIndex = 0;
|
||||||
|
let imagePreviewMode = globalConfigStore.zfileConfig.imagePreview.mode;
|
||||||
|
if (imagePreviewMode === 'only') {
|
||||||
|
images.push(row.url);
|
||||||
|
} else {
|
||||||
|
fileDataStore.filterFileByType('image').forEach((image, index) => {
|
||||||
|
if (row.name === image.name) {
|
||||||
|
currIndex = index;
|
||||||
|
}
|
||||||
|
images.push(image.url);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
v3ImgPreviewFn({
|
||||||
|
images: images,
|
||||||
|
index: currIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const openText = () => {
|
||||||
|
dialogTextVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openOffice = () => {
|
||||||
|
dialogOfficeVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPdf = () => {
|
||||||
|
dialogPdfVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open3d = () => {
|
||||||
|
dialog3dVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openVideo, dialogVideoVisible,
|
||||||
|
openText, dialogTextVisible,
|
||||||
|
openOffice, dialogOfficeVisible,
|
||||||
|
openImage,
|
||||||
|
openAudio,
|
||||||
|
openPdf, dialogPdfVisible,
|
||||||
|
open3d, dialog3dVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
52
web/src/components/file/file/useFilePwd.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import minimatch from "minimatch";
|
||||||
|
import useRouterData from "@/components/file/useRouterData";
|
||||||
|
import { removeDuplicateSlashes } from "fast-glob/out/managers/patterns";
|
||||||
|
import common from "@/components/file/common";
|
||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
let { storageKey, currentPath } = useRouterData()
|
||||||
|
|
||||||
|
const zfilePasswordCache = useStorage('zfile-pwd-cache', {});
|
||||||
|
|
||||||
|
export default function useFilePwd() {
|
||||||
|
|
||||||
|
// 向缓存中写入当前路径密码
|
||||||
|
let putPathPwd = (pattern, password) => {
|
||||||
|
if (pattern) {
|
||||||
|
// 如果表达式开头没写 / ,则自动补全
|
||||||
|
pattern = pattern.startsWith('/') ? pattern : '/' + pattern;
|
||||||
|
|
||||||
|
if (!zfilePasswordCache.value[storageKey.value]) {
|
||||||
|
zfilePasswordCache.value[storageKey.value]= {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修正 glob 表达式兼容性和服务端不同的 bug
|
||||||
|
if (pattern.endsWith("**") && !pattern.endsWith("/**")) {
|
||||||
|
pattern = removeDuplicateSlashes(pattern.substring(0, pattern.length - 2) + "/**");
|
||||||
|
console.log('检测到密码文件夹通配符 ** 前未写 /,自动将其修正为为:', pattern);
|
||||||
|
}
|
||||||
|
zfilePasswordCache.value[storageKey.value][pattern] = password;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前路径缓存中的密码
|
||||||
|
let getPathPwd = (path) => {
|
||||||
|
let currentPathValue = path || currentPath.value;
|
||||||
|
currentPathValue = common.removeDuplicateSeparator('/' + currentPathValue + "/");
|
||||||
|
for (let storageTag of Object.keys(zfilePasswordCache.value)) {
|
||||||
|
if (storageTag === storageKey.value) {
|
||||||
|
for (let key of Object.keys(zfilePasswordCache.value[storageTag])) {
|
||||||
|
if (minimatch(currentPathValue, key)) {
|
||||||
|
return zfilePasswordCache.value[storageTag][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
putPathPwd,
|
||||||
|
getPathPwd
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
91
web/src/components/file/file/useFileSelect.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
const selectRows = ref([]);
|
||||||
|
let vueInstance = null;
|
||||||
|
import { ref,computed} from "vue";
|
||||||
|
export default function useFileSelect(currentInstance) {
|
||||||
|
|
||||||
|
if (currentInstance) {
|
||||||
|
vueInstance = currentInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
vueInstance.proxy.$refs.fileTableRef.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleRowSelection = (row, selected) => {
|
||||||
|
if (row?.type === 'BACK') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vueInstance.proxy.$refs.fileTableRef.toggleRowSelection(row, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleAllSelection = () => {
|
||||||
|
vueInstance.proxy.$refs.fileTableRef.toggleAllSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 文件是否可被选择
|
||||||
|
const checkSelectable = (row) => {
|
||||||
|
return row.type === "FILE" || row.type === "FOLDER";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前最后选中的文件行
|
||||||
|
const selectRow = computed(() => {
|
||||||
|
if (selectRows.value.length > 0) {
|
||||||
|
return selectRows.value[selectRows.value.length - 1];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前选中的文件
|
||||||
|
const selectFiles = computed(() => {
|
||||||
|
return selectRows.value.filter((row) => {
|
||||||
|
return row.type === "FILE";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前选中的文件夹
|
||||||
|
const selectFolders = computed(() => {
|
||||||
|
return selectRows.value.filter((row) => {
|
||||||
|
return row.type === "FOLDER";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新选中的文件列表
|
||||||
|
const selectRowsChange = (selection) => {
|
||||||
|
selectRows.value = selection;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 行选中 class
|
||||||
|
const tableRowClassName = ({ row, rowIndex }) => {
|
||||||
|
row.index = rowIndex;
|
||||||
|
return selectRows.value.indexOf(row) !== -1 ? "select-row" : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 多选统计信息
|
||||||
|
const selectStatistics = computed(() => {
|
||||||
|
let selectRowsLength = selectRows.value.length;
|
||||||
|
let selectFilesLength = selectFiles.value.length;
|
||||||
|
let selectFoldersLength = selectFolders.value.length;
|
||||||
|
|
||||||
|
let isSingleSelect = selectRowsLength === 1;
|
||||||
|
let isMultiSelect = selectRowsLength > 1;
|
||||||
|
let isAllFile = selectFilesLength === selectRowsLength;
|
||||||
|
let isAllFolder = selectFoldersLength === selectRowsLength;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSingleSelect,
|
||||||
|
isMultiSelect,
|
||||||
|
isAllFile,
|
||||||
|
isAllFolder
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkSelectable, tableRowClassName, selectRowsChange,
|
||||||
|
selectRow, selectRows,
|
||||||
|
selectFiles, selectFolders,
|
||||||
|
selectStatistics,
|
||||||
|
clearSelection, toggleRowSelection, toggleAllSelection
|
||||||
|
};
|
||||||
|
}
|
749
web/src/components/file/file/useFileUpload.js
Normal file
@ -0,0 +1,749 @@
|
|||||||
|
import {ElLoading} from "element-plus";
|
||||||
|
import { ref,reactive,computed,nextTick,watch} from "vue";
|
||||||
|
import {uploadFileReq} from "@/api/file-operator";
|
||||||
|
import axios from "axios";
|
||||||
|
import common from "@/components/file/common";
|
||||||
|
import {removeDuplicateSlashes} from "fast-glob/out/managers/patterns";
|
||||||
|
import { useEventBus } from '@vueuse/core';
|
||||||
|
import useFileDataStore from "@/components/file/stores/file-data";
|
||||||
|
let fileDataStore = useFileDataStore();
|
||||||
|
|
||||||
|
import useStorageConfigStore from "@/components/file/stores/storage-config";
|
||||||
|
let storageConfigStore = useStorageConfigStore();
|
||||||
|
|
||||||
|
import {hasDialog, hasPasswordInputFocus} from "@/components/file/file/useTableOperator";
|
||||||
|
|
||||||
|
// 拖拽上传状态, true 表示正有文件拖拽悬浮在上传框上
|
||||||
|
let dropState = ref(false);
|
||||||
|
import useRouterData from "@/components/file/useRouterData";
|
||||||
|
|
||||||
|
|
||||||
|
// 是否已经初始化监听 watch, 防止重复引用该文件导致的重复 watch
|
||||||
|
let isInitWatch = false;
|
||||||
|
|
||||||
|
// 是否显示上传框.
|
||||||
|
const visible = ref(false);
|
||||||
|
|
||||||
|
// 当前上传模式, 'file' or 'folder'
|
||||||
|
const uploadMode = ref('');
|
||||||
|
|
||||||
|
// 正在上传或已完成文件列表
|
||||||
|
const uploadingFileList = reactive([
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 等待上传文件列表
|
||||||
|
const waitingFileList = reactive([]);
|
||||||
|
|
||||||
|
// 取消上传文件的 cancelTokenSource 映射表
|
||||||
|
const cancelTokenSourceMap = new Map();
|
||||||
|
|
||||||
|
// 正在上传或已完成文件映射表 key: fileInfo.index value: fileInfo
|
||||||
|
const uploadingFileMap = new Map();
|
||||||
|
|
||||||
|
// 文件上传 index
|
||||||
|
let uploadIndex = 0;
|
||||||
|
|
||||||
|
// 文件上传状态顺序
|
||||||
|
const uploadFileTypeSortMap = {
|
||||||
|
"error": 1,
|
||||||
|
"uploading": 2,
|
||||||
|
"waiting": 3,
|
||||||
|
"finished": 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useFileUpload() {
|
||||||
|
|
||||||
|
let { storageKey, currentPath } = useRouterData();
|
||||||
|
const maxFileUploads = storageConfigStore.globalConfig.maxFileUploads;
|
||||||
|
|
||||||
|
// 监听文件拖拽上传事件
|
||||||
|
const listenDropFile = (dropArea) => {
|
||||||
|
// 拖拽进入, 显示提示.
|
||||||
|
dropArea = document.querySelector('body');
|
||||||
|
|
||||||
|
const dropOrPasteUpload = (e) => {
|
||||||
|
let isDrop = e.dataTransfer?.files?.length > 0;
|
||||||
|
|
||||||
|
// 关闭提示
|
||||||
|
removeDragClass();
|
||||||
|
dropState.value = false;
|
||||||
|
|
||||||
|
// 如果是拖拽上传,但已经打开了 dialog,则不处理上传.
|
||||||
|
if (hasDialog() && isDrop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 focus 在密码输入框,则不处理上传.
|
||||||
|
if (hasPasswordInputFocus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storageKey.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不允许文件操作,直接返回.
|
||||||
|
if (!storageConfigStore.permission.upload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//阻止事件冒泡
|
||||||
|
e.stopPropagation();
|
||||||
|
//阻止事件的默认行为
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 打开全屏 loading
|
||||||
|
const loadingInstance = ElLoading.service({
|
||||||
|
text: '文件读取中...',
|
||||||
|
background: 'rgba(0, 0, 0, .3)'
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = e.clipboardData?.items || e.dataTransfer?.items;
|
||||||
|
|
||||||
|
getFilesByDataTransferItemList(items).then((fileList) => {
|
||||||
|
// 关闭 loading
|
||||||
|
nextTick(() => {
|
||||||
|
// Loading should be closed asynchronously
|
||||||
|
loadingInstance.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (fileList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFileAction = () => {
|
||||||
|
visible.value = true;
|
||||||
|
fileList.forEach((item) => {
|
||||||
|
beforeUpload({
|
||||||
|
file: item
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件过多时,提示.
|
||||||
|
if (fileList.length > 100) {
|
||||||
|
ElMessageBox.confirm(`文件数量为 ${fileList.length} 个,是否确认上传?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'success',
|
||||||
|
callback: (action) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
uploadFileAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
uploadFileAction();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后拖拽的目标元素
|
||||||
|
let lastEnterTarget = null;
|
||||||
|
// 拖拽进入时,记录当前拖拽的目标元素,并添加防止拖拽穿透的 class
|
||||||
|
dropArea.addEventListener('dragenter', (event) => {
|
||||||
|
if (!storageKey.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不允许文件操作,直接返回.
|
||||||
|
if (!storageConfigStore.permission.upload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEnterTarget = event.target;
|
||||||
|
addDragClass();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
dropState.value = true;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// 拖拽进入后, 移动位置时, 记录拖拽状态
|
||||||
|
dropArea.addEventListener("dragover", function(event) {
|
||||||
|
if (!storageKey.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不允许文件操作,直接返回.
|
||||||
|
if (!storageConfigStore.permission.upload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
dropState.value = true;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// 拖拽离开, 移除防拖拽穿透 class, 并关闭拖拽提示
|
||||||
|
dropArea.addEventListener("dragleave", function(event) {
|
||||||
|
if (!storageKey.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不允许文件操作,直接返回.
|
||||||
|
if (!storageConfigStore.permission.upload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lastEnterTarget === event.target){
|
||||||
|
removeDragClass();
|
||||||
|
dropState.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// 拖拽放下时, 移除防拖拽穿透 class, 并关闭拖拽提示, 开始上传
|
||||||
|
dropArea.addEventListener('drop', dropOrPasteUpload, false);
|
||||||
|
|
||||||
|
// ctrl + v 粘贴文件时, 开始上传
|
||||||
|
dropArea.addEventListener('paste', dropOrPasteUpload, false);
|
||||||
|
|
||||||
|
// 根据拖拽或粘贴事件的 DataTransferItemList 列表获取文件列表
|
||||||
|
const getFilesByDataTransferItemList = async (dataTransferItemList) => {
|
||||||
|
// 储存获取到的文件列表
|
||||||
|
let fileList = [];
|
||||||
|
let DirectoryEntryList = [];
|
||||||
|
|
||||||
|
if (dataTransferItemList) {
|
||||||
|
// 拖拽对象列表转换成数组
|
||||||
|
let items = new Array(...dataTransferItemList);
|
||||||
|
// 获得 DirectoryEntry 对象列表
|
||||||
|
for (let index = 0; index < items.length; index++) {
|
||||||
|
let e = items[index];
|
||||||
|
let item = null;
|
||||||
|
// 兼容不同内核的浏览器
|
||||||
|
if (e.webkitGetAsEntry) {
|
||||||
|
item = e.webkitGetAsEntry();
|
||||||
|
if (!item) {
|
||||||
|
item = e.getAsFile();
|
||||||
|
if (item) {
|
||||||
|
fileList.push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.getAsEntry) {
|
||||||
|
item = e.getAsEntry();
|
||||||
|
} else {
|
||||||
|
ElMessage.warning("浏览器不支持拖拽上传");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DirectoryEntryList.push(item);
|
||||||
|
}
|
||||||
|
if (DirectoryEntryList.length > 0) {
|
||||||
|
for (let index = 0; index < DirectoryEntryList.length; index++) {
|
||||||
|
let item = DirectoryEntryList[index];
|
||||||
|
if (item) {
|
||||||
|
//获取文件夹目录
|
||||||
|
let FileTree = await getFileTree(item);
|
||||||
|
// 拿到目录下的所有文件
|
||||||
|
if (Array.isArray(FileTree)) {
|
||||||
|
//展平文件夹
|
||||||
|
flattenArray(FileTree, fileList);
|
||||||
|
} else {
|
||||||
|
//方便后续处理,单文件时也包装成数组
|
||||||
|
fileList.push(FileTree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 class, 防止拖拽穿透到子元素上
|
||||||
|
const addDragClass = () => {
|
||||||
|
dropArea.classList.add('dragging-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽完成, 移除 class
|
||||||
|
const removeDragClass = () => {
|
||||||
|
dropArea.classList.remove('dragging-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件
|
||||||
|
*/
|
||||||
|
function fileSync(item) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
item.file(res => {
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件夹下的文件
|
||||||
|
function readEntriesSync(dirReader) {
|
||||||
|
return new Promise((rel, rej) => {
|
||||||
|
dirReader.readEntries(res => {
|
||||||
|
rel(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展平数组
|
||||||
|
* @param array 需要展平的数组
|
||||||
|
* @param result 展平后的数组
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function flattenArray(array, result) {
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
if (Array.isArray(array[i])) {
|
||||||
|
flattenArray(array[i], result);
|
||||||
|
} else {
|
||||||
|
result.push(array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件目录结构树
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function getFileTree(item) {
|
||||||
|
let path = item.fullPath || "";
|
||||||
|
let dir = [];
|
||||||
|
if (item.isFile) {
|
||||||
|
let resFile = await fileSync(item);
|
||||||
|
resFile.dropUploadPath = path;
|
||||||
|
return resFile;
|
||||||
|
// item 为文件夹时
|
||||||
|
} else if (item.isDirectory) {
|
||||||
|
let dirReader = item.createReader();
|
||||||
|
let entries = await readEntriesSync(dirReader);
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
let proItem = await getFileTree(entries[i]);
|
||||||
|
dir.push(proItem);
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开上传文件 dialog
|
||||||
|
const openUploadDialog = () => {
|
||||||
|
visible.value = true;
|
||||||
|
uploadMode.value = 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开上传文件夹 dialog
|
||||||
|
const openUploadFolderDialog = () => {
|
||||||
|
visible.value = true;
|
||||||
|
uploadMode.value = 'folder';
|
||||||
|
nextTick(() => {
|
||||||
|
document.getElementsByClassName('el-upload__input')[0].webkitdirectory = true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件前的一些操作
|
||||||
|
* @param param
|
||||||
|
*/
|
||||||
|
const beforeUpload = (param) => {
|
||||||
|
uploadFile(param.file, param.uploadBasePath);
|
||||||
|
}
|
||||||
|
// 文件上传操作.
|
||||||
|
const uploadFile = (file, uploadBasePath) => {
|
||||||
|
const fileIndex = uploadIndex++;
|
||||||
|
|
||||||
|
uploadBasePath = uploadBasePath || currentPath.value;
|
||||||
|
|
||||||
|
let uploadToPath = uploadBasePath;
|
||||||
|
|
||||||
|
// 如果包含 webkitRelativePath, 则表示是文件夹上传, 需要获取文件完整路径
|
||||||
|
if (file.webkitRelativePath || file.dropUploadPath) {
|
||||||
|
let pathStr = file.webkitRelativePath || file.dropUploadPath;
|
||||||
|
if (!pathStr.startsWith('/')) {
|
||||||
|
pathStr = '/' + pathStr;
|
||||||
|
}
|
||||||
|
let pathList = pathStr.split('/');
|
||||||
|
pathList.forEach((item, index) => {
|
||||||
|
let isFirstItem = 0 === index;
|
||||||
|
let isLastItem = pathList.length - 1 === index;
|
||||||
|
|
||||||
|
if (isFirstItem || isLastItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
uploadToPath += ('/' + item);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let param = {
|
||||||
|
storageKey: 'minio',
|
||||||
|
path: localStorage.getItem('filepath'),
|
||||||
|
name: file.name,
|
||||||
|
size: file.size
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('当前上传信息:', param, ', 当前同时上传文件数:',uploadProgressInfoStatistics.value.totalUploading, '限制同时上传文件数:', maxFileUploads);
|
||||||
|
if (uploadProgressInfoStatistics.value.totalUploading >= maxFileUploads) {
|
||||||
|
console.log(`上传文件数超出 ${maxFileUploads}, 等待上传`);
|
||||||
|
waitingFileList.push({
|
||||||
|
index: fileIndex,
|
||||||
|
file: file,
|
||||||
|
uploadBasePath: uploadBasePath,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploadFileInfo = {
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
speed: '-',
|
||||||
|
progress: 0,
|
||||||
|
loaded: 0,
|
||||||
|
status: 'uploading',
|
||||||
|
startTime: Date.now(),
|
||||||
|
file: file,
|
||||||
|
index: fileIndex
|
||||||
|
}
|
||||||
|
uploadingFileList.push(uploadFileInfo);
|
||||||
|
cancelTokenSourceMap.set(fileIndex, axios.CancelToken.source());
|
||||||
|
uploadingFileMap.set(fileIndex, uploadingFileList[uploadingFileList.length - 1]);
|
||||||
|
|
||||||
|
uploadFileReq(param).then((res) => {
|
||||||
|
const { on } = useEventBus(`cancel-upload-${fileIndex}`);
|
||||||
|
on(() => {
|
||||||
|
let cancelTokenSource = cancelTokenSourceMap.get(fileIndex);
|
||||||
|
if (cancelTokenSource) {
|
||||||
|
cancelTokenSource.cancel();
|
||||||
|
uploadingFileList.find((item, index) => {
|
||||||
|
let b = item.name === file.name;
|
||||||
|
if (b) {
|
||||||
|
uploadingFileList.splice(index, 1);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let proxyUploadType = common.storageType.proxyType;
|
||||||
|
let s3UploadType = common.storageType.s3Type;
|
||||||
|
let onedriveUploadType = common.storageType.micrsoftType;
|
||||||
|
|
||||||
|
if (proxyUploadType.includes( param.storageKey)) {
|
||||||
|
fileProxyUpload(file, res.data, fileIndex);
|
||||||
|
} else if (s3UploadType.includes( param.storageKey)) {
|
||||||
|
s3FileUpload(file, res.data, fileIndex);
|
||||||
|
} else if (onedriveUploadType.includes( param.storageKey)) {
|
||||||
|
onedriveUpload(file, res.data, fileIndex);
|
||||||
|
} else if ( param.storageKey === 'upyun') {
|
||||||
|
upyunFileUpload(file, res.data, fileIndex);
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
baseOnUploadError(fileIndex, err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务器代理上传
|
||||||
|
const fileProxyUpload = (file, uploadUrl, fileIndex) => {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
axios.post(uploadUrl, formData, {
|
||||||
|
cancelToken: cancelTokenSourceMap.get(fileIndex).token,
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
baseOnUploadProgress(progressEvent, fileIndex, true);
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
baseOnUploadFinish(fileIndex);
|
||||||
|
}).catch((err) => {
|
||||||
|
baseOnUploadError(fileIndex, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// s3 通用上传
|
||||||
|
const s3FileUpload = (file, uploadUrl, fileIndex) => {
|
||||||
|
axios.put(uploadUrl, file, {
|
||||||
|
withCredentials: false,
|
||||||
|
cancelToken: cancelTokenSourceMap.get(fileIndex).token,
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
baseOnUploadProgress(progressEvent, fileIndex);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
baseOnUploadFinish(fileIndex);
|
||||||
|
}).catch((err) => {
|
||||||
|
baseOnUploadError(fileIndex, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OneDrive SharePoint 上传
|
||||||
|
const onedriveUpload = (file, uploadUrl, fileIndex) => {
|
||||||
|
let index = 1; // 当前块数
|
||||||
|
let start = 0; // 开始字节数
|
||||||
|
let end = 0; // 结束字节数
|
||||||
|
let fileSize = file.size; // 文件大小
|
||||||
|
const MAX_FILE_SIZE = 104857599; // 每块大小 100M
|
||||||
|
|
||||||
|
if (fileSize === 0) {
|
||||||
|
baseOnUploadError(fileIndex, '当前存储类型不支持上传空文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分块上传
|
||||||
|
const uploadBlock = () => {
|
||||||
|
// 计算每块的开始和结束位置
|
||||||
|
if (start + MAX_FILE_SIZE >= fileSize) {
|
||||||
|
end = fileSize;
|
||||||
|
} else {
|
||||||
|
end = start + MAX_FILE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > 1) {
|
||||||
|
cancelTokenSourceMap.set(fileIndex, axios.CancelToken.source());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截取文件每块上传
|
||||||
|
let fileBlock = file.slice(start, end);
|
||||||
|
axios.put(`${uploadUrl}`, fileBlock, {
|
||||||
|
cancelToken: cancelTokenSourceMap.get(fileIndex).token,
|
||||||
|
timeout: 10000000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Range': `bytes ${start}-${end - 1}/${file.size}`
|
||||||
|
},
|
||||||
|
type: 'sync',
|
||||||
|
withCredentials: false,
|
||||||
|
onUploadProgress: progressEvent => {
|
||||||
|
if (progressEvent.lengthComputable) {
|
||||||
|
let uploadFileInfo = uploadingFileMap.get(fileIndex);
|
||||||
|
|
||||||
|
const realLoaded = progressEvent.loaded + start;
|
||||||
|
|
||||||
|
uploadFileInfo.size = fileSize;
|
||||||
|
uploadFileInfo.loaded = realLoaded;
|
||||||
|
uploadFileInfo.progress = Math.round(realLoaded / fileSize * 100);
|
||||||
|
uploadFileInfo.speed = common.fileSizeFormat(Math.round(realLoaded / (Date.now() - uploadFileInfo.startTime) * 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status === 202) {
|
||||||
|
start += MAX_FILE_SIZE;
|
||||||
|
index += 1;
|
||||||
|
uploadBlock();
|
||||||
|
} else if (response.status === 201 || response.status === 200) {
|
||||||
|
// console.log('file upload full success.', start, end);
|
||||||
|
baseOnUploadFinish(fileIndex);
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
baseOnUploadError(fileIndex, e)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 又拍云上传
|
||||||
|
const upyunFileUpload = (file, uploadUrl, fileIndex) => {
|
||||||
|
let uploadInfo = JSON.parse(uploadUrl);
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append('name', file.name);
|
||||||
|
formData.append("authorization", uploadInfo.signature);
|
||||||
|
formData.append("policy", uploadInfo.policy);
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
axios.post(uploadInfo.url, formData, {
|
||||||
|
withCredentials: false,
|
||||||
|
cancelToken: cancelTokenSourceMap.get(fileIndex).token,
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
baseOnUploadProgress(progressEvent, fileIndex);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
baseOnUploadFinish(fileIndex);
|
||||||
|
}).catch((err) => {
|
||||||
|
baseOnUploadError(fileIndex, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用上传结束设置.
|
||||||
|
const baseOnUploadError = (fileIndex, err) => {
|
||||||
|
let uploadFileInfo = uploadingFileMap.get(fileIndex);
|
||||||
|
uploadFileInfo.status = 'error';
|
||||||
|
uploadFileInfo.endTime = Date.now();
|
||||||
|
uploadFileInfo.msg = err?.response?.data || err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用上传结束设置.
|
||||||
|
const baseOnUploadFinish = (fileIndex) => {
|
||||||
|
let uploadFileInfo = uploadingFileMap.get(fileIndex);
|
||||||
|
uploadFileInfo.progress = 100;
|
||||||
|
uploadFileInfo.status = 'finished';
|
||||||
|
uploadFileInfo.endTime = Date.now();
|
||||||
|
uploadFileInfo.msg = '上传成功';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用上传进度条处理
|
||||||
|
const baseOnUploadProgress = (progressEvent, fileIndex, isProxyUpload) => {
|
||||||
|
let uploadFileInfo = uploadingFileMap.get(fileIndex);
|
||||||
|
uploadFileInfo.size = progressEvent.total;
|
||||||
|
uploadFileInfo.loaded = progressEvent.loaded;
|
||||||
|
uploadFileInfo.progress = Math.round(progressEvent.loaded / progressEvent.total * 100);
|
||||||
|
uploadFileInfo.speed = common.fileSizeFormat(Math.round(progressEvent.loaded / (Date.now() - uploadFileInfo.startTime) * 1000));
|
||||||
|
|
||||||
|
console.log('uploadFileInfo', uploadFileInfo, isProxyUpload);
|
||||||
|
if (isProxyUpload && uploadFileInfo.progress === 100) {
|
||||||
|
uploadFileInfo.msg = '上传完成, 服务器中转中...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消上传请求
|
||||||
|
const cancelUpload = (item) => {
|
||||||
|
ElMessageBox.confirm(`是否确定取消文件 ${item.name} 上传?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '返回',
|
||||||
|
type: 'warning',
|
||||||
|
callback: action => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
useEventBus(`cancel-upload-${item.index}`).emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传文件信息
|
||||||
|
const uploadProgressInfoStatistics = computed(() => {
|
||||||
|
let totalSize = uploadingFileList.length;
|
||||||
|
let totalUploading = 0;
|
||||||
|
let totalFinish = 0;
|
||||||
|
uploadingFileList.forEach((item) => {
|
||||||
|
if (item.status === 'uploading') {
|
||||||
|
totalUploading++;
|
||||||
|
} else if (item.status === 'finished') {
|
||||||
|
totalFinish++;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let totalUploadingAndWaiting = totalUploading + waitingFileList.length;
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalSize,
|
||||||
|
totalUploading,
|
||||||
|
totalFinish,
|
||||||
|
totalUploadingAndWaiting
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上传文件排序结果
|
||||||
|
const uploadProgressInfoSorted = computed(() => {
|
||||||
|
let result = [];
|
||||||
|
result.push(...uploadingFileList);
|
||||||
|
waitingFileList.forEach((item) => {
|
||||||
|
result.push({
|
||||||
|
name: item.file.name,
|
||||||
|
size: item.file.size,
|
||||||
|
status: 'waiting',
|
||||||
|
msg: '排队中...',
|
||||||
|
index: item.index
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
result.sort((a, b) => {
|
||||||
|
let aStatus = a.status;
|
||||||
|
let bStatus = b.status;
|
||||||
|
if (aStatus !== bStatus) {
|
||||||
|
return uploadFileTypeSortMap[aStatus] - uploadFileTypeSortMap[bStatus];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.startTime !== b.startTime) {
|
||||||
|
return a.startTime - b.startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果状态一样,则按照开始时间排序
|
||||||
|
return a.endTime - b.endTime;
|
||||||
|
});
|
||||||
|
console.log('uploadProgressInfoSorted', result);
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isInitWatch) {
|
||||||
|
watch(() => uploadProgressInfoStatistics.value.totalUploading, (newValue) => {
|
||||||
|
if (newValue < maxFileUploads) {
|
||||||
|
console.log('检测到上传中的文件个数小于最大上传限制.');
|
||||||
|
if (waitingFileList.length === 0) {
|
||||||
|
console.log('等待上传的文件数为 0, 无需继续上传.');
|
||||||
|
} else {
|
||||||
|
let spliceList = waitingFileList.splice(0, 1);
|
||||||
|
let fileItem = spliceList[0];
|
||||||
|
beforeUpload({
|
||||||
|
file: fileItem.file,
|
||||||
|
uploadBasePath: fileItem.uploadBasePath
|
||||||
|
});
|
||||||
|
console.log('开始从等待队列中获取上传文件: ', fileItem.file.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
isInitWatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeUploadFileByIndex = (fileIndex) => {
|
||||||
|
if (fileIndex === null || fileIndex === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let removeIndex = uploadingFileList.findIndex((item, index) => {
|
||||||
|
if (item.index === fileIndex) {
|
||||||
|
uploadingFileList.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (removeIndex === -1) {
|
||||||
|
removeIndex = waitingFileList.findIndex((item, index) => {
|
||||||
|
if (item.index === fileIndex) {
|
||||||
|
waitingFileList.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeIndex !== -1) {
|
||||||
|
uploadingFileMap.delete(fileIndex)
|
||||||
|
cancelTokenSourceMap.delete(fileIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearALlFinishedUploadFile = () => {
|
||||||
|
let deleteCount = 0;
|
||||||
|
for (let i = uploadingFileList.length - 1; i >= 0; i--) {
|
||||||
|
let item = uploadingFileList[i];
|
||||||
|
if (item.status === 'finished') {
|
||||||
|
deleteCount++;
|
||||||
|
uploadingFileList.splice(i, 1);
|
||||||
|
uploadingFileMap.delete(item.index)
|
||||||
|
cancelTokenSourceMap.delete(item.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deleteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryUpload = (item) => {
|
||||||
|
console.log('重新上传文件', item);
|
||||||
|
removeUploadFileByIndex(item.index);
|
||||||
|
uploadFile(item.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible, uploadMode, openUploadDialog, openUploadFolderDialog, cancelUpload, dropState, listenDropFile,
|
||||||
|
beforeUpload, uploadFile, uploadProgressInfoSorted, uploadProgressInfoStatistics,
|
||||||
|
clearALlFinishedUploadFile, removeUploadFileByIndex, retryUpload
|
||||||
|
}
|
||||||
|
}
|
193
web/src/components/file/file/useTableOperator.js
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { useKeyModifier } from '@vueuse/core'
|
||||||
|
import {removeDuplicateSlashes} from "fast-glob/out/managers/patterns";
|
||||||
|
|
||||||
|
import useGlobalConfigStore from "@/components/file/stores/global-config";
|
||||||
|
let globalConfigStore = useGlobalConfigStore();
|
||||||
|
|
||||||
|
import useStorageConfigStore from "@/components/file/stores/storage-config";
|
||||||
|
let storageConfigStore = useStorageConfigStore();
|
||||||
|
|
||||||
|
import useFileDataStore from "@/components/file/stores/file-data";
|
||||||
|
let fileDataStore = useFileDataStore();
|
||||||
|
import { ref,computed,watch} from "vue";
|
||||||
|
// 按键监听
|
||||||
|
const mateState = useKeyModifier('Meta');
|
||||||
|
const controlState = useKeyModifier('Control');
|
||||||
|
const shiftState = useKeyModifier('Shift');
|
||||||
|
|
||||||
|
// 是否按住了 Ctrl(Windows) 或者 Command(Mac) 键
|
||||||
|
let isMultiSelectState = computed(() => {
|
||||||
|
return mateState.value || controlState.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
import useFileOperator from "@/components/file/file/useFileOperator";
|
||||||
|
let { batchDelete } = useFileOperator();
|
||||||
|
|
||||||
|
import useFileData from "@/components/file/file/useFileData";
|
||||||
|
const { skeletonLoading, openRow } = useFileData();
|
||||||
|
|
||||||
|
import useFileSelect from "@/components/file/file/useFileSelect";
|
||||||
|
const { selectRows, selectRow, clearSelection, toggleRowSelection, toggleAllSelection } = useFileSelect();
|
||||||
|
|
||||||
|
// ctrl + a 全选
|
||||||
|
window.addEventListener("keydown", function(e) {
|
||||||
|
if (e.key === 'Escape' && allowShortcuts()) {
|
||||||
|
clearSelection();
|
||||||
|
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey) && allowShortcuts()) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleAllSelection();
|
||||||
|
} else if (e.key === 'Delete' && allowShortcuts()) { // 如果按了删除键,且当前状态允许快捷键操作
|
||||||
|
if (batchDelete && selectRows?.value?.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
batchDelete();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Backspace' && allowShortcuts()) { // 如果按了删除键,且当前状态允许快捷键操作
|
||||||
|
if (fileDataStore.fileList.length > 0 && fileDataStore.fileList[0].type === 'BACK') {
|
||||||
|
tableClickRow(fileDataStore.fileList[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
export const allowShortcuts = () => {
|
||||||
|
// 仅鼠标悬浮在 table 上时且没有打开 dialog 时,才允许快捷键操作.
|
||||||
|
return hoverBody() && hasDialog() === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否悬浮在首页上
|
||||||
|
export const hoverBody = () => {
|
||||||
|
// 仅鼠标悬浮在 table 上时且没有打开 dialog 时,才允许快捷键操作.
|
||||||
|
return document.querySelector(".zfile-index-body:hover")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前是否打开了 dialog
|
||||||
|
export const hasDialog = () => {
|
||||||
|
return !!document.querySelector(".el-popup-parent--hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前是否聚焦在密码输入框
|
||||||
|
export const hasPasswordInputFocus = () => {
|
||||||
|
return document.querySelector(".is-message-box .el-input__inner") === document.activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽选择相关
|
||||||
|
// 开始拖拽的文件行索引
|
||||||
|
const startDragClickIndex = ref(-1);
|
||||||
|
// 结束拖拽的文件行索引
|
||||||
|
const endDragClickIndex = ref(-1);
|
||||||
|
// 是否按下鼠标左键
|
||||||
|
import { useMousePressed } from '@vueuse/core'
|
||||||
|
const { pressed } = useMousePressed()
|
||||||
|
|
||||||
|
watch(() => pressed.value, (value, oldValue) => {
|
||||||
|
// 如果之前是点击状态, 现在松开了则清除记录的拖拽索引
|
||||||
|
if (value === false && oldValue === true) {
|
||||||
|
startDragClickIndex.value = -1;
|
||||||
|
endDragClickIndex.value = -1;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
let tableClickRow;
|
||||||
|
|
||||||
|
export default function useTableOperator() {
|
||||||
|
|
||||||
|
// 文件单击事件
|
||||||
|
tableClickRow = (row, event) => {
|
||||||
|
if (event === undefined) {
|
||||||
|
openRow(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isClickSelection = event.type === 'selection';
|
||||||
|
|
||||||
|
// 如果点击的是文件或文件夹, 且点击的不是 checkbox 列, 且操作习惯是单击打开, 则打开文件/文件夹
|
||||||
|
if (!isClickSelection && storageConfigStore.globalConfig.fileClickMode === 'click') {
|
||||||
|
openRow(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载骨架屏时,点击无效.
|
||||||
|
if (skeletonLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isClickSelf = selectRows.value.length === 1 && selectRow.value?.name === row.name;
|
||||||
|
|
||||||
|
// 如果按住了 shift 选中
|
||||||
|
if (shiftState.value) {
|
||||||
|
|
||||||
|
// 上一个选择的 index
|
||||||
|
let prevSelectIndex = fileDataStore.fileList.findIndex(value => value.name === selectRow.value.name);
|
||||||
|
|
||||||
|
// 如果选中行之前也按 shift 选中过行,表示要进行多选
|
||||||
|
if (prevSelectIndex !== null) {
|
||||||
|
let currentShiftIndex = fileDataStore.fileList.findIndex(value => value.name === row.name);
|
||||||
|
|
||||||
|
let start = Math.min(currentShiftIndex, prevSelectIndex);
|
||||||
|
let end = Math.max(currentShiftIndex, prevSelectIndex);
|
||||||
|
|
||||||
|
for (let i = start + 1; i < end; i++) {
|
||||||
|
let item = fileDataStore.fileList[i];
|
||||||
|
toggleRowSelection(item, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果不是以下情况,则取消所有选择:
|
||||||
|
// 1. 多选状态:按住 command (Mac) 或 Ctrl (Windows)
|
||||||
|
// 2. 选中当前文件: 如目前只选中了一个文件,且就是当前文件, 则不需要取消所有选择, 取消后, 无法 toggle 当前行.
|
||||||
|
// 3. 点击的区域不是 selection, 如果点击的是 selection, 则可能为误触, 实际想点击的是 checkbox
|
||||||
|
else if (!isMultiSelectState.value && !isClickSelf && !isClickSelection) {
|
||||||
|
clearSelection();
|
||||||
|
}
|
||||||
|
toggleRowSelection(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件双击事件
|
||||||
|
const tableDbClickRow = (row) => {
|
||||||
|
// 加载骨架屏时,点击无效.
|
||||||
|
if (skeletonLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入悬浮事件
|
||||||
|
const tableHoverRow = (row, column, cell, event) => {
|
||||||
|
// 如果当前是点击状态,且开始拖拽选中索引不为 -1, 表示要进行拖拽了.
|
||||||
|
if (event.buttons === 1 && startDragClickIndex.value !== -1) {
|
||||||
|
// 将开始拖拽的行选中
|
||||||
|
if (endDragClickIndex.value === -1) {
|
||||||
|
clearSelection();
|
||||||
|
let item = fileDataStore.fileList[startDragClickIndex.value];
|
||||||
|
toggleRowSelection(item, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let newEndDragClickIndex = row.index;
|
||||||
|
|
||||||
|
let oldEndDragClickIndex = endDragClickIndex.value >= 0 ? endDragClickIndex.value : newEndDragClickIndex - 1;
|
||||||
|
|
||||||
|
// 可能是倒序拖拽的,所以要区分 start 和 end
|
||||||
|
let start = Math.min(oldEndDragClickIndex, newEndDragClickIndex);
|
||||||
|
let end = Math.max(oldEndDragClickIndex, newEndDragClickIndex);
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
let item = fileDataStore.fileList[i];
|
||||||
|
toggleRowSelection(item, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
endDragClickIndex.value = newEndDragClickIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 离开悬浮事件
|
||||||
|
const tableLeaveRow = (row, column, cell, event) => {
|
||||||
|
if (event.buttons === 1 && startDragClickIndex.value === -1) {
|
||||||
|
startDragClickIndex.value = row.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tableClickRow, tableDbClickRow,
|
||||||
|
tableHoverRow, tableLeaveRow
|
||||||
|
}
|
||||||
|
}
|
112
web/src/components/file/messageBox/confirm/confirm.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<TransitionRoot as="template" :show="show">
|
||||||
|
<Dialog as="div" class="relative z-10" @close="handlerClose('close')">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
||||||
|
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||||
|
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 w-11/12 max-w-sm sm:w-full sm:max-w-md sm:p-6">
|
||||||
|
<div class="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
|
||||||
|
<div type="button" class="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" @click="handlerClose('close')">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<XMarkIcon class="h-7 w-7" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full space-y-3">
|
||||||
|
<DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900">{{ title }}</DialogTitle>
|
||||||
|
<div class="flex flex-row items-center space-x-2">
|
||||||
|
<div>
|
||||||
|
<InformationCircleIcon v-if="type === 'info'" class="h-7 w-7 text-gray-400" aria-hidden="true" />
|
||||||
|
<ExclamationCircleIcon v-else-if="type === 'warning'" class="h-7 w-7 text-yellow-500" aria-hidden="true" />
|
||||||
|
<XCircleIcon v-else-if="type === 'error'" class="h-7 w-7 text-red-400" aria-hidden="true" />
|
||||||
|
<CheckCircleIcon v-else-if="type === 'success'" class="h-7 w-7 text-green-500" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500 text-sm" v-if="dangerouslyUseHTMLString" v-html="message"></p>
|
||||||
|
<p class="text-gray-500 text-sm" v-else>{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="checkbox ? 'sm:justify-between' : 'sm:justify-end'"
|
||||||
|
class="mt-5 sm:mt-4 sm:flex">
|
||||||
|
<div class="space-y-4 sm:space-y-0 sm:space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
||||||
|
@click="handlerClose('cancel')">
|
||||||
|
{{ props.cancelButtonText }}
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex w-full justify-center rounded-md bg-blue-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 sm:ml-3 sm:w-auto" @click="handlerClose('confirm')">
|
||||||
|
{{ confirmButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import { XMarkIcon } from '@heroicons/vue/24/outline'
|
||||||
|
import { InformationCircleIcon, ExclamationCircleIcon, XCircleIcon, CheckCircleIcon} from '@heroicons/vue/24/solid'
|
||||||
|
import { defineProps, defineEmits } from 'vue'
|
||||||
|
import { MessageType } from "./types";
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String as () => MessageType,
|
||||||
|
default: 'info'
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
confirmButtonText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Confirm',
|
||||||
|
},
|
||||||
|
cancelButtonText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Cancel',
|
||||||
|
},
|
||||||
|
onClose: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
dangerouslyUseHTMLString: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 响应关闭事件
|
||||||
|
const emit = defineEmits(['update:show'])
|
||||||
|
const handlerClose = (type: 'cancel' | 'close' | 'confirm') => {
|
||||||
|
emit('update:show', false)
|
||||||
|
props.onClose(type)
|
||||||
|
}
|
||||||
|
</script>
|
27
web/src/components/file/messageBox/confirm/index.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import Confirm from "./confirm.vue";
|
||||||
|
import { Props } from "./types";
|
||||||
|
|
||||||
|
const createDialog = (message: string, title: string, options: Props = {}) => new Promise((resolve, reject) => {
|
||||||
|
const mountNode = document.createElement('div')
|
||||||
|
const Instance = createApp(Confirm, {
|
||||||
|
show: true,
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
...options,
|
||||||
|
onClose: (type: string) => {
|
||||||
|
if (type === 'confirm') {
|
||||||
|
resolve(type);
|
||||||
|
} else {
|
||||||
|
reject(type);
|
||||||
|
}
|
||||||
|
Instance.unmount();
|
||||||
|
document.body.removeChild(mountNode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.body.appendChild(mountNode)
|
||||||
|
Instance.mount(mountNode)
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createDialog;
|
12
web/src/components/file/messageBox/confirm/types.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export declare type MessageType = '' | 'success' | 'warning' | 'info' | 'error';
|
||||||
|
|
||||||
|
// 声明 Options 类型
|
||||||
|
export interface Props {
|
||||||
|
type?: MessageType;
|
||||||
|
title?: String;
|
||||||
|
message?: String;
|
||||||
|
confirmButtonText?: String;
|
||||||
|
cancelButtonText?: String;
|
||||||
|
dangerouslyUseHTMLString?: Boolean;
|
||||||
|
|
||||||
|
}
|
9
web/src/components/file/messageBox/messageBox.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import openPrompt from "./prompt/index";
|
||||||
|
import openConfirm from "./confirm/index";
|
||||||
|
|
||||||
|
const MessageBox = {
|
||||||
|
prompt: openPrompt,
|
||||||
|
confirm: openConfirm
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessageBox;
|
27
web/src/components/file/messageBox/prompt/index.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import Prompt from "./prompt.vue";
|
||||||
|
import { ConfirmResult, Props } from "./types";
|
||||||
|
|
||||||
|
const createDialog = (message: string, title: string, options: Props = {}) => new Promise((resolve, reject) => {
|
||||||
|
const mountNode = document.createElement('div')
|
||||||
|
const Instance = createApp(Prompt, {
|
||||||
|
show: true,
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
...options,
|
||||||
|
onClose: (res: ConfirmResult) => {
|
||||||
|
if (res.type === 'confirm') {
|
||||||
|
resolve(res);
|
||||||
|
} else {
|
||||||
|
reject(res);
|
||||||
|
}
|
||||||
|
Instance.unmount();
|
||||||
|
document.body.removeChild(mountNode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.body.appendChild(mountNode)
|
||||||
|
Instance.mount(mountNode)
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createDialog;
|
171
web/src/components/file/messageBox/prompt/prompt.vue
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<TransitionRoot as="template" :show="show">
|
||||||
|
<Dialog as="div" class="relative z-10" @close="handlerClose('close')">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100" leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
||||||
|
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100" leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||||
|
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 w-11/12 max-w-sm sm:w-full sm:max-w-md sm:p-6">
|
||||||
|
<div class="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
|
||||||
|
<div type="button" class="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" @click="handlerClose('close')">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<PencilIcon class="h-6 w-6 text-blue-600" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full space-y-1">
|
||||||
|
<DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900">{{ title }}</DialogTitle>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500 text-sm" v-if="dangerouslyUseHTMLString" v-html="message"></p>
|
||||||
|
<p class="text-gray-500 text-sm" v-else>{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
<el-input :placeholder="inputPlaceholder"
|
||||||
|
v-model="form.inputModel"
|
||||||
|
:type="inputType"
|
||||||
|
@keyup.enter="handlerClose('confirm')"
|
||||||
|
:autofocus="true"
|
||||||
|
:show-password="inputType === 'password' && showPassword"
|
||||||
|
class="h-10"></el-input>
|
||||||
|
<div class="text-red-500 text-sm text-left" v-if="errorFields?.inputModel?.length">{{ errorFields.inputModel[0].message }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="checkbox ? 'sm:justify-between' : 'sm:justify-end'"
|
||||||
|
class="mt-5 sm:mt-4 sm:flex">
|
||||||
|
<div class="sm:ml-14" v-if="checkbox">
|
||||||
|
<el-checkbox v-model="form.checkboxModel">{{ checkboxLabel }}</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4 sm:space-y-0 sm:space-x-2">
|
||||||
|
<button type="button"
|
||||||
|
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
||||||
|
@click="handlerClose('cancel')">
|
||||||
|
{{ props.cancelButtonText }}
|
||||||
|
</button>
|
||||||
|
<button :disabled="!pass" type="button"
|
||||||
|
:class="pass ? '' : 'cursor-not-allowed opacity-50'"
|
||||||
|
class="inline-flex w-full justify-center rounded-md bg-blue-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 sm:ml-3 sm:w-auto" @click="handlerClose('confirm')">
|
||||||
|
{{ confirmButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
DialogTitle,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import { XMarkIcon, PencilIcon } from '@heroicons/vue/24/outline'
|
||||||
|
import { useAsyncValidator } from '@vueuse/integrations/useAsyncValidator'
|
||||||
|
import { MaybeComputedRef } from "@vueuse/shared";
|
||||||
|
import { Rules } from "async-validator";
|
||||||
|
import { ConfirmResult } from "./types";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
confirmButtonText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Confirm',
|
||||||
|
},
|
||||||
|
cancelButtonText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Cancel',
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
checkboxLabel: {
|
||||||
|
type: String,
|
||||||
|
default: 'Remember me',
|
||||||
|
},
|
||||||
|
defaultChecked: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
inputType: {
|
||||||
|
type: String,
|
||||||
|
default: 'text',
|
||||||
|
},
|
||||||
|
inputDefault: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
inputPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请输入',
|
||||||
|
},
|
||||||
|
showPassword: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
inputValidator: {
|
||||||
|
type: Function,
|
||||||
|
default: () => true,
|
||||||
|
},
|
||||||
|
inputErrorMessage: {
|
||||||
|
type: String,
|
||||||
|
default: 'input error',
|
||||||
|
},
|
||||||
|
onClose: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
dangerouslyUseHTMLString: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单数据和校验规则
|
||||||
|
const form = ref({ inputModel: props.inputDefault, checkboxModel: props.defaultChecked })
|
||||||
|
const rules:MaybeComputedRef<Rules> = {
|
||||||
|
inputModel: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
message: props.inputErrorMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pass, errorFields } = useAsyncValidator(form, rules)
|
||||||
|
|
||||||
|
// 响应关闭事件
|
||||||
|
const emit = defineEmits(['update:show'])
|
||||||
|
const handlerClose = (type: 'cancel' | 'close' | 'confirm') => {
|
||||||
|
const result: ConfirmResult = {
|
||||||
|
checkbox: form.value.checkboxModel,
|
||||||
|
value: form.value.inputModel,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:show', false)
|
||||||
|
props.onClose(result)
|
||||||
|
}
|
||||||
|
</script>
|
25
web/src/components/file/messageBox/prompt/types.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// 声明返回值类型
|
||||||
|
export interface ConfirmResult {
|
||||||
|
checkbox: boolean
|
||||||
|
value: string
|
||||||
|
type: 'cancel' | 'close' | 'confirm'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 声明 Options 类型
|
||||||
|
export interface Props {
|
||||||
|
title?: String;
|
||||||
|
message?: String;
|
||||||
|
confirmButtonText?: String;
|
||||||
|
cancelButtonText?: String;
|
||||||
|
inputType?: "text" | "password";
|
||||||
|
inputPlaceholder?: String;
|
||||||
|
inputDefault?: String;
|
||||||
|
showPassword?: Boolean;
|
||||||
|
checkbox?: Boolean;
|
||||||
|
checkboxLabel?: String;
|
||||||
|
readonly defaultChecked?: Boolean;
|
||||||
|
inputValidator?: Function;
|
||||||
|
inputErrorMessage?: String;
|
||||||
|
dangerouslyUseHTMLString?: Boolean;
|
||||||
|
}
|
107
web/src/components/file/stores/file-data.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import common from "@/components/file/common";
|
||||||
|
import useStorageConfigStore from "./storage-config";
|
||||||
|
|
||||||
|
// 当前存储源的配置信息,数据来源为服务端配置。请求存储源后会获取其配置信息。
|
||||||
|
const useFileDataStore = defineStore('fileDataStore', {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
currentClickRow: {},
|
||||||
|
currentRightClickRow: {},
|
||||||
|
currentStorageSource: {
|
||||||
|
id: null,
|
||||||
|
type: {
|
||||||
|
description: '',
|
||||||
|
key: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imgMode: false,
|
||||||
|
newImgMode: false,
|
||||||
|
oldStorageKey: null,
|
||||||
|
searchParam: '',
|
||||||
|
fileListSource: [],
|
||||||
|
loadFileSize: -1,
|
||||||
|
audioArray: [],
|
||||||
|
audioIndex: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
filterFileByType: (state) => {
|
||||||
|
return (type: string) => {
|
||||||
|
return state.fileListSource.filter(function (item:any) {
|
||||||
|
if (item.type === 'BACK') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let name = item.name;
|
||||||
|
let suffix = name.substr(name.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
return common.constant.fileTypeMap[type].indexOf(suffix) !== -1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fileList: state => {
|
||||||
|
if (state.loadFileSize === -1) return [];
|
||||||
|
let firstIsBack = state.fileListSource[0]?.type === 'BACK';
|
||||||
|
let toSize = firstIsBack ? state.loadFileSize + 1 : state.loadFileSize;
|
||||||
|
toSize = toSize > state.fileListSource.length ? state.fileListSource.length : toSize;
|
||||||
|
let tableData = state.fileListSource.slice(0, toSize);
|
||||||
|
tableData.forEach((item:any) => {
|
||||||
|
// 生成图标
|
||||||
|
if (!item.icon) {
|
||||||
|
item['icon'] = common.getFileIconName(item);
|
||||||
|
}
|
||||||
|
if (item.preview !== null) {
|
||||||
|
// 获取文件类型
|
||||||
|
let fileType = common.getFileType(item.name);
|
||||||
|
if (fileType) {
|
||||||
|
// 获取文件是否可预览
|
||||||
|
item['fileType'] = fileType;
|
||||||
|
item.preview = common.constant.previewFileType.indexOf(fileType) !== -1;
|
||||||
|
} else {
|
||||||
|
item.preview = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tableData;
|
||||||
|
},
|
||||||
|
getFileUrlByName: state => {
|
||||||
|
return (name: string) => {
|
||||||
|
let item = state.fileListSource.find((item:any) => item.name === name);
|
||||||
|
if (item) {
|
||||||
|
return item.url;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
updateCurrentStorageSource(val: any) {
|
||||||
|
this.currentStorageSource = val;
|
||||||
|
},
|
||||||
|
updateAudioIndex(val: any) {
|
||||||
|
this.audioIndex = val;
|
||||||
|
},
|
||||||
|
updateAudioList(val: any) {
|
||||||
|
this.audioArray = val;
|
||||||
|
},
|
||||||
|
updateCurrentClickRow(val: any) {
|
||||||
|
this.currentClickRow = val;
|
||||||
|
},
|
||||||
|
updateCurrentRightClickRow(val: any) {
|
||||||
|
this.currentRightClickRow = val;
|
||||||
|
},
|
||||||
|
updateFileList(val: any) {
|
||||||
|
this.fileListSource = val;
|
||||||
|
this.loadFileSize = useStorageConfigStore().globalConfig.maxShowSize;
|
||||||
|
},
|
||||||
|
updateOldStorageKey(val: any) {
|
||||||
|
this.oldStorageKey = val;
|
||||||
|
},
|
||||||
|
updateLoadFileSize(val: number) {
|
||||||
|
this.loadFileSize = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default useFileDataStore;
|
42
web/src/components/file/stores/global-config.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
// 全局配置信息, 初始化自 /public/zfile.config.json 文件中.
|
||||||
|
const useGlobalConfigStore = defineStore('globalConfigStore', {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
zfileConfig: {
|
||||||
|
baseUrl: "",
|
||||||
|
router: {
|
||||||
|
mode: "history"
|
||||||
|
},
|
||||||
|
skeleton: {
|
||||||
|
enable: true,
|
||||||
|
show: "always",
|
||||||
|
size: 20
|
||||||
|
},
|
||||||
|
gallery: {
|
||||||
|
mobileColumn: 5,
|
||||||
|
column: 3,
|
||||||
|
columnSpacing: 50,
|
||||||
|
rowSpacing: 10,
|
||||||
|
showInfo: true,
|
||||||
|
showInfoMode: "hover",
|
||||||
|
roundedBorder: true,
|
||||||
|
showBackTop: true,
|
||||||
|
},
|
||||||
|
imagePreview: {
|
||||||
|
mode: "full",
|
||||||
|
gallery: true
|
||||||
|
},
|
||||||
|
officePreview: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
updateZfileConfig(val: any) {
|
||||||
|
this.zfileConfig = val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default useGlobalConfigStore;
|
77
web/src/components/file/stores/storage-config.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
import useFileSelect from "@/components/file/file/useFileSelect";
|
||||||
|
|
||||||
|
let { selectStatistics } = useFileSelect();
|
||||||
|
|
||||||
|
|
||||||
|
// 当前存储源的配置信息,数据来源为服务端配置。请求存储源后会获取其配置信息。
|
||||||
|
const useStorageConfigStore = defineStore('storageConfigStore', {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
globalConfig: {
|
||||||
|
siteName: '',
|
||||||
|
directLinkPrefix: '',
|
||||||
|
infoEnable: false,
|
||||||
|
showLinkBtn: false,
|
||||||
|
recordDownloadLog: false,
|
||||||
|
showShortLink: false,
|
||||||
|
showPathLink: false,
|
||||||
|
tableSize: 'small',
|
||||||
|
rootShowStorage: true,
|
||||||
|
fileClickMode: 'dbclick',
|
||||||
|
showDocument: false,
|
||||||
|
debugMode: false,
|
||||||
|
domain: '',
|
||||||
|
icp: '',
|
||||||
|
avatar: '',
|
||||||
|
announcement: '',
|
||||||
|
layout: 'full',
|
||||||
|
showAnnouncement: false,
|
||||||
|
searchEnable: false,
|
||||||
|
showLogin: false,
|
||||||
|
siteHomeName: '首页',
|
||||||
|
siteHomeLogo: '',
|
||||||
|
siteHomeLogoLink: '',
|
||||||
|
siteHomeLogoTargetMode: '',
|
||||||
|
maxShowSize: 1000,
|
||||||
|
loadMoreSize: 50,
|
||||||
|
defaultSortField: 'name',
|
||||||
|
defaultSortOrder: 'asc',
|
||||||
|
linkExpireTimes: ''
|
||||||
|
},
|
||||||
|
folderConfig: {
|
||||||
|
readmeText: null,
|
||||||
|
readmeDisplayMode: null,
|
||||||
|
defaultSwitchToImgMode: false,
|
||||||
|
enableFileOperator: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
permission: (state) => {
|
||||||
|
return {
|
||||||
|
open: selectStatistics.value.isSingleSelect && selectStatistics.value.isAllFolder,
|
||||||
|
preview: selectStatistics.value.isAllFile && selectStatistics.value.isSingleSelect,
|
||||||
|
download: selectStatistics.value.isAllFile,
|
||||||
|
link: selectStatistics.value.isAllFile && state.globalConfig.showLinkBtn && (state.globalConfig.showShortLink || state.globalConfig.showPathLink),
|
||||||
|
rename: state.folderConfig.enableFileOperator && selectStatistics.value.isSingleSelect,
|
||||||
|
delete: state.folderConfig.enableFileOperator,
|
||||||
|
newFolder: state.folderConfig.enableFileOperator,
|
||||||
|
upload: state.folderConfig.enableFileOperator,
|
||||||
|
pathLink: state.globalConfig.showPathLink,
|
||||||
|
shortLink: state.globalConfig.showShortLink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
updateGlobalConfig(val: any) {
|
||||||
|
this.globalConfig = val;
|
||||||
|
},
|
||||||
|
updateFolderConfig(val: any) {
|
||||||
|
this.folderConfig = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default useStorageConfigStore;
|
80
web/src/components/file/useCommon.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// 获取窗口宽高
|
||||||
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
import { ref,computed} from "vue";
|
||||||
|
const { width, height } = useWindowSize()
|
||||||
|
|
||||||
|
export default function useCommon() {
|
||||||
|
|
||||||
|
const isMobile = computed(() => {
|
||||||
|
return width.value < 768;
|
||||||
|
})
|
||||||
|
|
||||||
|
const isNotMobile = computed(() => {
|
||||||
|
return width.value >= 768;
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* encodeURIComponent 编码所有 url, 但忽略 / 字符
|
||||||
|
* @param str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const encodeAllIgnoreSlashes = (str) => {
|
||||||
|
if (strIsEmpty(str)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
let prevIndex = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const c = str.charAt(i);
|
||||||
|
if (c === '/') {
|
||||||
|
if (prevIndex < i) {
|
||||||
|
let subStr = str.substring(prevIndex + 1, i);
|
||||||
|
result += encodeURIComponent(subStr);
|
||||||
|
prevIndex = i;
|
||||||
|
}
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === str.length - 1 && prevIndex < i) {
|
||||||
|
let subStr = str.substring(prevIndex + 1, i + 1);
|
||||||
|
result += encodeURIComponent(subStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strIsEmpty = (str) => {
|
||||||
|
return str === null || str === undefined || str === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const strIsNotEmpty = (str) => {
|
||||||
|
return !strIsEmpty(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeDuplicateSeparator = (path) => {
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
if (path.indexOf("http://") === 0) {
|
||||||
|
result = "http://";
|
||||||
|
} else if (path.indexOf("https://") === 0) {
|
||||||
|
result = "https://";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = result.length; i < path.length - 1; i++) {
|
||||||
|
let current = path.charAt(i);
|
||||||
|
let next = path.charAt(i + 1);
|
||||||
|
if (!(current === '/' && next === '/')) {
|
||||||
|
result += current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += path.charAt(path.length - 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isMobile, isNotMobile, height, encodeAllIgnoreSlashes, strIsEmpty, strIsNotEmpty, removeDuplicateSeparator
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
39
web/src/components/file/useRouterData.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const routerRef = ref(null);
|
||||||
|
const routeRef = ref(null);
|
||||||
|
import { ref,computed} from "vue";
|
||||||
|
export default function useRouterData(initRouter, initRoute) {
|
||||||
|
|
||||||
|
if (initRouter && !routerRef.value) {
|
||||||
|
routerRef.value = initRouter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initRoute && !routeRef.value) {
|
||||||
|
routeRef.value = initRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前所在驱动器 key
|
||||||
|
const storageKey = computed(() => {
|
||||||
|
return routeRef.value?.params.storageKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前所在目录
|
||||||
|
const currentPath = computed(() => {
|
||||||
|
if (routeRef.value?.params.fullpath) {
|
||||||
|
return '/' + routeRef.value.params.fullpath.join('/');
|
||||||
|
} else {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fullpath = computed(() => {
|
||||||
|
return routeRef.value?.params.fullpath;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
routerRef,
|
||||||
|
storageKey,
|
||||||
|
currentPath,
|
||||||
|
fullpath
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
84
web/src/components/header/useHeaderBreadcrumb.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { removeDuplicateSlashes } from "fast-glob/out/managers/patterns";
|
||||||
|
import useHeaderStorageList from "./useHeaderStorageList";
|
||||||
|
|
||||||
|
import useStorageConfigStore from "@/components/file/stores/storage-config";
|
||||||
|
let storageConfigStore = useStorageConfigStore();
|
||||||
|
|
||||||
|
import useRouterData from "@/components/file/useRouterData";
|
||||||
|
let { fullpath, storageKey } = useRouterData();
|
||||||
|
|
||||||
|
import useCommon from "@/components/file/useCommon";
|
||||||
|
const { encodeAllIgnoreSlashes } = useCommon();
|
||||||
|
|
||||||
|
// 面包屑数据
|
||||||
|
let breadcrumbData = ref([]);
|
||||||
|
let initialized = false;
|
||||||
|
export default function useBreadcrumb() {
|
||||||
|
|
||||||
|
let rootShowStorage = storageConfigStore.globalConfig.rootShowStorage;
|
||||||
|
|
||||||
|
// 构建面包屑
|
||||||
|
let buildBreadcrumbData = () => {
|
||||||
|
if (!rootShowStorage && !storageKey.value) {
|
||||||
|
breadcrumbData.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
breadcrumbData.value = [
|
||||||
|
{
|
||||||
|
name: storageConfigStore.globalConfig.siteHomeName || '首页',
|
||||||
|
href: rootPath.value,
|
||||||
|
disable: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 如果为包含根目录模式,则面包屑显示驱动器
|
||||||
|
if (rootShowStorage) {
|
||||||
|
let { findStorageByKey } = useHeaderStorageList();
|
||||||
|
let storageByKey = findStorageByKey(storageKey.value);
|
||||||
|
if (storageByKey) {
|
||||||
|
breadcrumbData.value.push({
|
||||||
|
name: storageByKey.name,
|
||||||
|
href: encodeAllIgnoreSlashes('/' + storageByKey.key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullpath.value) {
|
||||||
|
fullpath.value.forEach((item, index, arr) => {
|
||||||
|
if (item) {
|
||||||
|
let breadcrumbItem = {
|
||||||
|
name: item,
|
||||||
|
href: encodeAllIgnoreSlashes(removeDuplicateSlashes('/' + storageKey.value + '/' + arr.slice(0, index + 1).join('/'))),
|
||||||
|
disable: index === arr.length - 1
|
||||||
|
}
|
||||||
|
breadcrumbData.value.push(breadcrumbItem);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根目录路径
|
||||||
|
*/
|
||||||
|
let rootPath = computed(() => '/' + (rootShowStorage ? '' : storageKey.value));
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
// 当 URL 变化, 则自动重新 build 面包屑
|
||||||
|
watch(() => fullpath.value, () => {
|
||||||
|
buildBreadcrumbData();
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当 URL 变化, 则自动重新 build 面包屑
|
||||||
|
watch(() => storageKey.value, () => {
|
||||||
|
buildBreadcrumbData();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rootPath,
|
||||||
|
breadcrumbData,
|
||||||
|
buildBreadcrumbData
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
26
web/src/components/header/useHeaderDebugMode.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {resetPasswordReq} from "@/api/header";
|
||||||
|
|
||||||
|
export default function useHeaderDebugMode() {
|
||||||
|
// debug 模式重置管理员密码
|
||||||
|
const resetAdminPwd = () => {
|
||||||
|
ElMessageBox.confirm('是否确认重置后台管理员密码?重置后用户名/密码将强制修改为 admin 123456', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
callback: action => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
resetPasswordReq().then((response) => {
|
||||||
|
if (response.code === 0) {
|
||||||
|
ElMessage.success("重置成功,请及时关闭 debug 功能,防止出现安全问题!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
resetAdminPwd
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
130
web/src/components/header/useHeaderStorageList.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { getSourceListReq } from "@/api/header";
|
||||||
|
import { ref,computed,watch} from "vue";
|
||||||
|
import useFileDataStore from "@/components/file/stores/file-data";
|
||||||
|
let fileDataStore = useFileDataStore();
|
||||||
|
|
||||||
|
import useStorageConfigStore from "@/components/file/stores/storage-config";
|
||||||
|
let storageConfigStore = useStorageConfigStore();
|
||||||
|
|
||||||
|
import useRouterData from "@/components/file/useRouterData";
|
||||||
|
let { routerRef, fullpath, storageKey } = useRouterData();
|
||||||
|
|
||||||
|
let storageList = ref([]);
|
||||||
|
let currentStorageKey = ref();
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
export default function useHeaderStorageList() {
|
||||||
|
|
||||||
|
let rootShowStorage = storageConfigStore.globalConfig.rootShowStorage;
|
||||||
|
|
||||||
|
|
||||||
|
// 加载存储源列表
|
||||||
|
let loadStorageSourceList = () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 请求获取所有存储器列表
|
||||||
|
getSourceListReq().then((response) => {
|
||||||
|
storageList.value = response.data;
|
||||||
|
// 如果没有存储源, 则直接提示是否添加
|
||||||
|
if (storageList.value.length === 0) {
|
||||||
|
ElMessageBox.confirm('当前无可用存储源,是否跳转至管理员页面添加存储源?', '提示', {
|
||||||
|
confirmButtonText: '确定', cancelButtonText: '取消', type: 'info', callback: action => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
routerRef.value.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rootPathAction(rootShowStorage);
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果路由处理根目录的操作.
|
||||||
|
const rootPathAction = (rootShowStorage) => {
|
||||||
|
// 如果当前 URL 参数中有存储源 ID, 则直接用当前的.
|
||||||
|
if (storageKey.value) {
|
||||||
|
// 判断 url 中的 storageKey 是否存在于 storageList 中. 如果不存在, 则跳转到首页
|
||||||
|
let storageByKey = findStorageByKey(storageKey.value);
|
||||||
|
|
||||||
|
if (storageByKey) {
|
||||||
|
currentStorageKey.value = storageKey.value;
|
||||||
|
} else {
|
||||||
|
// 否则读取存储源列表中的第一个, 并跳转到响应的 URL 中.
|
||||||
|
routerToFirstStorage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rootShowStorage) {
|
||||||
|
fileDataStore.updateFileList(storageListAsFileList.value);
|
||||||
|
document.title = storageConfigStore.globalConfig.siteName + ' | 首页';
|
||||||
|
} else {
|
||||||
|
routerToFirstStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshCurrentStorageSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将存储源类别转换到文件列表
|
||||||
|
const storageListAsFileList = computed(() => {
|
||||||
|
let fileList = [];
|
||||||
|
storageList.value.forEach((item) => {
|
||||||
|
fileList.push({
|
||||||
|
name: item.name, path: item.key, size: 0, time: '-', type: 'ROOT'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return fileList;
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新当前存储策略到 pinia 中
|
||||||
|
const refreshCurrentStorageSource = () => {
|
||||||
|
storageList.value.some((item) => {
|
||||||
|
if (item.key === currentStorageKey.value) {
|
||||||
|
fileDataStore.updateCurrentStorageSource(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
// 当存储策略变化时, 切换路由中的值
|
||||||
|
watch(() => currentStorageKey.value, (newVal, oldVal) => {
|
||||||
|
// 如果切换到了新存储源,且没有指定路径,则进行切换
|
||||||
|
if ((newVal && !fullpath.value) || oldVal !== undefined) {
|
||||||
|
routerRef.value.push('/' + newVal);
|
||||||
|
refreshCurrentStorageSource();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => storageKey.value, (val) => {
|
||||||
|
rootPathAction(true);
|
||||||
|
})
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到第一个存储器
|
||||||
|
const routerToFirstStorage = () => {
|
||||||
|
// 否则读取存储源列表中的第一个, 并跳转到响应的 URL 中.
|
||||||
|
if (storageList.value.length > 0) {
|
||||||
|
let firstStorageKey = storageList.value[0].key;
|
||||||
|
currentStorageKey.value = firstStorageKey;
|
||||||
|
routerRef.value.push('/' + firstStorageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据存储源 key 查找对象
|
||||||
|
*/
|
||||||
|
const findStorageByKey = (key) => {
|
||||||
|
return storageList.value.find(item => {
|
||||||
|
if (item.key == key) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadStorageSourceList, storageList, findStorageByKey, currentStorageKey, storageListAsFileList
|
||||||
|
}
|
||||||
|
}
|
55
web/src/components/header/useSetting.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const visible = ref(false);
|
||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import useCommon from "@/components/file/useCommon";
|
||||||
|
const { isMobile } = useCommon();
|
||||||
|
|
||||||
|
// 获取默认值
|
||||||
|
import useGlobalConfigStore from "@/components/file/stores/global-config";
|
||||||
|
let globalConfigStore = useGlobalConfigStore();
|
||||||
|
let zfileConfig = globalConfigStore.zfileConfig;
|
||||||
|
|
||||||
|
let baseData = {
|
||||||
|
view: {
|
||||||
|
size: 2
|
||||||
|
},
|
||||||
|
gallery: {
|
||||||
|
column: isMobile.value ? zfileConfig.gallery.mobileColumn : zfileConfig.gallery.column,
|
||||||
|
columnSpacing: zfileConfig.gallery.columnSpacing,
|
||||||
|
rowSpacing: zfileConfig.gallery.rowSpacing,
|
||||||
|
showInfo: zfileConfig.gallery.showInfo,
|
||||||
|
showInfoMode: zfileConfig.gallery.showInfoMode,
|
||||||
|
roundedBorder: zfileConfig.gallery.roundedBorder,
|
||||||
|
showBackTop: zfileConfig.gallery.showBackTop
|
||||||
|
},
|
||||||
|
imagePreview: {
|
||||||
|
mode: zfileConfig.imagePreview.mode,
|
||||||
|
gallery: zfileConfig.imagePreview.gallery
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extend = (a, b) => {
|
||||||
|
b = b || this;
|
||||||
|
|
||||||
|
for(let key in a) {
|
||||||
|
if (b[key] === undefined) {
|
||||||
|
b[key] = a[key];
|
||||||
|
} else if (a[key] instanceof Object && b[key] instanceof Object) {
|
||||||
|
extend(a[key], b[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const zfileSettingCache = useStorage('zfile-setting-cache', baseData);
|
||||||
|
|
||||||
|
export default function useSetting() {
|
||||||
|
|
||||||
|
const openSettingVisible = () => {
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(baseData, zfileSettingCache.value)
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible, zfileSettingCache, openSettingVisible
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
html .el-dialog__header{
|
html .el-dialog__header{
|
||||||
background-color: rgba(248, 248, 248, 1);
|
// background-color: rgb(250, 250, 250);
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
html.dark .fixed-header{
|
html.dark .fixed-header{
|
||||||
|
@ -587,183 +587,9 @@ function tableMouseLeave(){
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-container"
|
<div class="dashboard-container"
|
||||||
style="display: flex;align-items: center;justify-content: center;flex-wrap: wrap;align-content:center;">
|
style="display: flex;align-items: center;justify-content: center;flex-wrap: wrap;align-content:center;">
|
||||||
<!-- <div class="dashboard-container-title">
|
|
||||||
<div>北京市最新筛查数据统计</div>
|
|
||||||
</div>
|
|
||||||
<div class="dashboard-container-top">
|
|
||||||
<div class="top-box">
|
|
||||||
<div>
|
<div>
|
||||||
<div class="top-box-title">参检学校数量</div>
|
<img src="@/assets/login/90.jfif" alt="">
|
||||||
<div class="top-box-num">34</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box-right">
|
|
||||||
<img src="@/assets/dashboard/top1.png" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box">
|
|
||||||
<div>
|
|
||||||
<div class="top-box-title">参检学生数量</div>
|
|
||||||
<div class="top-box-num">2354</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box-right" style="background-color: #A99EF3;">
|
|
||||||
<img src="@/assets/dashboard/top2.png" style="top:14px;" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box">
|
|
||||||
<div>
|
|
||||||
<div class="top-box-title">检查人数</div>
|
|
||||||
<div class="top-box-num">20000</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box-right" style="background-color: #409EFF;">
|
|
||||||
<img src="@/assets/dashboard/top3.png" style="top:14px;left: 14px;" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box">
|
|
||||||
<div>
|
|
||||||
<div class="top-box-title">近视人数</div>
|
|
||||||
<div class="top-box-num">12586</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box-right" style="background-color: #CC99FF;">
|
|
||||||
<img src="@/assets/dashboard/top4.png" style="top:14px;left: 10px;" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box">
|
|
||||||
<div>
|
|
||||||
<div class="top-box-title">视力异常人数</div>
|
|
||||||
<div class="top-box-num">20000</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box-right" style="background-color: #F46868;">
|
|
||||||
<img src="@/assets/dashboard/top5.png" style="top:15px;left: 11px;" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box">
|
|
||||||
<div>
|
|
||||||
<div class="top-box-title">远视储备不足人数</div>
|
|
||||||
<div class="top-box-num">12586</div>
|
|
||||||
</div>
|
|
||||||
<div class="top-box-right" style="background-color: #51CE8C;">
|
|
||||||
<img src="@/assets/dashboard/top6.png" style="top:16px;left: 11px;" alt="">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dashboard-container-bottom">
|
|
||||||
<div class="container-bottom-top">
|
|
||||||
<div class="bottom-top-box">
|
|
||||||
<div class="box-content-title">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="titleicon"></div>
|
|
||||||
<div class="titletext">近三年近视变化请何况</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button :icon="Refresh" style="height: 25px;width:60px;border-radius:2px;" @click="refreshdata('1')">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box-content-charts">
|
|
||||||
<div id="echarts1" style="width: 100%; height:100%;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottom-top-box">
|
|
||||||
<div class="box-content-title">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="titleicon"></div>
|
|
||||||
<div class="titletext">近三年近视发展趋势</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button :icon="Refresh" style="height: 25px;width:60px;border-radius:2px;" @click="refreshdata('2')">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box-content-charts">
|
|
||||||
<div id="echarts2" style="width: 100%; height:100%;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottom-top-box">
|
|
||||||
<div class="box-content-title">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="titleicon"></div>
|
|
||||||
<div class="titletext">各地区近视人数统计</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button :icon="Refresh" style="height: 25px;width:60px;border-radius:2px;" @click="refreshdata('3')">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box-content-charts">
|
|
||||||
<div id="echarts3" style="width: 100%; height:100%;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container-bottom-top">
|
|
||||||
<div class="bottom-top-box">
|
|
||||||
<div class="box-content-title">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="titleicon"></div>
|
|
||||||
<div class="titletext">各学校近视人数统计</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button :icon="Refresh" style="height: 25px;width:60px;border-radius:2px;" @click="refreshdata('4')">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box-content-charts">
|
|
||||||
<el-table ref="myTable" :data="tableData" stripe style="height: calc(100% - 10px);" @mouseenter="tablemouseenter" @mouseleave="tableMouseLeave">
|
|
||||||
<el-table-column prop="name" label="学校名称" align="center" min-width="100" />
|
|
||||||
<el-table-column prop="checknum" label="检查人数" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<div v-if="scope.row.checknum !== '' &&scope.row.checknum !== undefined">
|
|
||||||
{{scope.row.checknum}}人
|
|
||||||
</div>
|
|
||||||
<div v-else></div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="num" label="近视人数" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<div v-if="scope.row.num !== '' &&scope.row.num !== undefined">
|
|
||||||
{{scope.row.num}}人
|
|
||||||
</div>
|
|
||||||
<div v-else></div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottom-top-box">
|
|
||||||
<div class="box-content-title">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="titleicon"></div>
|
|
||||||
<div class="titletext">男女近视对比</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button :icon="Refresh" style="height: 25px;width:60px;border-radius:2px;" @click="refreshdata('5')">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box-content-charts flex justify-between items-center" style="padding:10px 40px">
|
|
||||||
<div>
|
|
||||||
<div class="tschartstext">男生</div>
|
|
||||||
<div class="boytext">42%</div>
|
|
||||||
</div>
|
|
||||||
<div id="echarts4" style="width: 60%; height:100%;"></div>
|
|
||||||
<div style="font-weight: 400;color: #787878;font-size: 16px;">
|
|
||||||
<div class="tschartstext">女生</div>
|
|
||||||
<div class="girltext">58%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bottom-top-box">
|
|
||||||
<div class="box-content-title">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="titleicon"></div>
|
|
||||||
<div class="titletext">各年级近视人数分布</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button :icon="Refresh" style="height: 25px;width:60px;border-radius:2px;" @click="refreshdata('6')">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box-content-charts">
|
|
||||||
<div id="echarts5" style="width: 100%; height:100%;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
<div>
|
|
||||||
<img src="@/assets/images/home.png" alt="">
|
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 100%; text-align: center;
|
<div style="width: 100%; text-align: center;
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container" :style="{backgroundImage:'url('+bgImg+')'}">
|
<div class="login-container" >
|
||||||
<div class="login-container-title">{{title}}</div>
|
<div class="login-container-title">{{title}}</div>
|
||||||
<img style=" position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 40px;
|
|
||||||
max-width: 200px;max-height: 55px;" :src="logo" alt="">
|
|
||||||
<!-- <div class="login-container-left">
|
<!-- <div class="login-container-left">
|
||||||
<el-image
|
<el-image
|
||||||
:src="loginImg"
|
:src="loginImg"
|
||||||
@ -98,14 +95,7 @@
|
|||||||
<p>{{ $t('login.icp') }}</p>
|
<p>{{ $t('login.icp') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-containe-footer">
|
|
||||||
<div style="font-size: 16px;">{{ content1 }}</div>
|
|
||||||
<div style="margin-top: 5px;">{{ content2 }}</div>
|
|
||||||
<div style="margin-top: 5px;">{{ content3 }}</div>
|
|
||||||
<!-- <div style="font-size: 16px;">杭州明眸慧眼科技有限公司</div>
|
|
||||||
<div style="margin-top: 5px;">浙江省杭州市余杭区祥茂路166号华滋科欣设计创意园4号楼</div>
|
|
||||||
<div style="margin-top: 5px;">400-6588695</div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -295,7 +285,7 @@ $light_gray: #eee;
|
|||||||
|
|
||||||
}
|
}
|
||||||
.login-container {
|
.login-container {
|
||||||
background: url('../../assets/login/loginbg.jpg') no-repeat;
|
background: url('../../assets/login/beijing.jpg') no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
@ -287,7 +287,7 @@ $light_gray: #eee;
|
|||||||
|
|
||||||
}
|
}
|
||||||
.login-container {
|
.login-container {
|
||||||
background: url('../../assets/login/loginbg.jpg') no-repeat;
|
background: url('../../assets/login/beijing.jpg') no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
@ -9,16 +9,12 @@ import { ref, onMounted, nextTick } from "vue";
|
|||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { ElMessageBox, ElMessage } from "element-plus";
|
import { ElMessageBox, ElMessage } from "element-plus";
|
||||||
import Page from '@/components/Pagination/page.vue';
|
import Page from '@/components/Pagination/page.vue';
|
||||||
import { projectList, getNodesTree, addNodes, updateNodes, deleteNodesById } from "@/api/document";
|
import { projectList, getNodesTree, addNodes, updateNodes, deleteNodesById,getFilesPage } from "@/api/document";
|
||||||
import uploadFiles from '@/components/uploadFiles/index.vue'
|
import ZUpload from '@/components/file/ZUpload.vue'
|
||||||
|
import useFileUpload from "@/components/file/file/useFileUpload";
|
||||||
const handleSuccess = (file: any) => {
|
import useHeaderStorageList from "@/components/header/useHeaderStorageList";
|
||||||
console.log('上传成功:', file.name)
|
const { openUploadDialog, openUploadFolderDialog, uploadProgressInfoStatistics } = useFileUpload();
|
||||||
}
|
const { currentStorageKey } = useHeaderStorageList();
|
||||||
|
|
||||||
const handleError = ({ file, error }) => {
|
|
||||||
console.error('上传失败:', file.name, error)
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getProject()
|
getProject()
|
||||||
});
|
});
|
||||||
@ -71,8 +67,9 @@ function gettreedata() {
|
|||||||
treeloading.value = false
|
treeloading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const pathid = ref()
|
||||||
function handleNodeClick(data: any, node: any) {
|
function handleNodeClick(data: any, node: any) {
|
||||||
|
pathid.value = data.id
|
||||||
}
|
}
|
||||||
//子项目配置
|
//子项目配置
|
||||||
const frame = ref(false)
|
const frame = ref(false)
|
||||||
@ -210,7 +207,7 @@ const queryParams: any = ref({
|
|||||||
name: ''
|
name: ''
|
||||||
});
|
});
|
||||||
//定义表格数据
|
//定义表格数据
|
||||||
const tableData: any = ref([{}, {}]);
|
const tableData: any = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
// 表格加载
|
// 表格加载
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -221,12 +218,38 @@ function getdata() {
|
|||||||
//上传组件
|
//上传组件
|
||||||
const upfile = ref(false)
|
const upfile = ref(false)
|
||||||
function openFile() {
|
function openFile() {
|
||||||
|
localStorage.setItem('filepath', findPathById(treedata.value,pathid.value));
|
||||||
upfile.value = true
|
upfile.value = true
|
||||||
}
|
}
|
||||||
function fileClose() {
|
function fileClose() {
|
||||||
upfile.value = false
|
upfile.value = false
|
||||||
}
|
}
|
||||||
|
function findPathById(array:any, targetId:any) {
|
||||||
|
// 辅助函数,用于递归查找路径
|
||||||
|
function recursiveSearch(items:any, target:any, path:any) {
|
||||||
|
for (let item of items) {
|
||||||
|
// 将当前对象的name添加到路径中
|
||||||
|
let newPath = [...path, item.nodeName];
|
||||||
|
|
||||||
|
// 检查当前对象的id是否匹配目标id
|
||||||
|
if (item.id === target) {
|
||||||
|
return newPath.join('/'); // 找到匹配项,返回路径字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到匹配项,继续检查children数组
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
let result = recursiveSearch(item.children, target, newPath);
|
||||||
|
if (result) { // 如果在children中找到了匹配项,返回结果
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null; // 如果没有找到匹配项,返回null
|
||||||
|
}
|
||||||
|
// 从顶层数组开始搜索
|
||||||
|
let patharr = recursiveSearch(array, targetId, [])
|
||||||
|
return '/'+patharr; // 注意这里我们传入了一个空数组作为初始路径
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -280,9 +303,10 @@ function fileClose() {
|
|||||||
<el-button type="primary" @click="getdata()">搜索</el-button>
|
<el-button type="primary" @click="getdata()">搜索</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<el-button type="primary" @click="openFile">上传</el-button>
|
<!-- :disabled="pathid.value" -->
|
||||||
<el-button type="primary">新增</el-button>
|
<el-button type="primary" :disabled="!pathid" @click="openFile" >上传</el-button>
|
||||||
<el-button type="primary">删除</el-button>
|
<el-button type="primary">删除</el-button>
|
||||||
|
<el-button type="primary">下载</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -298,7 +322,6 @@ function fileClose() {
|
|||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span
|
<span
|
||||||
style="display: flex;display: -webkit-flex;justify-content: space-around;-webkit-justify-content: space-around; ">
|
style="display: flex;display: -webkit-flex;justify-content: space-around;-webkit-justify-content: space-around; ">
|
||||||
{{ scope }}
|
|
||||||
<img src="@/assets/MenuIcon/lbcz_xg.png" alt="" title="修改" style="cursor: pointer;">
|
<img src="@/assets/MenuIcon/lbcz_xg.png" alt="" title="修改" style="cursor: pointer;">
|
||||||
<img src="@/assets/MenuIcon/lbcz_sc.png" alt="" title="删除" style="cursor: pointer;">
|
<img src="@/assets/MenuIcon/lbcz_sc.png" alt="" title="删除" style="cursor: pointer;">
|
||||||
</span>
|
</span>
|
||||||
@ -325,30 +348,18 @@ function fileClose() {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<el-dialog title="上传文件" v-model="upfile" width="35%" :before-close="fileClose" top="30px" draggable
|
<div class="upload">
|
||||||
|
<el-dialog title="上传文件" v-model="upfile" width="15%" :before-close="fileClose" top="30px" draggable
|
||||||
destroy-on-close>
|
destroy-on-close>
|
||||||
<el-form ref="ruleFormRef" style="max-width: 600px" :model="projectForme" :rules="moderules"
|
<el-button @click="openUploadDialog" type="primary">上传文件</el-button>
|
||||||
label-width="auto" class="demo-ruleForm" status-icon>
|
<el-button @click="openUploadFolderDialog" type="primary">上传文件夹</el-button>
|
||||||
<el-form-item label="文件上传" prop="nodeName">
|
<!-- <div style="cursor: pointer;">上传文件</div>
|
||||||
<uploadFiles upload-url="/api/upload" :max-size="20"
|
<div style="cursor: pointer;">上传文件夹</div> -->
|
||||||
@upload-success="handleSuccess" @upload-error="handleError" />
|
<ZUpload />
|
||||||
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="关键字" prop="nodeName">
|
|
||||||
<el-input v-model="projectForme.nodeName" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="描述" prop="nodeName">
|
|
||||||
<el-input v-model="projectForme.nodeName" :rows="2" type="textarea" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<div style="width: 100%;display: flex;justify-content: end;">
|
|
||||||
<el-button type="primary" @click="submitForm(ruleFormRef)">确定</el-button>
|
|
||||||
<el-button @click="handleClose(ruleFormRef)">取消</el-button>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -448,4 +459,9 @@ function fileClose() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.upload{
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -40,6 +40,9 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
|||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve('./src')
|
'@': path.resolve('./src')
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
define: {
|
||||||
|
'process.env': {}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|