文件上传组件迁移

This commit is contained in:
wangxk 2025-02-12 09:17:33 +08:00
parent ee174283ae
commit 33c13838a1
49 changed files with 3541 additions and 232 deletions

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 915 B

View File

@ -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",

View File

@ -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
});
}

View 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
View 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
View 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
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

View 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>

View 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>

View 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;

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
};
}

View 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
}
}

View 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
}
}

View 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>

View 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;

View 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;
}

View File

@ -0,0 +1,9 @@
import openPrompt from "./prompt/index";
import openConfirm from "./confirm/index";
const MessageBox = {
prompt: openPrompt,
confirm: openConfirm
}
export default MessageBox;

View 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;

View 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>

View 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;
}

View 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;

View 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;

View 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;

View 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
}
}

View 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
};
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View File

@ -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{

View File

@ -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;

View File

@ -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%;

View File

@ -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%;

View File

@ -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];
// idid
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>

View File

@ -40,6 +40,9 @@ export default ({ mode }: ConfigEnv): UserConfig => {
alias: {
'@': path.resolve('./src')
}
}
},
define: {
'process.env': {}
},
};
};