|
|
@@ -0,0 +1,471 @@
|
|
|
+<template>
|
|
|
+ <div class="file-picker-main">
|
|
|
+ <div class="file-picker-body">
|
|
|
+ <div class="file-picker-toolbar">
|
|
|
+ <ElUpload
|
|
|
+ action=""
|
|
|
+ :accept="accept"
|
|
|
+ :showFileList="false"
|
|
|
+ :beforeUpload="handleUpload"
|
|
|
+ >
|
|
|
+ <ElButton type="primary" class="ele-btn-icon" :icon="UploadOutlined">
|
|
|
+ 上传
|
|
|
+ </ElButton>
|
|
|
+ </ElUpload>
|
|
|
+ <div class="file-picker-search">
|
|
|
+ <ElInput
|
|
|
+ :clearable="true"
|
|
|
+ v-model="searchKeyword"
|
|
|
+ placeholder="请输入文件名"
|
|
|
+ @clear="handleSearch"
|
|
|
+ @change="handleSearch"
|
|
|
+ />
|
|
|
+ <ElButton type="primary" @click="handleSearch">搜索</ElButton>
|
|
|
+ </div>
|
|
|
+ <EleSegmented v-model="isGridMode" :items="modeSegmentedItems" />
|
|
|
+ </div>
|
|
|
+ <template v-if="fileData.length">
|
|
|
+ <div class="file-picker-file-list" @scroll="handleFileListScroll">
|
|
|
+ <EleFileList
|
|
|
+ :boxChoose="true"
|
|
|
+ :icons="localIcons"
|
|
|
+ :smallIcons="localSmallIcons"
|
|
|
+ :contextMenuProps="contextMenuProps"
|
|
|
+ v-bind="fileListProps || {}"
|
|
|
+ :data="fileData"
|
|
|
+ :grid="isGridMode === 1"
|
|
|
+ :selectionType="limit === 1 ? 'radio' : 'checkbox'"
|
|
|
+ :selections="fileSelections"
|
|
|
+ v-model:current="fileCurrent"
|
|
|
+ :contextMenus="fileContextMenus"
|
|
|
+ :class="[{ 'is-ping-top': isPingTop }]"
|
|
|
+ @itemClick="handleFileItemClick"
|
|
|
+ @itemContextMenu="handleFileCtxMenuClick"
|
|
|
+ @update:selections="updateSelections"
|
|
|
+ @itemContextOpen="handleFileCtxMenuOpen"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <ElePagination
|
|
|
+ size="small"
|
|
|
+ :teleported="false"
|
|
|
+ :pageSizes="[18, 24, 30, 36, 40, 42]"
|
|
|
+ layout="total,prev,pager,next,sizes"
|
|
|
+ v-bind="paginationProps || {}"
|
|
|
+ :currentPage="currentPage"
|
|
|
+ :pageSize="pageSize"
|
|
|
+ :total="total"
|
|
|
+ @update:currentPage="handleCurrentPageChange"
|
|
|
+ @update:pageSize="handlePageSizeChange"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ <ElEmpty
|
|
|
+ v-else
|
|
|
+ :imageSize="80"
|
|
|
+ description="无数据"
|
|
|
+ v-bind="emptyProps || {}"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+ import { ref, reactive, watch, markRaw } from 'vue';
|
|
|
+ import {
|
|
|
+ localIcons,
|
|
|
+ localSmallIcons
|
|
|
+ } from 'ele-admin-plus/es/ele-file-list/icons';
|
|
|
+ import {
|
|
|
+ UploadOutlined,
|
|
|
+ MenuOutlined,
|
|
|
+ AppstoreOutlined,
|
|
|
+ EditOutlined,
|
|
|
+ DeleteOutlined
|
|
|
+ } from '@/components/icons';
|
|
|
+ import { uploadFile, fetchSourcePageList } from '@/api/system/file';
|
|
|
+ import request from '@/utils/request';
|
|
|
+
|
|
|
+ const props = defineProps({
|
|
|
+ /** 最大选择数量 */
|
|
|
+ limit: Number,
|
|
|
+ /** 文件大小限制, 单位MB */
|
|
|
+ fileLimit: Number,
|
|
|
+ /** 接受上传的文件类型 */
|
|
|
+ accept: String,
|
|
|
+ /** 接口查询参数 */
|
|
|
+ params: Object,
|
|
|
+ /** 文件列表自定义属性 */
|
|
|
+ fileListProps: Object,
|
|
|
+ /** 空组件属性 */
|
|
|
+ emptyProps: Object,
|
|
|
+ /** 分页组件属性 */
|
|
|
+ paginationProps: Object,
|
|
|
+ /** 统一设置层级 */
|
|
|
+ baseIndex: Number,
|
|
|
+ /** 消息提示组件 */
|
|
|
+ messageIns: [Object, Function],
|
|
|
+ /** 文件分类 */
|
|
|
+ sourceCate: String,
|
|
|
+ /** 文件类型 */
|
|
|
+ sourceType: String
|
|
|
+ });
|
|
|
+
|
|
|
+ const emit = defineEmits([
|
|
|
+ 'queryStart',
|
|
|
+ 'queryDone',
|
|
|
+ 'renameFile',
|
|
|
+ 'moveFile',
|
|
|
+ 'removeFile',
|
|
|
+ 'fileItemContextOpen'
|
|
|
+ ]);
|
|
|
+
|
|
|
+ /** 当前分组id */
|
|
|
+ const fileParentId = ref();
|
|
|
+
|
|
|
+ /** 文件数据 */
|
|
|
+ const fileData = ref([]);
|
|
|
+
|
|
|
+ /** 选中的文件数据 */
|
|
|
+ const fileSelections = ref([]);
|
|
|
+
|
|
|
+ /** 单选选中的文件数据 */
|
|
|
+ const fileCurrent = ref();
|
|
|
+
|
|
|
+ /** 是否网格模式 */
|
|
|
+ const isGridMode = ref(1);
|
|
|
+
|
|
|
+ /** 搜索关键字 */
|
|
|
+ const searchKeyword = ref('');
|
|
|
+
|
|
|
+ /** 当前页码 */
|
|
|
+ const currentPage = ref(1);
|
|
|
+
|
|
|
+ /** 每页显示数量 */
|
|
|
+ const pageSize = ref(40);
|
|
|
+
|
|
|
+ /** 总数量 */
|
|
|
+ const total = ref(0);
|
|
|
+
|
|
|
+ /** 文件列表是否固定表头 */
|
|
|
+ const isPingTop = ref(false);
|
|
|
+
|
|
|
+ /** 视图模式分段器数据 */
|
|
|
+ const modeSegmentedItems = [
|
|
|
+ { icon: MenuOutlined, value: 0, iconStyle: { transform: 'scale(0.9)' } },
|
|
|
+ { icon: AppstoreOutlined, value: 1 }
|
|
|
+ ];
|
|
|
+
|
|
|
+ /** 文件列表右键菜单属性 */
|
|
|
+ const contextMenuProps = reactive({
|
|
|
+ menuStyle: { minWidth: '120px' },
|
|
|
+ iconProps: { size: 15 },
|
|
|
+ popperOptions: { strategy: 'fixed' },
|
|
|
+ zIndex: props.baseIndex
|
|
|
+ });
|
|
|
+
|
|
|
+ /** 校验最大选择数量 */
|
|
|
+ const checkLimit = (selections, isAdd) => {
|
|
|
+ if (
|
|
|
+ props.limit &&
|
|
|
+ props.limit > 1 &&
|
|
|
+ (isAdd
|
|
|
+ ? selections.length >= props.limit
|
|
|
+ : selections.length > props.limit)
|
|
|
+ ) {
|
|
|
+ props.messageIns?.error?.(`最多只能选择 ${props.limit} 个`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 更新选中数据 */
|
|
|
+ const updateSelections = (selections) => {
|
|
|
+ if (checkLimit(selections) !== false) {
|
|
|
+ fileSelections.value = selections;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (props.limit && props.limit > 1) {
|
|
|
+ fileSelections.value = selections.slice(0, props.limit);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 清空选中 */
|
|
|
+ const clearSelections = () => {
|
|
|
+ fileSelections.value = [];
|
|
|
+ fileCurrent.value = void 0;
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 搜索 */
|
|
|
+ const handleSearch = () => {
|
|
|
+ queryData({ pageNum: 1 });
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 切换当前页码 */
|
|
|
+ const handleCurrentPageChange = (page) => {
|
|
|
+ if (currentPage.value !== page) {
|
|
|
+ currentPage.value = page;
|
|
|
+ queryData();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 切换每页显示数量 */
|
|
|
+ const handlePageSizeChange = (limit) => {
|
|
|
+ if (pageSize.value !== limit) {
|
|
|
+ currentPage.value = 1;
|
|
|
+ pageSize.value = limit;
|
|
|
+ const maxPage = Math.ceil(total.value / limit);
|
|
|
+ if (maxPage && currentPage.value > maxPage) {
|
|
|
+ currentPage.value = maxPage;
|
|
|
+ }
|
|
|
+ queryData();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 文件列表点击事件 */
|
|
|
+ const handleFileItemClick = (item) => {
|
|
|
+ if (props.limit === 1) {
|
|
|
+ fileCurrent.value = item;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const index = fileSelections.value.findIndex((d) => d.key === item.key);
|
|
|
+ if (index !== -1) {
|
|
|
+ fileSelections.value.splice(index, 1);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (checkLimit(fileSelections.value, true) !== false) {
|
|
|
+ fileSelections.value.push(item);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 文件列表右键菜单点击事件 */
|
|
|
+ const handleFileCtxMenuClick = (option) => {
|
|
|
+ const { key, item } = option;
|
|
|
+ const userFileItem = item.userFile;
|
|
|
+ if (key === 'preview') {
|
|
|
+ if (!item.thumbnail) {
|
|
|
+ window.open(item.url);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const data = fileData.value.filter((d) => !!d.thumbnail);
|
|
|
+ const urls = data.map((d) => d.url);
|
|
|
+ const index = data.indexOf(item);
|
|
|
+ if (index !== -1) {
|
|
|
+ window.open(item.url);
|
|
|
+ }
|
|
|
+ } else if (key === 'rename') {
|
|
|
+ emit('renameFile', userFileItem, true);
|
|
|
+ } else if (key === 'move') {
|
|
|
+ emit('moveFile', userFileItem, true);
|
|
|
+ } else if (key === 'remove') {
|
|
|
+ emit('removeFile', userFileItem, true);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 文件列表右键菜单数据 */
|
|
|
+ const fileContextMenus = (item) => {
|
|
|
+ const menus = [
|
|
|
+ {
|
|
|
+ title: '重命名',
|
|
|
+ command: 'rename',
|
|
|
+ icon: markRaw(EditOutlined)
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '删除',
|
|
|
+ command: 'remove',
|
|
|
+ icon: markRaw(DeleteOutlined),
|
|
|
+ divided: true,
|
|
|
+ danger: true
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ if (item.thumbnail) {
|
|
|
+ menus[0].divided = true;
|
|
|
+ menus.unshift({ title: '预览', command: 'preview' });
|
|
|
+ } else {
|
|
|
+ menus[0].divided = true;
|
|
|
+ menus.unshift({ title: '打开', command: 'preview' });
|
|
|
+ }
|
|
|
+ return menus;
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 文件列表右键菜单打开事件 */
|
|
|
+ const handleFileCtxMenuOpen = () => {
|
|
|
+ emit('fileItemContextOpen');
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 校验选择的文件 */
|
|
|
+ const checkFile = (file) => {
|
|
|
+ if (!file) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (props.accept === 'image/*') {
|
|
|
+ if (!file.type.startsWith('image')) {
|
|
|
+ props.messageIns?.error?.('只能选择图片');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } else if (props.accept === '.xls,.xlsx') {
|
|
|
+ if (
|
|
|
+ ![
|
|
|
+ 'application/vnd.ms-excel',
|
|
|
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
|
+ ].includes(file.type)
|
|
|
+ ) {
|
|
|
+ props.messageIns?.error?.('只能选择 excel 文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (props.fileLimit && file.size / 1024 / 1024 > props.fileLimit) {
|
|
|
+ props.messageIns?.error?.(`大小不能超过 ${props.fileLimit}MB`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 文件上传事件 */
|
|
|
+ const handleUpload = (file) => {
|
|
|
+ if (checkFile(file)) {
|
|
|
+ const loading = props.messageIns?.loading?.({
|
|
|
+ message: '上传中..',
|
|
|
+ plain: true,
|
|
|
+ mask: true
|
|
|
+ });
|
|
|
+ uploadFile(file).then((res) => {
|
|
|
+ if (res.code === 200) {
|
|
|
+ file.url = res.url;
|
|
|
+ addFileToDir(file);
|
|
|
+ props.messageIns?.success?.('上传成功');
|
|
|
+ } else {
|
|
|
+ props.messageIns?.error?.(res.msg);
|
|
|
+ }
|
|
|
+ }).finally(() => {
|
|
|
+ loading?.close?.();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ };
|
|
|
+ //新增文件到指定目录
|
|
|
+ const addFileToDir = (file) => {
|
|
|
+ let data = {
|
|
|
+ sourceName: file.name,
|
|
|
+ sourceUrl: file.url,
|
|
|
+ sourceType: props.sourceType,
|
|
|
+ sourceCate: props.sourceCate
|
|
|
+ };
|
|
|
+ request
|
|
|
+ .post('/baseinfo/source/add', data)
|
|
|
+ .then((res) => {
|
|
|
+ if (res.data.code === 200) {
|
|
|
+ props.messageIns?.success?.('上传成功');
|
|
|
+ queryData();
|
|
|
+ } else {
|
|
|
+ props.messageIns?.error?.(res.data.msg);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 文件列表滚动事件 */
|
|
|
+ const handleFileListScroll = (e) => {
|
|
|
+ const wrapEl = e.currentTarget;
|
|
|
+ const scrollTop = wrapEl.scrollTop;
|
|
|
+ isPingTop.value = scrollTop > 1;
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 触发文件数据请求完成 */
|
|
|
+ const handleQueryDone = () => {
|
|
|
+ emit('queryDone');
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 判断是否是图片文件 */
|
|
|
+ const isImageFile = (item) => {
|
|
|
+ // 根据sourceType判断是否为图片,1为图片
|
|
|
+ if (item.sourceType === '1') {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ typeof item.contentType === 'string' &&
|
|
|
+ item.contentType.startsWith('image/') &&
|
|
|
+ item.sourceUrl
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 格式化文件大小 */
|
|
|
+ const formatLength = (length) => {
|
|
|
+ if (length == null) {
|
|
|
+ return '-';
|
|
|
+ }
|
|
|
+ if (length < 1024) {
|
|
|
+ return length + 'B';
|
|
|
+ } else if (length < 1024 * 1024) {
|
|
|
+ return (length / 1024).toFixed(1) + 'KB';
|
|
|
+ } else if (length < 1024 * 1024 * 1024) {
|
|
|
+ return (length / 1024 / 1024).toFixed(1) + 'M';
|
|
|
+ } else {
|
|
|
+ return (length / 1024 / 1024 / 1024).toFixed(1) + 'G';
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 查询文件数据 */
|
|
|
+ const queryData = (params) => {
|
|
|
+ emit('queryStart');
|
|
|
+
|
|
|
+ // 构建请求参数
|
|
|
+ const requestParams = {
|
|
|
+ sourceNameLike: searchKeyword.value,
|
|
|
+ sourceType: props.sourceType,
|
|
|
+ sourceCate: props.sourceCate,
|
|
|
+ pageNum: currentPage.value,
|
|
|
+ pageSize: pageSize.value,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 使用新的API
|
|
|
+ fetchSourcePageList(requestParams)
|
|
|
+ .then((result) => {
|
|
|
+ console.log('result:', result);
|
|
|
+ if (!result?.rows?.length && result?.total) {
|
|
|
+ const maxPage = Math.ceil(result.total / pageSize.value);
|
|
|
+ if (maxPage && currentPage.value > maxPage) {
|
|
|
+ queryData({ pageNum: maxPage });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ total.value = result?.total || 0;
|
|
|
+ fileData.value =
|
|
|
+ result?.rows?.map?.((d) => {
|
|
|
+ return {
|
|
|
+ key: d.id,
|
|
|
+ name: d.sourceName,
|
|
|
+ url: d.sourceUrl,
|
|
|
+ thumbnail: isImageFile(d) ? d.sourceUrl : void 0,
|
|
|
+ length: formatLength(d.length),
|
|
|
+ updateTime: d.createTime,
|
|
|
+ isDirectory: false,
|
|
|
+ userFile: {
|
|
|
+ ...d,
|
|
|
+ name: d.sourceName,
|
|
|
+ url: d.sourceUrl
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }) || [];
|
|
|
+ console.log('fileData:', fileData.value);
|
|
|
+ handleQueryDone();
|
|
|
+ })
|
|
|
+ .catch((e) => {
|
|
|
+ fileData.value = [];
|
|
|
+ props.messageIns?.error?.(e.message);
|
|
|
+ handleQueryDone();
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.limit,
|
|
|
+ () => {
|
|
|
+ clearSelections();
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => props.baseIndex,
|
|
|
+ (baseIndex) => {
|
|
|
+ contextMenuProps.zIndex = baseIndex;
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ defineExpose({ queryData, clearSelections });
|
|
|
+</script>
|