文件上传组件迁移
@ -10,13 +10,18 @@
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
"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",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@vueuse/core": "^9.1.1",
|
||||
"@vueuse/integrations": "^9.13.0",
|
||||
"@wangeditor/editor": "^5.0.0",
|
||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||
"axios": "^1.2.0",
|
||||
"beautify-qrcode": "^1.0.3",
|
||||
"better-scroll": "^2.4.2",
|
||||
"default-passive-events": "^2.0.0",
|
||||
"docx-preview": "^0.3.0",
|
||||
@ -28,6 +33,7 @@
|
||||
"js-cookie": "^3.0.1",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jspdf": "^2.5.1",
|
||||
"minimatch": "^5.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
@ -35,6 +41,9 @@
|
||||
"screenfull": "^6.0.0",
|
||||
"sortablejs": "^1.14.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-clipboard3": "^2.0.0",
|
||||
"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": "杭州明眸慧眼科技有限公司",
|
||||
"content2": "浙江省杭州市余杭区祥茂路166号华滋科欣设计创意园4号楼",
|
||||
"content3": "400-6588695",
|
||||
"bgImg": "loginbg.jpg",
|
||||
"bgImg": "beijing.jpg",
|
||||
"loginLogo": "uplogo.png",
|
||||
"headerLogo": "logo.png",
|
||||
"ItutionIds": "a35ce60b10e425df77f401ebf1af80ac",
|
||||
|
@ -37,4 +37,12 @@ export function deleteNodesById(queryParams:any) {
|
||||
method: 'post',
|
||||
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{
|
||||
background-color: rgba(248, 248, 248, 1);
|
||||
// background-color: rgb(250, 250, 250);
|
||||
margin-right: 0px;
|
||||
}
|
||||
html.dark .fixed-header{
|
||||
|
@ -587,183 +587,9 @@ function tableMouseLeave(){
|
||||
<template>
|
||||
<div class="dashboard-container"
|
||||
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 class="top-box-title">参检学校数量</div>
|
||||
<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="">
|
||||
<img src="@/assets/login/90.jfif" alt="">
|
||||
</div>
|
||||
<div style="width: 100%; text-align: center;
|
||||
font-size: 50px;
|
||||
|
@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div class="login-container" :style="{backgroundImage:'url('+bgImg+')'}">
|
||||
<div class="login-container" >
|
||||
<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">
|
||||
<el-image
|
||||
:src="loginImg"
|
||||
@ -98,14 +95,7 @@
|
||||
<p>{{ $t('login.icp') }}</p>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -295,7 +285,7 @@ $light_gray: #eee;
|
||||
|
||||
}
|
||||
.login-container {
|
||||
background: url('../../assets/login/loginbg.jpg') no-repeat;
|
||||
background: url('../../assets/login/beijing.jpg') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
min-height: 100%;
|
||||
|
@ -287,7 +287,7 @@ $light_gray: #eee;
|
||||
|
||||
}
|
||||
.login-container {
|
||||
background: url('../../assets/login/loginbg.jpg') no-repeat;
|
||||
background: url('../../assets/login/beijing.jpg') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
min-height: 100%;
|
||||
|
@ -9,16 +9,12 @@ import { ref, onMounted, nextTick } from "vue";
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { ElMessageBox, ElMessage } from "element-plus";
|
||||
import Page from '@/components/Pagination/page.vue';
|
||||
import { projectList, getNodesTree, addNodes, updateNodes, deleteNodesById } from "@/api/document";
|
||||
import uploadFiles from '@/components/uploadFiles/index.vue'
|
||||
|
||||
const handleSuccess = (file: any) => {
|
||||
console.log('上传成功:', file.name)
|
||||
}
|
||||
|
||||
const handleError = ({ file, error }) => {
|
||||
console.error('上传失败:', file.name, error)
|
||||
}
|
||||
import { projectList, getNodesTree, addNodes, updateNodes, deleteNodesById,getFilesPage } from "@/api/document";
|
||||
import ZUpload from '@/components/file/ZUpload.vue'
|
||||
import useFileUpload from "@/components/file/file/useFileUpload";
|
||||
import useHeaderStorageList from "@/components/header/useHeaderStorageList";
|
||||
const { openUploadDialog, openUploadFolderDialog, uploadProgressInfoStatistics } = useFileUpload();
|
||||
const { currentStorageKey } = useHeaderStorageList();
|
||||
onMounted(() => {
|
||||
getProject()
|
||||
});
|
||||
@ -71,8 +67,9 @@ function gettreedata() {
|
||||
treeloading.value = false
|
||||
})
|
||||
}
|
||||
const pathid = ref()
|
||||
function handleNodeClick(data: any, node: any) {
|
||||
|
||||
pathid.value = data.id
|
||||
}
|
||||
//子项目配置
|
||||
const frame = ref(false)
|
||||
@ -210,7 +207,7 @@ const queryParams: any = ref({
|
||||
name: ''
|
||||
});
|
||||
//定义表格数据
|
||||
const tableData: any = ref([{}, {}]);
|
||||
const tableData: any = ref([]);
|
||||
const total = ref(0);
|
||||
// 表格加载
|
||||
const loading = ref(false)
|
||||
@ -221,12 +218,38 @@ function getdata() {
|
||||
//上传组件
|
||||
const upfile = ref(false)
|
||||
function openFile() {
|
||||
localStorage.setItem('filepath', findPathById(treedata.value,pathid.value));
|
||||
upfile.value = true
|
||||
}
|
||||
function fileClose() {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -280,9 +303,10 @@ function fileClose() {
|
||||
<el-button type="primary" @click="getdata()">搜索</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="openFile">上传</el-button>
|
||||
<el-button type="primary">新增</el-button>
|
||||
<!-- :disabled="pathid.value" -->
|
||||
<el-button type="primary" :disabled="!pathid" @click="openFile" >上传</el-button>
|
||||
<el-button type="primary">删除</el-button>
|
||||
<el-button type="primary">下载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -298,7 +322,6 @@ function fileClose() {
|
||||
<template #default="scope">
|
||||
<span
|
||||
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_sc.png" alt="" title="删除" style="cursor: pointer;">
|
||||
</span>
|
||||
@ -325,29 +348,17 @@ function fileClose() {
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</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>
|
||||
<el-form ref="ruleFormRef" style="max-width: 600px" :model="projectForme" :rules="moderules"
|
||||
label-width="auto" class="demo-ruleForm" status-icon>
|
||||
<el-form-item label="文件上传" prop="nodeName">
|
||||
<uploadFiles upload-url="/api/upload" :max-size="20"
|
||||
@upload-success="handleSuccess" @upload-error="handleError" />
|
||||
|
||||
</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-button @click="openUploadDialog" type="primary">上传文件</el-button>
|
||||
<el-button @click="openUploadFolderDialog" type="primary">上传文件夹</el-button>
|
||||
<!-- <div style="cursor: pointer;">上传文件</div>
|
||||
<div style="cursor: pointer;">上传文件夹</div> -->
|
||||
<ZUpload />
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -448,4 +459,9 @@ function fileClose() {
|
||||
}
|
||||
}
|
||||
}
|
||||
.upload{
|
||||
:deep(.el-dialog__header) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -40,6 +40,9 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||
alias: {
|
||||
'@': path.resolve('./src')
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
'process.env': {}
|
||||
},
|
||||
};
|
||||
};
|
||||
|